diff --git a/crates/doc/src/eval_surface.rs b/crates/doc/src/eval_surface.rs index 5282c821..faf8a787 100644 --- a/crates/doc/src/eval_surface.rs +++ b/crates/doc/src/eval_surface.rs @@ -98,9 +98,9 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap, 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, 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, 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, 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 = 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>> { diff --git a/crates/doc/src/lineage.rs b/crates/doc/src/lineage.rs index 5565bd39..3424254c 100644 --- a/crates/doc/src/lineage.rs +++ b/crates/doc/src/lineage.rs @@ -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:` 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], +} + +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> { + 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 = 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:` for the first section's, `side:s.` for section `k`'s), named after /// the section with the most segments (the one each side spans exactly one segment of) and diff --git a/crates/engine/src/cmd/surface.rs b/crates/engine/src/cmd/surface.rs index e78f526a..31810cc0 100644 --- a/crates/engine/src/cmd/surface.rs +++ b/crates/engine/src/cmd/surface.rs @@ -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. diff --git a/crates/kernel/src/build.rs b/crates/kernel/src/build.rs index f4681874..a6e61347 100644 --- a/crates/kernel/src/build.rs +++ b/crates/kernel/src/build.rs @@ -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 { + 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 { } } 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), diff --git a/crates/kernel/src/surfaces.rs b/crates/kernel/src/surfaces.rs index d082fc81..4810df11 100644 --- a/crates/kernel/src/surfaces.rs +++ b/crates/kernel/src/surfaces.rs @@ -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 { 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)> { + 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)], fixed: &[mt::VertexID]) -> Vec { + 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) -> Result { + let history: Option> = sides.iter().map(|(_, p)| p.clone()).collect(); + let shell = mt::Shell::from(sides.into_iter().map(|(f, _)| f).collect::>()); + 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], 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], 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 = chain.iter().flat_map(|s| s.polyline(1e-3)).collect(); @@ -145,21 +170,26 @@ pub fn revolve_sheet(plane: &Plane, chains: &[Vec], 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 = [(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], 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 Vec3>) = match *seg { PathSeg::Line { a, b } => { @@ -207,14 +240,13 @@ pub fn sweep_sheet(plane: &Plane, chains: &[Vec], 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], 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| { - 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)]) -> Result { } 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 = 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::>()); } - (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)]) -> Result { None => crate::build::ruled_loft_faces(&wires, &sources), } })?; - let mut rec = Rebuild::default(); - let faces: Vec = 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). diff --git a/crates/kernel/src/sweep_path.rs b/crates/kernel/src/sweep_path.rs index 8020c448..d6480a29 100644 --- a/crates/kernel/src/sweep_path.rs +++ b/crates/kernel/src/sweep_path.rs @@ -208,27 +208,37 @@ fn ruled_stations(pts: &[Vec3]) -> Result<(Vec, Vec)> { /// 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], path: &[Vec3]) -> Result> { +pub(crate) fn ruled_sheet(plane: &Plane, chains: &[Vec], path: &[Vec3]) -> Result> { 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> = st - .iter() - .map(|s| { - let pl = station_plane(plane, s0, s); - chains.iter().map(|c| crate::build::chain_wire(&pl, c)).collect::>>() - }) - .collect::>()?; + // 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 = chains.iter().scan(0, |o, c| Some(std::mem::replace(o, *o + c.len()))).collect(); + let mut wires: Vec> = Vec::with_capacity(st.len()); + let mut numbers: Vec>> = 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 = 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], 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], path: &[Vec3]) -> Result>> { +pub(crate) fn smooth_sheet(plane: &Plane, chains: &[Vec], path: &[Vec3]) -> Result>> { 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], 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 = 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], 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)) }