Surface walls named after their sketch curves (#757) (#850)

Each wall of a surface built from sketch curves (Surface Extrude, Revolve, Sweep and Loft) is now named after the curve it comes from, exactly rather than from geometry, so references to those faces survive edits.
This commit is contained in:
Irakli Kourtis authored and GitHub committed 2026-10-13 21:47:24 -04:00
1 parent 8746376ee9
commit d2e409f40c
6 files changed
+216 -76

No files matched your search

+20 -10
View File
@@ -98,9 +98,9 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
let d = val(vals, &extent.distance, Kind::Length)?;
let off = extent.start_offset.as_deref().map(|e| val(vals, e, Kind::Length)).transpose()?.unwrap_or(0.0);
let (a, b) = extent_sides(vals, extent, d, extent.direction)?;
let body = kernel::extrude_sheet(&ss.plane, &sheet_chains(&ss, profiles.as_ref(), curves)?, a + off, b + off)?;
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::new(name, body, f.id));
let chains = sheet_chains(&ss, profiles.as_ref(), curves)?;
let body = kernel::extrude_sheet(&ss.plane, &chains, a + off, b + off)?;
push_sheet(st, f, crate::lineage::ChainRoles { sketch: &ss, chains: &chains }.tool(body));
Ok(())
}
FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric } => {
@@ -119,9 +119,9 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
(None, true) => (-a / 2.0, a),
(None, false) => (0.0, a),
};
let body = kernel::revolve_sheet(&ss.plane, &sheet_chains(&ss, profiles.as_ref(), curves)?, o, d, start, sweep)?;
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::new(name, body, f.id));
let chains = sheet_chains(&ss, profiles.as_ref(), curves)?;
let body = kernel::revolve_sheet(&ss.plane, &chains, o, d, start, sweep)?;
push_sheet(st, f, crate::lineage::ChainRoles { sketch: &ss, chains: &chains }.tool(body));
Ok(())
}
FeatureKind::Unstitch { body, faces } => {
@@ -177,12 +177,13 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
Some(pts) => kernel::sweep_sheet_points(&ss.plane, &chains, &pts)?,
None => kernel::sweep_sheet(&ss.plane, &chains, &path_segments(ps, path, start)?)?,
};
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::new(name, body, f.id));
let traced = crate::lineage::ChainRoles { sketch: ss, chains: &chains }.tool(body);
push_sheet(st, f, traced);
Ok(())
}
FeatureKind::SurfaceLoft { sections } => {
let mut secs = Vec::with_capacity(sections.len());
let mut sketches = Vec::with_capacity(sections.len());
for sec in sections {
let ss = sketch_in_feature_frame(
doc,
@@ -201,16 +202,25 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
));
};
secs.push((ss.plane, chain));
sketches.push(ss);
}
let body = kernel::loft_sheet(&secs)?;
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::new(name, body, f.id));
// Each wall named by the section curves it runs between, as a loft's sides are.
let loops: Vec<solidcraft_geom::Loop2> = secs.iter().map(|(_, c)| solidcraft_geom::Loop2 { segs: c.clone() }).collect();
let traced = crate::lineage::SectionRoles { sections: sketches.iter().zip(&loops).collect() }.tool(body);
push_sheet(st, f, traced);
Ok(())
}
_ => Err(DocError::Invalid("not a surface feature".into())),
}
}
/// A surface builder's body, added to the model with what its faces came from.
fn push_sheet(st: &mut ModelState, f: &Feature, traced: crate::lineage::Traced) {
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::traced_new(name, traced, f.id));
}
/// What a surface builder sweeps: the boundary loops of the picked profiles, then the picked
/// curves chained end to end.
fn sheet_chains(ss: &SolvedSketch, profiles: Option<&ProfileSel>, curves: &[String]) -> Result<Vec<Vec<Seg2>>> {
+41
View File
@@ -249,6 +249,47 @@ impl<'a> ProfileRoles<'a> {
}
}
/// How a surface built from chains of sketch segments (Surface Extrude, Revolve, Sweep) names
/// its walls: each `side:<curve>` of the sketch curve its segment lies on exactly, segments
/// numbered through the chains in order. It has no caps.
pub(crate) struct ChainRoles<'a> {
pub sketch: &'a SolvedSketch,
pub chains: &'a [Vec<solidcraft_geom::Seg2>],
}
impl ChainRoles<'_> {
/// The surface `body`, its walls named by their roles where its history gives them.
pub fn tool(&self, body: Body) -> Traced {
let roles = body.history().filter(|h| h.operands == 0).map(|h| self.of(h));
Traced::made(body, roles)
}
fn of(&self, history: &History) -> Vec<Vec<String>> {
let segments: Vec<&solidcraft_geom::Seg2> = self.chains.iter().flatten().collect();
let curves = crate::config::ExactSketchCurves::new(self.sketch);
history
.faces
.iter()
.map(|face| {
let mut roles: Vec<String> = Vec::new();
for origin in &face.origins {
let role = match *origin {
FaceOrigin::Profile { segment } => segments.get(segment).and_then(|s| curves.find_along(s)).map(|id| format!("side:{id}")),
_ => None,
};
match role {
Some(role) if !roles.contains(&role) => roles.push(role),
Some(_) => {}
// One unknown origin leaves the face unknown: never a partial guess.
None => return Vec::new(),
}
}
roles
})
.collect()
}
}
/// How a loft names its faces: each side by the sketch curves of the sections it runs between
/// (`side:<curve>` for the first section's, `side:s<k>.<curve>` for section `k`'s), named after
/// the section with the most segments (the one each side spans exactly one segment of) and