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
+41
View File
@@ -544,6 +544,47 @@ mod tests {
);
}
/// Each wall of a surface built from sketch curves is named after the curve it comes from,
/// exactly (not from geometry): extrude, revolve, sweep and loft.
#[test]
fn surface_walls_are_named_after_their_curves() {
let mut s = Session::default();
run(&mut s, "construct.plane.offset", json!({"base": "XY", "offset": 5, "name": "Mid"}));
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
run(&mut s, "sketch.line", json!({"points": [[3, 0], [3, 4]], "ids": ["l1"]}));
run(&mut s, "sketch.line", json!({"points": [[3, 4], [6, 4]], "ids": ["l2"]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "sketch.create", json!({"plane": "XZ", "name": "P"}));
run(&mut s, "sketch.line", json!({"points": [[0, 0], [0, 5]], "ids": ["up"]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "sketch.create", json!({"plane": "Mid", "name": "T"}));
run(&mut s, "sketch.line", json!({"points": [[3, 0], [3, 4]], "ids": ["m1"]}));
run(&mut s, "sketch.line", json!({"points": [[3, 4], [6, 4]], "ids": ["m2"]}));
run(&mut s, "sketch.finish", json!({}));
let made = [
("surface.extrude", json!({"sketch": "S", "curves": ["l1", "l2"], "distance": 3, "body_name": "E"})),
("surface.revolve", json!({"sketch": "S", "curves": ["l1", "l2"], "axis": "y", "angle": "90 deg", "body_name": "R"})),
("surface.sweep", json!({"sketch": "S", "curves": ["l1", "l2"], "path_sketch": "P", "path": ["up"], "body_name": "W"})),
(
"surface.loft",
json!({"sections": [{"sketch": "S", "curves": ["l1", "l2"]}, {"sketch": "T", "curves": ["m1", "m2"]}], "body_name": "L"}),
),
];
for (cmd, p) in made {
let body = p["body_name"].as_str().unwrap().to_string();
run(&mut s, cmd, p);
let fid = s.doc.features.last().unwrap().id;
let st = s.model.state();
let b = st.body(&body).unwrap();
let names = solidcraft_doc::naming::face_names(b);
for curve in ["l1", "l2"] {
let want = format!("F{fid}:side:{curve}");
let i = names.iter().position(|n| n.starts_with(&want)).unwrap_or_else(|| panic!("{cmd}: no face named {want}: {names:?}"));
assert!(solidcraft_doc::naming::face_name_exact(b, i), "{cmd}: {want} is named from geometry");
}
}
}
/// The planned surface commands are registered but refuse to run: disabled with the
/// issue that tracks them, an error (never a panic) from every caller, and the design left
/// as it was.
+8 -1
View File
@@ -37,6 +37,12 @@ fn wire_sources(plane: &Plane, lp: &Loop2) -> Result<(mt::Wire, Vec<(mt::Edge, S
/// An open wire in 3D through a chain of sketch segments, each starting where the one before
/// ends (a closed chain makes a closed wire).
pub(crate) fn chain_wire(plane: &Plane, segs: &[Seg2]) -> Result<mt::Wire> {
chain_wire_seeds(plane, segs).map(|(wire, _)| wire)
}
/// [`chain_wire`], with each edge's id and the position in the chain of the segment it was
/// built from (a big arc makes two edges), in wire order.
pub(crate) fn chain_wire_seeds(plane: &Plane, segs: &[Seg2]) -> Result<(mt::Wire, Vec<(mt::EdgeID, usize)>)> {
let n = segs.len();
if n == 0 || n > MAX_SEGS {
return Err(KernelError::Invalid(format!("curve chain with {n} segments")));
@@ -50,7 +56,8 @@ pub(crate) fn chain_wire(plane: &Plane, segs: &[Seg2]) -> Result<mt::Wire> {
}
}
let closed = segs.first().zip(segs.last()).is_some_and(|(f, l)| l.end().dist(f.start()) <= 1e-6);
chain_sources(plane, segs, closed).map(|(wire, _)| wire)
let (wire, sources) = chain_sources(plane, segs, closed)?;
Ok((wire, sources.into_iter().map(|(e, _, i)| (e.id(), i)).collect()))
}
/// The edges of a chain of segments (closed: the last one ends on the first's start vertex),
+79 -51
View File
@@ -12,8 +12,8 @@ use solidcraft_geom::{Plane, Region2, Seg2, Vec2, Vec3};
use truck_modeling as mt;
use crate::body::{Body, from_p3, p3};
use crate::history::{Change, EdgeOrigin, FaceOrigin};
use crate::record::{Rebuild, rebuilt_body};
use crate::history::{Change, EdgeOrigin, FaceOrigin, FaceProv};
use crate::record::{self, Rebuild, Side, rebuilt_body};
use crate::{KernelError, Result, guard};
impl Body {
@@ -64,6 +64,33 @@ pub fn patch_region(plane: &Plane, region: &Region2) -> Result<Body> {
Ok(rebuilt_body(surface_body(vec![f]), &[], &rec))
}
/// A chain's edge seeds numbered across all the chains: the segment at `i` of the chain that
/// starts at segment `offset` is segment `offset + i`.
fn numbered(seeds: &[(mt::EdgeID, usize)], offset: usize) -> Vec<(mt::EdgeID, Vec<usize>)> {
seeds.iter().map(|(id, i)| (*id, vec![offset + i])).collect()
}
/// The faces truck swept from the edges `seeds`, each with the segments under the edge whose
/// column it lies in (none where the faces don't follow the pattern; see [`record::follow`]).
fn swept_sides(shell: &mt::Shell, seeds: &[(mt::EdgeID, Vec<usize>)], fixed: &[mt::VertexID]) -> Vec<Side> {
let provs = record::columns(shell.face_iter(), seeds, None, record::Column::ToEnd, fixed);
shell.face_iter().enumerate().map(|(k, f)| (f.clone(), provs.as_ref().and_then(|p| p.get(k).cloned().flatten()))).collect()
}
/// A body of a sheet builder's faces: a surface, or a solid where they close; with the history
/// of a body built from the chains' segments (none unless every face's is known).
fn sheet_body(sides: Vec<Side>) -> Result<Body> {
let history: Option<Vec<FaceProv>> = sides.iter().map(|(_, p)| p.clone()).collect();
let shell = mt::Shell::from(sides.into_iter().map(|(f, _)| f).collect::<Vec<_>>());
if shell.shell_condition() == mt::ShellCondition::Closed {
return match crate::body::Solid::try_new(vec![shell.clone()]) {
Ok(solid) => record::attach(Body::new(solid)?, &[], history, Default::default()),
Err(_) => crate::record::rebuilt(mt::Solid::new_unchecked(vec![shell]), &[], &Rebuild::default()),
};
}
record::attach(surface_of(shell), &[], history, Default::default())
}
/// Most curve chains one surface extrude takes.
const MAX_CHAINS: usize = 10_000;
@@ -87,17 +114,15 @@ pub fn extrude_sheet(plane: &Plane, chains: &[Vec<Seg2>], start: f64, end: f64)
}
let base = plane.offset(lo);
let along = crate::body::v3(plane.normal() * (hi - lo));
let mut rec = Rebuild::default();
let mut faces = Vec::new();
let mut sides = Vec::new();
let mut offset = 0;
for chain in chains {
let wire = crate::build::chain_wire(&base, chain)?;
let (wire, seeds) = crate::build::chain_wire_seeds(&base, chain)?;
let walls: mt::Shell = guard("surface extrude", || Ok(mt::builder::tsweep(&wire, along)))?;
for f in walls.face_iter() {
rec.made(f, Vec::new());
faces.push(f.clone());
}
sides.extend(swept_sides(&walls, &numbered(&seeds, offset), &[]));
offset += chain.len();
}
Ok(rebuilt_body(surface_body(faces), &[], &rec))
sheet_body(sides)
}
/// A surface revolved from chains of sketch segments on `plane` about the sketch line through
@@ -125,8 +150,8 @@ pub fn revolve_sheet(plane: &Plane, chains: &[Vec<Seg2>], axis_origin: Vec2, axi
let on_axis = |p: Vec2| side(p).abs() < 1e-7;
let (origin, n) = (plane.to_world(axis_origin), plane.normal());
let axis = plane.dir_to_world(d).normalized().ok_or_else(|| KernelError::Invalid("revolve frame has no axis".into()))?;
let mut rec = Rebuild::default();
let mut faces = Vec::new();
let mut sides = Vec::new();
let mut offset = 0;
for chain in chains {
// On one side of the axis; the point farthest from it fixes the turning sense.
let pts: Vec<Vec2> = chain.iter().flat_map(|s| s.polyline(1e-3)).collect();
@@ -145,21 +170,26 @@ pub fn revolve_sheet(plane: &Plane, chains: &[Vec<Seg2>], axis_origin: Vec2, axi
if closed && pts.iter().any(|p| on_axis(*p)) {
return Err(KernelError::Invalid("not supported yet: a closed curve touching the revolve axis".into()));
}
let wire = crate::build::chain_wire(plane, chain)?;
// The chain starts `start` round: its sketch frame turned there, so the wire's own edges
// are the ones swept (and its faces can be followed back to them).
let turn = |v: Vec3| crate::build::rotate_about(v, Vec3::ZERO, a, start);
let turned = Plane { origin: crate::build::rotate_about(plane.origin, origin, a, start), x: turn(plane.x), y: turn(plane.y) };
let (wire, seeds) = crate::build::chain_wire_seeds(&turned, chain)?;
let fixed: Vec<mt::VertexID> = [(on_axis(first), wire.front_vertex()), (on_axis(last), wire.back_vertex())]
.into_iter()
.filter_map(|(apex, v)| v.filter(|_| apex).map(|v| v.id()))
.collect();
let walls: mt::Shell = guard("surface revolve", || {
let wire = mt::builder::rotated(&wire, p3(origin), crate::body::v3(a), mt::Rad(start));
Ok(match (on_axis(first), on_axis(last)) {
(true, _) => mt::builder::cone(&wire, crate::body::v3(a), mt::Rad(sweep)),
(false, true) => mt::builder::cone(&wire.inverse(), crate::body::v3(a), mt::Rad(sweep)),
(false, false) => mt::builder::rsweep(&wire, p3(origin), crate::body::v3(a), mt::Rad(sweep)),
})
})?;
for f in walls.face_iter() {
rec.made(f, Vec::new());
faces.push(f.clone());
}
sides.extend(swept_sides(&walls, &numbered(&seeds, offset), &fixed));
offset += chain.len();
}
body_of(faces, &[], &rec)
sheet_body(sides)
}
/// A surface swept from chains of sketch segments on `plane` along a path that starts near
@@ -191,10 +221,13 @@ pub fn sweep_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[crate::PathSeg])
}
return sweep_sheet_points(plane, chains, &pts);
}
let mut rec = Rebuild::default();
let mut faces = Vec::new();
let mut sides = Vec::new();
let mut offset = 0;
for chain in chains {
let mut cur = crate::build::chain_wire(plane, chain)?;
let (mut cur, seeds) = crate::build::chain_wire_seeds(plane, chain)?;
// Each edge's segments: the next stretch of the path starts from the edges the last one
// carried them onto, in the same order.
let mut seeds = numbered(&seeds, offset);
for seg in path {
let (part, map): (mt::Shell, Box<dyn Fn(Vec3) -> Vec3>) = match *seg {
PathSeg::Line { a, b } => {
@@ -207,14 +240,13 @@ pub fn sweep_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[crate::PathSeg])
}
};
let next = far_wire(&part, &cur, map.as_ref())?;
for f in part.face_iter() {
rec.made(f, Vec::new());
faces.push(f.clone());
}
sides.extend(swept_sides(&part, &seeds, &[]));
seeds = next.edge_iter().map(|e| e.id()).zip(seeds.into_iter().map(|(_, n)| n)).collect();
cur = next;
}
offset += chain.len();
}
body_of(faces, &[], &rec)
sheet_body(sides)
}
/// A surface swept from chains of sketch segments on `plane` along a 3D polyline path: one smooth
@@ -227,18 +259,11 @@ pub fn sweep_sheet_points(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) ->
if chains.is_empty() || chains.len() > MAX_CHAINS {
return Err(KernelError::Invalid("pick the curves or profiles to sweep".into()));
}
let sheet = |faces: Vec<mt::Face>| {
let mut rec = Rebuild::default();
for f in &faces {
rec.made(f, Vec::new());
}
body_of(faces, &[], &rec)
};
let ruled = sheet(crate::sweep_path::ruled_sheet(plane, chains, path)?)?;
let ruled = sheet_body(crate::sweep_path::ruled_sheet(plane, chains, path)?)?;
// Smooth walls where the path has no corners, kept when their area agrees with the ruled
// walls' (a fit that went wrong falls back to those).
if let Ok(Some(faces)) = crate::sweep_path::smooth_sheet(plane, chains, path)
&& let Ok(smooth) = sheet(faces)
if let Ok(Some(sides)) = crate::sweep_path::smooth_sheet(plane, chains, path)
&& let Ok(smooth) = sheet_body(sides)
&& let (Ok(a), Ok(b)) = (crate::measure(&smooth), crate::measure(&ruled))
&& (a.area - b.area).abs() <= 0.02 * b.area.abs()
{
@@ -310,25 +335,33 @@ pub fn loft_sheet(sections: &[(Plane, Vec<Seg2>)]) -> Result<Body> {
} else {
let n = sections.iter().map(|(_, c)| c.len()).max().unwrap_or(0);
let mut wires = Vec::with_capacity(sections.len());
let mut sources = Vec::with_capacity(sections.len());
let mut prev: Option<(Vec3, Vec3)> = None;
// Segments are numbered through the sections in the order given.
let mut offset = 0;
for (plane, chain) in sections {
let mut segs = chain.clone();
// Each piece with the segment it is (part of).
let mut segs: Vec<(Seg2, usize)> = chain.iter().enumerate().map(|(i, s)| (*s, offset + i)).collect();
offset += chain.len();
// Split the longest segment in half until the counts match.
while segs.len() < n {
let Some(i) = segs.iter().enumerate().max_by(|a, b| a.1.length().total_cmp(&b.1.length())).map(|(i, _)| i) else { break };
let (a, b) = segs[i].split_half();
segs.splice(i..=i, [a, b]);
let Some(i) = segs.iter().enumerate().max_by(|a, b| a.1.0.length().total_cmp(&b.1.0.length())).map(|(i, _)| i) else { break };
let (s, k) = segs[i];
let (a, b) = s.split_half();
segs.splice(i..=i, [(a, k), (b, k)]);
}
let ends = |c: &[Seg2]| c.first().zip(c.last()).map(|(f, l)| (plane.to_world(f.start()), plane.to_world(l.end())));
let ends = |c: &[(Seg2, usize)]| c.first().zip(c.last()).map(|(f, l)| (plane.to_world(f.0.start()), plane.to_world(l.0.end())));
if let (Some((ps, pe)), Some((s, e))) = (prev, ends(&segs))
&& s.dist(ps) + e.dist(pe) > s.dist(pe) + e.dist(ps)
{
segs = segs.iter().rev().map(Seg2::reversed).collect();
segs = segs.iter().rev().map(|(s, k)| (s.reversed(), *k)).collect();
}
prev = ends(&segs);
wires.push(crate::build::uniform_chain_wire(plane, &segs, false)?);
let pieces: Vec<Seg2> = segs.iter().map(|(s, _)| *s).collect();
wires.push(crate::build::uniform_chain_wire(plane, &pieces, false)?);
sources.push(segs.iter().map(|(_, k)| vec![*k]).collect::<Vec<_>>());
}
(wires, Vec::new())
(wires, sources)
};
let faces = guard("surface loft", || {
let smooth = if wires.len() >= 3 { crate::build::smooth_loft_faces(&wires, &sources, all_closed).ok() } else { None };
@@ -337,12 +370,7 @@ pub fn loft_sheet(sections: &[(Plane, Vec<Seg2>)]) -> Result<Body> {
None => crate::build::ruled_loft_faces(&wires, &sources),
}
})?;
let mut rec = Rebuild::default();
let faces: Vec<mt::Face> = faces.into_iter().map(|(f, _)| f).collect();
for f in &faces {
rec.made(f, Vec::new());
}
body_of(faces, &[], &rec)
sheet_body(faces)
}
/// The edge of a body nearest a point (by samples along each edge).
+27 -14
View File
@@ -208,27 +208,37 @@ fn ruled_stations(pts: &[Vec3]) -> Result<(Vec<Vec3>, Vec<Station>)> {
/// Walls of chains of sketch segments on `plane` (open, or closed) carried along a 3D polyline
/// path that starts near them: ruled between stations at the path's points, mitred at corners,
/// with no caps. Neighbouring walls share their edges.
pub(crate) fn ruled_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Vec<mt::Face>> {
pub(crate) fn ruled_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Vec<Side>> {
let (pts, st) = ruled_stations(&clean(path)?)?;
let edges: usize = chains.iter().map(Vec::len).sum();
if st.len().saturating_mul(edges) > 400_000 {
return Err(KernelError::Invalid("the sweep is too detailed (curves × path points)".into()));
}
let s0 = pts.first().copied().unwrap_or_default();
let wires: Vec<Vec<mt::Wire>> = st
.iter()
.map(|s| {
let pl = station_plane(plane, s0, s);
chains.iter().map(|c| crate::build::chain_wire(&pl, c)).collect::<Result<Vec<_>>>()
})
.collect::<Result<_>>()?;
// Each chain's segments are numbered on from the last chain's; every station's wire of a
// chain has the same edges, each over the same segment.
let offsets: Vec<usize> = chains.iter().scan(0, |o, c| Some(std::mem::replace(o, *o + c.len()))).collect();
let mut wires: Vec<Vec<mt::Wire>> = Vec::with_capacity(st.len());
let mut numbers: Vec<Vec<Vec<usize>>> = Vec::new();
for s in &st {
let pl = station_plane(plane, s0, s);
let mut at = Vec::with_capacity(chains.len());
for (c, offset) in chains.iter().zip(&offsets) {
let (wire, seeds) = crate::build::chain_wire_seeds(&pl, c)?;
if numbers.len() < chains.len() {
numbers.push(seeds.iter().map(|(_, i)| vec![offset + i]).collect());
}
at.push(wire);
}
wires.push(at);
}
guard("path sweep", || {
let mut faces = Vec::new();
let mut faces: Vec<Side> = Vec::new();
for w in wires.windows(2) {
let (Some(w0), Some(w1)) = (w.first(), w.get(1)) else { continue };
for (a, b) in w0.iter().zip(w1) {
for ((a, b), n) in w0.iter().zip(w1).zip(&numbers) {
let sides: mt::Shell = builder::try_wire_homotopy(a, b).map_err(|e| KernelError::Failed(format!("path sweep: {e}")))?;
faces.extend(sides.face_iter().cloned());
faces.extend(record::ruled(&sides, a, b, n, n));
}
}
Ok(faces)
@@ -239,7 +249,7 @@ pub(crate) fn ruled_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) ->
/// turn sharper than [`CORNER`]): one B-spline wall per segment through evenly spaced,
/// rotation-minimising stations, neighbouring walls sharing their seam edges; no caps. `None`
/// for a path with corners (see [`ruled_sheet`]).
pub(crate) fn smooth_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Option<Vec<mt::Face>>> {
pub(crate) fn smooth_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -> Result<Option<Vec<Side>>> {
let pts = clean(path)?;
if pts.windows(3).any(|w| turn(w[1] - w[0], w[2] - w[1]) > CORNER) {
return Ok(None);
@@ -255,7 +265,8 @@ pub(crate) fn smooth_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -
let s0 = st.first().map(|s| s.at).unwrap_or_default();
let at = |i: usize, q: Vec2| st.get(i).map(|s| s.at + s.apply(plane.to_world(q) - s0)).unwrap_or_default();
let n = st.len() - 1;
let mut faces = Vec::new();
let mut faces: Vec<Side> = Vec::new();
let mut offset = 0;
for chain in chains {
let closed = chain.first().zip(chain.last()).is_some_and(|(f, l)| l.end().dist(f.start()) <= 1e-6);
// Each segment: its curve at the first and last station, the columns through its start
@@ -320,7 +331,9 @@ pub(crate) fn smooth_sheet(plane: &Plane, chains: &[Vec<Seg2>], path: &[Vec3]) -
}
Ok(walls)
})?;
faces.extend(walls);
// Wall j comes from segment j of the chain.
faces.extend(walls.into_iter().enumerate().map(|(j, w)| (w, Some(record::profile(&[offset + j])))));
offset += chain.len();
}
Ok(Some(faces))
}