feat: preserve body history and placed modelling workflows

Record ownership transfers in the timeline so earlier Join/Cut membership,
world placement, machining data and styles survive component moves, edits,
independent copying, undo and reopening. Preserve pending positions in history
snapshots and keep constructor transfers ordered during feature redefinition.

Retain shallow sheet pockets and exact planar intersection islands through
unfold/refold, with depth-aware flat previews and DXF export. Store explicit
3D wire coordinate spaces and migrate known legacy provenance without moving
saved points or analytic fits.

Infer unique eligible cylindrical Emboss targets, align Revolve manipulators
with actual extent geometry, and discard obsolete preview errors after input
correction. Certify constructed and immutable imported face styles using exact
source metadata while preserving legacy face names. Upgrade designs to format10
with bounded validation and geometry-preserving legacy migration.
This commit is contained in:
bherbruck committed 2026-10-11 03:59:12 -04:00
1 parent 0398a813c8
commit c1d819f235
86 files changed
+6011 -488

No files matched your search

+106 -36
View File
@@ -29,7 +29,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("solid.emboss", "Emboss", emboss)
.at("SOLID", "CREATE")
.icon("emboss")
.params("sketch, profiles?; depth; mode?: emboss|deboss; targets?; face?: world point [x,y,z] selecting a planar or complete exterior cylindrical wall (required for wrapped orthogonal line profiles)"),
.params("sketch, profiles?; depth; mode?: emboss|deboss; targets?; face?: world point [x,y,z] selecting a planar or complete exterior cylindrical wall (inferred when exactly one supported tangent cylinder matches the selected profiles)"),
CommandSpec::new("solid.replace_face", "Replace Face", replace_face)
.at("SOLID", "MODIFY")
.icon("offset_face")
@@ -192,50 +192,120 @@ fn emboss(s: &mut Session, p: &Value) -> Result<Value> {
// angular position, and certify their distance before storing an
// exact surface anchor for later replay.
let at = solvecraft_kernel::cylinder_emboss_anchor(body, index, at).map_err(|e| bad(cmd, e.to_string()))?;
let original = s.model.state();
let owner = s.doc.body_component(name, selected.feature);
let paths: Vec<_> = s
.doc
.placements()
.into_iter()
.filter(|(comp, path, _)| *comp == owner && s.doc.world_body_name(&original, name, owner, path) == selected.name)
.map(|(_, path, _)| path)
.collect();
if paths.len() != 1 {
return Err(bad(cmd, "selected cylinder placement is ambiguous"));
}
let path = paths.into_iter().next().ok_or_else(|| bad(cmd, "selected cylinder placement"))?;
if path.len() > 64 {
return Err(bad(cmd, "selected cylinder placement is too deep"));
}
let author_path = s.active_occurrence_path.clone().unwrap_or_else(|| s.doc.first_path(s.active_component));
if author_path.len() > 64 {
return Err(bad(cmd, "author placement is too deep"));
}
let sketch_owner = s.doc.feature(sketch).map(|f| f.component).ok_or_else(|| bad(cmd, "profile sketch no longer exists"))?;
let sketch_path = if sketch_owner == owner {
path.clone()
} else if sketch_owner == s.active_component {
author_path.clone()
} else {
s.doc.first_path(sketch_owner)
};
if path.iter().chain(&author_path).chain(&sketch_path).any(|id| s.pending_moves.contains_key(id)) {
return Err(bad(cmd, "capture pending source, author and profile positions with Capture Position before cylinder emboss"));
}
let inverse = solvecraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid active frame"))?;
let at = solvecraft_doc::apply_point(&inverse, at);
let face = emboss_selection(s, selected, at)?;
check_emboss_positions(s, sketch, selected, &face)?;
targets = vec![name.to_owned()];
Some(solvecraft_doc::EmbossFace { body: name.to_owned(), at, path, author_path })
Some(face)
}
_ => return Err(bad(cmd, "only planar faces and complete exterior cylindrical walls support emboss")),
}
} else {
None
let inferred = infer_emboss_selection(s, sketch, &profiles, &depth, deboss, &targets)?;
if let Some(face) = &inferred {
targets = vec![face.body.clone()];
}
inferred
};
add_feature(s, p, FeatureKind::Emboss { sketch, profiles, depth, deboss, targets, face })
}
fn emboss_selection(s: &Session, selected: &solvecraft_doc::ModelBody, at: Vec3) -> Result<solvecraft_doc::EmbossFace> {
let cmd = "solid.emboss";
let name = selected.definition_name();
let original = s.model.state();
let owner = s.doc.model_body_component(selected);
let paths: Vec<_> = s
.doc
.placements()
.into_iter()
.filter(|(comp, path, _)| *comp == owner && s.doc.world_body_name(&original, name, owner, path) == selected.name)
.map(|(_, path, _)| path)
.collect();
if paths.len() != 1 {
return Err(bad(cmd, "selected cylinder placement is ambiguous"));
}
let path = paths.into_iter().next().ok_or_else(|| bad(cmd, "selected cylinder placement"))?;
let author_path = s.active_occurrence_path.clone().unwrap_or_else(|| s.doc.first_path(s.active_component));
if path.len() > 64 || author_path.len() > 64 {
return Err(bad(cmd, "cylinder placement is too deep"));
}
let inverse = solvecraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid active frame"))?;
Ok(solvecraft_doc::EmbossFace { body: name.to_owned(), at: solvecraft_doc::apply_point(&inverse, at), path, author_path })
}
fn check_emboss_positions(s: &Session, sketch: u64, body: &solvecraft_doc::ModelBody, face: &solvecraft_doc::EmbossFace) -> Result<()> {
let cmd = "solid.emboss";
let owner = s.doc.model_body_component(body);
let sketch_owner = s.doc.feature(sketch).map(|f| f.component).ok_or_else(|| bad(cmd, "profile sketch no longer exists"))?;
let sketch_path = if sketch_owner == owner {
face.path.clone()
} else if sketch_owner == s.active_component {
face.author_path.clone()
} else {
s.doc.first_path(sketch_owner)
};
if face.path.iter().chain(&face.author_path).chain(&sketch_path).any(|id| s.pending_moves.contains_key(id)) {
return Err(bad(cmd, "capture pending source, author and profile positions with Capture Position before cylinder emboss"));
}
Ok(())
}
fn infer_emboss_selection(
s: &Session,
sketch: u64,
profiles: &solvecraft_doc::ProfileSel,
depth: &str,
deboss: bool,
targets: &[String],
) -> Result<Option<solvecraft_doc::EmbossFace>> {
let cmd = "solid.emboss";
let original = s.model.state();
let ss = original.sketch(sketch).ok_or_else(|| bad(cmd, "the sketch is not evaluated"))?;
let regions = solvecraft_doc::select_profiles(ss, profiles).map_err(|e| bad(cmd, e.to_string()))?;
let sketch_owner = s.doc.feature(sketch).map(|f| f.component).ok_or_else(|| bad(cmd, "profile sketch no longer exists"))?;
let d = s.doc.eval(&s.doc.with_design_unit(depth, Kind::Length), Kind::Length).map_err(|e| bad(cmd, e.to_string()))?;
let world = s.world_state();
let placed = s.placed_document();
let mut chosen = None;
let mut faces = 0usize;
for selected in &world.bodies {
let name = selected.definition_name();
if !targets.is_empty() && !targets.iter().any(|n| n == name || n == &selected.name) {
continue;
}
let raw = original.body(name).ok_or_else(|| bad(cmd, "target definition no longer exists"))?;
faces = faces.saturating_add(raw.body.face_count());
if faces > 50_000 {
return Err(bad(cmd, "cylinder target search exceeds face work limit"));
}
if !solvecraft_kernel::has_cylinder_emboss_wall(&raw.body).map_err(|e| bad(cmd, e.to_string()))? {
continue;
}
let mut face = emboss_selection(s, selected, Vec3::ZERO)?;
let body_frame = face.body_frame(&placed, raw).map_err(|e| bad(cmd, e.to_string()))?;
let inverse = solvecraft_doc::mat_inverse(&body_frame).ok_or_else(|| bad(cmd, "invalid cylinder body frame"))?;
let displayed = world.sketch(sketch).ok_or_else(|| bad(cmd, "the profile sketch is not displayed"))?;
let plane = solvecraft_geom::Plane::new(
solvecraft_doc::apply_point(&inverse, displayed.plane.origin),
solvecraft_doc::apply_vector(&inverse, displayed.plane.x),
solvecraft_doc::apply_vector(&inverse, displayed.plane.y),
)
.ok_or_else(|| bad(cmd, "invalid cylinder sketch frame"))?;
if let Some((_, at)) =
solvecraft_kernel::infer_cylinder_emboss(&raw.body, &plane, &regions, if deboss { -d } else { d }).map_err(|e| bad(cmd, e.to_string()))?
{
face.sketch_frame(&placed, s.active_component, sketch_owner, raw).map_err(|e| bad(cmd, e.to_string()))?;
check_emboss_positions(s, sketch, selected, &face)?;
let author_inverse = solvecraft_doc::mat_inverse(&s.active_component_frame()).ok_or_else(|| bad(cmd, "invalid author frame"))?;
face.at = solvecraft_doc::apply_point(&solvecraft_doc::mat_mul(&author_inverse, &body_frame), at);
if chosen.is_some() {
return Err(bad(cmd, "multiple tangent cylinders match the selected profiles; select a target face or body"));
}
chosen = Some(face);
}
}
Ok(chosen)
}
fn face_points(p: &Value, k: &str, cmd: &str) -> Result<Vec<Vec3>> {
let a = p.get(k).and_then(Value::as_array).ok_or_else(|| bad(cmd, format!("`{k}` must list points [x, y, z]")))?;
if a.is_empty() || a.len() > 1000 {