feat: stabilize face attachments and closed spline workflows

This commit is contained in:
bherbruck committed 2026-10-11 05:32:46 -04:00
1 parent c1d819f235
commit a6bb8fc924
83 files changed
+6271 -385

No files matched your search

+95 -6
View File
@@ -16,7 +16,7 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("body.rename", "Rename Body", rename_body).params("body: its name, name: the new name (features that use the body follow)"),
CommandSpec::new("sketch.redefine", "Redefine Sketch Plane", redefine_sketch)
.icon("plane")
.params("sketch: id or name; plane: XY|XZ|YZ | construction plane name | {face: [x,y,z]} | {origin, normal} (the sketch keeps its geometry)"),
.params("sketch: id or name; plane: as sketch.create; author_path?: exact owner occurrence ids; explicitly rebinds or freezes the plane while preserving geometry and dimension names"),
CommandSpec::new("browser.group", "New Group", group)
.icon("folder")
.params("folder: bodies|sketches|construction; component?: id (default 0); name?; items?: [keys] (they leave other groups)"),
@@ -461,6 +461,19 @@ fn rename_body(s: &mut Session, p: &Value) -> Result<Value> {
// Later features refer to bodies by name.
let mut failed = None;
for f in d.features.iter_mut().filter(|f| f.id != fid) {
if let FeatureKind::Sketch { plane, sketch } = &mut f.kind
&& let Some(anchor) = super::sketch::face::anchor_mut(plane)
&& anchor.target.body == old
{
let ids = std::collections::BTreeMap::from([(anchor.target.origin_feature, anchor.target.origin_feature)]);
let bodies = std::collections::BTreeMap::from([(old.clone(), name.to_string())]);
anchor.remap_target(&ids, &bodies)?;
for link in &mut sketch.links {
if link.auto_face && matches!(&link.source,solvecraft_sketch::LinkSource::Face {body,..} if *body==old) {
link.face_name = Some(anchor.target.name.clone());
}
}
}
let Ok(mut v) = serde_json::to_value(&f.kind) else { continue };
if replace_str(&mut v, &old, name, 0) {
match serde_json::from_value(v) {
@@ -519,13 +532,89 @@ fn redefine_sketch(s: &mut Session, p: &Value) -> Result<Value> {
if p.get("plane").is_none() {
return Err(bad(cmd, "`plane` is required"));
}
let plane = super::sketch::plane_ref(s, p, cmd)?;
let (vals, _) = s.doc.param_values();
s.doc.resolve_plane_in(&vals, &plane, 0, Some(&s.model.state()))?;
if let Some(FeatureKind::Sketch { plane: slot, .. }) = s.doc_mut().feature_mut(id).map(|f| &mut f.kind) {
let feature = s.doc.feature(id).ok_or_else(|| bad(cmd, "sketch disappeared"))?;
let component = feature.component;
let index = s.doc.feature_index(id).ok_or_else(|| bad(cmd, "sketch disappeared"))?;
let author_path = super::sketch::author_path_for_rebind(s, feature, p, cmd)?;
let marker = s.doc.marker;
let mut baseline = s.scratch();
baseline.doc_mut().marker = Some(index + 1);
baseline.refresh();
let visible_geometry = baseline.model.state().sketch(id).map(|resolved| resolved.sketch.clone());
let mut trial = s.scratch();
trial.active_component = component;
trial.active_occurrence_path = (!author_path.is_empty()).then_some(author_path);
// A new attachment may only depend on geometry preceding this sketch.
trial.doc_mut().marker = Some(index);
trial.refresh();
let old_anchor = trial
.doc
.feature(id)
.and_then(|feature| match &feature.kind {
FeatureKind::Sketch { plane, .. } => super::sketch::face::anchor(plane),
_ => None,
})
.cloned();
let plane = super::sketch::plane_ref(&trial, p, cmd)?;
if super::sketch::face::is_face(p) {
super::sketch::face::capture_positions(&mut trial, &plane)?;
}
if let Some(anchor) = old_anchor
&& super::sketch::face::anchor(&plane)
.is_none_or(|next| next.target != anchor.target || next.component != anchor.component || next.path != anchor.path)
{
let sketch = trial.doc_mut().sketch_mut(id)?;
if let Some(mut visible) = visible_geometry {
let constraints: std::collections::BTreeMap<_, _> =
sketch.constraints.iter().map(|constraint| (constraint.id.clone(), constraint.clone())).collect();
for constraint in &mut visible.constraints {
if let Some(original) = constraints.get(&constraint.id) {
if std::mem::discriminant(&constraint.kind) != std::mem::discriminant(&original.kind) {
return Err(bad(cmd, "a dimension role changed during border refresh; repair it explicitly before rebinding"));
}
// Keep evaluated entity indices after linked topology rebuilds.
constraint.param = original.param.clone();
constraint.driven = original.driven;
constraint.text = original.text;
if let Some(value) = original.kind.value() {
constraint.kind.set_value(value);
}
}
}
*sketch = visible;
}
let affected: Vec<_> = sketch
.links
.iter()
.filter(|link| {
link.auto_face
&& link.face_name.as_ref() == Some(&anchor.target.name)
&& matches!(&link.source,solvecraft_sketch::LinkSource::Face {body,..} if *body==anchor.target.body)
})
.map(|link| link.id.clone())
.collect();
for link in affected {
sketch.break_link(&link)?;
}
}
if let Some(FeatureKind::Sketch { plane: slot, .. }) = trial.doc_mut().feature_mut(id).map(|feature| &mut feature.kind) {
*slot = plane;
}
Ok(json!({ "sketch": id }))
trial.doc_mut().marker = Some(index + 1);
trial.refresh();
if let Some(error) = trial.model.result(id).and_then(|result| result.error.as_ref()) {
return Err(bad(cmd, error.clone()));
}
trial.doc_mut().marker = marker;
trial.refresh();
s.doc = trial.doc;
s.pending_moves = trial.pending_moves;
let attachment = s.doc.feature(id).and_then(|feature| match &feature.kind {
FeatureKind::Sketch { plane, .. } => Some(if super::sketch::face::anchor(plane).is_some() { "exact_face" } else { "fixed" }),
_ => None,
});
Ok(json!({ "sketch": id,"attachment":attachment }))
}
#[cfg(test)]