refactor(kernel): number profile loops once and record face history as faces are built
One numbered loop type (crate::numbered) carries each segment's profile numbers through orientation, snapping, circle re-seaming, re-sampling and rotation; every builder orients its regions through it once. Builders that make their own side faces pair each face with its provenance as it is made (ruled walls by the wire edges each face holds, smooth lofts, coils and path sweeps by segment), and cap_and_close appends the caps itself. Column following is kept only for faces truck sweeps internally (rsweep, tsweep along paths, cones, where an apex vertex stays put). Extrude derives face sources and history in one topology pass. One attach entry point takes operands with their edges already indexed. Geometry, face order, face sources and recorded origins are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
This commit is contained in:
1 parent
8c81bc60d0
commit
d69ebbee1b
13 files changed
+705
-527
No files matched your search
+242
-285
@@ -5,6 +5,9 @@ use solvecraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
|
||||
use truck_modeling as mt;
|
||||
|
||||
use crate::body::{Body, Solid, from_p3, p3, v3};
|
||||
use crate::history::FaceProv;
|
||||
use crate::numbered::{self, NumberedLoop, NumberedRegion};
|
||||
use crate::record::{self, Side};
|
||||
use crate::{KernelError, Result, guard};
|
||||
|
||||
const MAX_SEGS: usize = 20_000;
|
||||
@@ -60,34 +63,61 @@ fn wire_sources(plane: &Plane, lp: &Loop2) -> Result<(mt::Wire, Vec<(mt::Edge, S
|
||||
Ok((edges.into(), sources))
|
||||
}
|
||||
|
||||
/// A region's wires, its loops oriented as the builders build them (outer counter-clockwise,
|
||||
/// holes clockwise); each edge with its segment; and each edge with the profile segments under
|
||||
/// it (`origins[k][i]`: loop `k`, segment `i` as oriented; see [`crate::record::loop_origins`]).
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn region_wires(
|
||||
plane: &Plane,
|
||||
r: &Region2,
|
||||
origins: &[Vec<Vec<usize>>],
|
||||
) -> Result<(Vec<mt::Wire>, Vec<(mt::Edge, Seg2)>, Vec<(mt::EdgeID, Vec<usize>)>)> {
|
||||
let mut wires = Vec::with_capacity(1 + r.holes.len());
|
||||
let (mut sources, mut seeds) = (Vec::new(), Vec::new());
|
||||
for (k, lp) in std::iter::once(r.outer.ccw()).chain(r.holes.iter().map(|h| h.ccw().reversed())).enumerate() {
|
||||
let (w, s) = wire_sources(plane, &lp)?;
|
||||
for (edge, segment, i) in s {
|
||||
if let Some(o) = origins.get(k).and_then(|l| l.get(i)) {
|
||||
seeds.push((edge.id(), o.clone()));
|
||||
}
|
||||
sources.push((edge, segment));
|
||||
}
|
||||
wires.push(w);
|
||||
/// An edge of a profile wire: the segment it was built from and the profile segments under it.
|
||||
pub(crate) struct ProfileEdge {
|
||||
pub edge: mt::Edge,
|
||||
pub segment: Seg2,
|
||||
pub numbers: Vec<usize>,
|
||||
}
|
||||
|
||||
/// A wire built from a profile loop, with its edges in wire order.
|
||||
pub(crate) struct ProfileWire {
|
||||
pub wire: mt::Wire,
|
||||
pub edges: Vec<ProfileEdge>,
|
||||
}
|
||||
|
||||
impl ProfileWire {
|
||||
/// The profile segments under each edge, in wire order.
|
||||
fn numbers(&self) -> Vec<Vec<usize>> {
|
||||
self.edges.iter().map(|e| e.numbers.clone()).collect()
|
||||
}
|
||||
Ok((wires, sources, seeds))
|
||||
}
|
||||
|
||||
/// The wires of a region oriented as the builders build it ([`NumberedRegion::oriented`]), each
|
||||
/// with what its edges were built from.
|
||||
fn region_wires(plane: &Plane, r: &NumberedRegion) -> Result<Vec<ProfileWire>> {
|
||||
r.loops()
|
||||
.map(|lp| {
|
||||
let (wire, s) = wire_sources(plane, &lp.lp)?;
|
||||
let edges = s.into_iter().map(|(edge, segment, i)| ProfileEdge { edge, segment, numbers: lp.numbers(i).to_vec() }).collect();
|
||||
Ok(ProfileWire { wire, edges })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The wires alone.
|
||||
fn wires_of(profile: &[ProfileWire]) -> Vec<mt::Wire> {
|
||||
profile.iter().map(|p| p.wire.clone()).collect()
|
||||
}
|
||||
|
||||
/// The profile edges as sweep seeds: each edge with the profile segments under it.
|
||||
fn seeds(profile: &[ProfileWire]) -> Vec<(mt::EdgeID, Vec<usize>)> {
|
||||
profile.iter().flat_map(|p| &p.edges).map(|e| (e.edge.id(), e.numbers.clone())).collect()
|
||||
}
|
||||
|
||||
/// A region numbered on its own, as given.
|
||||
fn numbered_region(r: &Region2) -> Result<NumberedRegion> {
|
||||
numbered::number_regions(std::slice::from_ref(r)).pop().ok_or_else(|| KernelError::Invalid("profile".into()))
|
||||
}
|
||||
|
||||
/// A region numbered on its own and oriented as the builders build it.
|
||||
fn oriented(r: &Region2) -> Result<NumberedRegion> {
|
||||
numbered_region(r).map(|n| n.oriented())
|
||||
}
|
||||
|
||||
/// A planar face from a region (outer loop with holes).
|
||||
pub(crate) fn face(plane: &Plane, r: &Region2) -> Result<mt::Face> {
|
||||
let (wires, ..) = region_wires(plane, r, &[])?;
|
||||
attach_profile(&wires)
|
||||
attach_profile(&wires_of(®ion_wires(plane, &oriented(r)?)?))
|
||||
}
|
||||
|
||||
fn attach_profile(wires: &[mt::Wire]) -> Result<mt::Face> {
|
||||
@@ -139,78 +169,81 @@ pub fn extrude(plane: &Plane, regions: &[Region2], start: f64, end: f64) -> Resu
|
||||
for r in regions {
|
||||
region_ok(r)?;
|
||||
let base = plane.offset(lo);
|
||||
let (wires, sources, seeds) = region_wires(&base, r, &crate::record::loop_origins(std::slice::from_ref(r)))?;
|
||||
let profile = region_wires(&base, &oriented(r)?)?;
|
||||
let wires = wires_of(&profile);
|
||||
let f = guard("extrude profile", || builder::try_attach_plane(&wires).map_err(|e| KernelError::Invalid(format!("profile: {e}"))))?;
|
||||
let solid: Solid = guard("extrude", || Ok(builder::tsweep(&f, v3(n * (hi - lo)))))?;
|
||||
let history = guard("extrude history", || Ok(crate::record::swept_face(&solid, &f, &seeds, false)))?;
|
||||
let upper = guard("extrude cap identities", || {
|
||||
let inputs: std::collections::HashSet<_> = sources.iter().map(|(edge, _)| edge.id()).collect();
|
||||
let body = Body::new(solid)?;
|
||||
// tsweep retains each actual input boundary edge on its generated side: one pass over
|
||||
// the faces matches topology identity (never face order, surface samples or
|
||||
// centroids) for both the face sources and the history. The profile face is the start
|
||||
// cap, the one face holding no profile edge the end cap.
|
||||
let (sources, history) = guard("extrude source identities", || {
|
||||
let by_edge: std::collections::HashMap<mt::EdgeID, &ProfileEdge> =
|
||||
profile.iter().flat_map(|p| &p.edges).map(|e| (e.edge.id(), e)).collect();
|
||||
let mut faces: Vec<(mt::FaceID, Vec<&ProfileEdge>)> = Vec::new();
|
||||
let mut visits = 0usize;
|
||||
let mut candidate = None;
|
||||
for (index, generated) in solid.face_iter().enumerate() {
|
||||
if index >= 50_000 {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut side = false;
|
||||
for generated in body.solid.face_iter() {
|
||||
let mut matching = Vec::new();
|
||||
for edge in generated.absolute_boundaries().iter().flatten() {
|
||||
visits += 1;
|
||||
if visits > 200_000 {
|
||||
return Ok(None);
|
||||
if let Some(e) = by_edge.get(&edge.id()) {
|
||||
matching.push(*e);
|
||||
}
|
||||
side |= inputs.contains(&edge.id());
|
||||
}
|
||||
if generated.id() != f.id() && !side {
|
||||
if candidate.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
candidate = Some(generated.id());
|
||||
}
|
||||
faces.push((generated.id(), matching));
|
||||
}
|
||||
Ok(candidate)
|
||||
})?;
|
||||
let body = Body::new(solid)?;
|
||||
// tsweep retains each actual input boundary edge on its generated side.
|
||||
// Match topology identity, never face order, surface samples or centroids.
|
||||
let provenance = guard("extrude source identities", || {
|
||||
let source_edges: std::collections::HashMap<_, _> = sources.iter().map(|(edge, segment)| (edge.id(), *segment)).collect();
|
||||
let mut result = Vec::new();
|
||||
for generated in body.solid.face_iter() {
|
||||
let matching: Vec<_> = generated.absolute_boundaries().iter().flatten().filter_map(|edge| source_edges.get(&edge.id())).collect();
|
||||
let source =
|
||||
if generated.id() == f.id() {
|
||||
upper.and_then(|_| {
|
||||
(r.outer.segs.len() + r.holes.iter().map(|hole| hole.segs.len()).sum::<usize>() <= 4096).then(|| {
|
||||
crate::FaceSource::ExtrudeCap {
|
||||
high: false,
|
||||
region: r.clone(),
|
||||
plane: Plane { origin: base.origin, x: plane.x, y: -plane.y },
|
||||
}
|
||||
})
|
||||
let mut ends = faces.iter().filter(|(id, matching)| *id != f.id() && matching.is_empty()).map(|(id, _)| *id);
|
||||
let end = match (ends.next(), ends.next()) {
|
||||
(Some(id), None) => Some(id),
|
||||
_ => None,
|
||||
};
|
||||
let upper = end.filter(|_| faces.len() <= 50_000 && visits <= 200_000);
|
||||
let small = r.outer.segs.len() + r.holes.iter().map(|hole| hole.segs.len()).sum::<usize>() <= 4096;
|
||||
let mut sources = Vec::with_capacity(faces.len());
|
||||
let mut history = Some(Vec::with_capacity(faces.len()));
|
||||
for (id, matching) in &faces {
|
||||
let (source, prov) = if *id == f.id() {
|
||||
let source = upper.and_then(|_| {
|
||||
small.then(|| crate::FaceSource::ExtrudeCap {
|
||||
high: false,
|
||||
region: r.clone(),
|
||||
plane: Plane { origin: base.origin, x: plane.x, y: -plane.y },
|
||||
})
|
||||
} else if Some(generated.id()) == upper {
|
||||
(r.outer.segs.len() + r.holes.iter().map(|hole| hole.segs.len()).sum::<usize>() <= 4096)
|
||||
.then(|| crate::FaceSource::ExtrudeCap { high: true, region: r.clone(), plane: plane.offset(hi) })
|
||||
} else if let [segment] = matching.as_slice() {
|
||||
let basis = match **segment {
|
||||
Seg2::Line { a, b } => {
|
||||
let d = b - a;
|
||||
d.normalized().and_then(|direction| {
|
||||
let foot = a - direction * a.dot(direction);
|
||||
Plane::new(plane.to_world(foot), plane.dir_to_world(direction), n)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Some(crate::FaceSource::ExtrudeSide { segment: **segment, plane: basis, coalesced: false })
|
||||
} else {
|
||||
None
|
||||
});
|
||||
(source, Some(record::cap(false)))
|
||||
} else if Some(*id) == upper {
|
||||
(small.then(|| crate::FaceSource::ExtrudeCap { high: true, region: r.clone(), plane: plane.offset(hi) }), Some(record::cap(true)))
|
||||
} else if let [side] = matching.as_slice() {
|
||||
let basis = match side.segment {
|
||||
Seg2::Line { a, b } => {
|
||||
let d = b - a;
|
||||
d.normalized().and_then(|direction| {
|
||||
let foot = a - direction * a.dot(direction);
|
||||
Plane::new(plane.to_world(foot), plane.dir_to_world(direction), n)
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
result.push(source);
|
||||
(
|
||||
Some(crate::FaceSource::ExtrudeSide { segment: side.segment, plane: basis, coalesced: false }),
|
||||
Some(record::profile(&side.numbers)),
|
||||
)
|
||||
} else if Some(*id) == end {
|
||||
(None, Some(record::cap(true)))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
sources.push(source);
|
||||
history = history.zip(prov).map(|(mut h, p)| {
|
||||
h.push(p);
|
||||
h
|
||||
});
|
||||
}
|
||||
Ok(result)
|
||||
Ok((sources, history))
|
||||
})?;
|
||||
let body = body.with_face_sources(Some(std::sync::Arc::new(provenance)));
|
||||
out.push(crate::record::attach_if(body, &[], history)?);
|
||||
let body = body.with_face_sources(Some(std::sync::Arc::new(sources)));
|
||||
out.push(record::attach(body, &[], history, Default::default())?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -265,20 +298,17 @@ pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir:
|
||||
let q = axis_origin + d * along;
|
||||
if p.dist(q) < 1e-7 { q } else { p }
|
||||
};
|
||||
let snap_loop = |l: &Loop2| Loop2 {
|
||||
segs: l
|
||||
.segs
|
||||
.iter()
|
||||
.map(|s| match *s {
|
||||
Seg2::Line { a, b } => Seg2::Line { a: snap(a), b: snap(b) },
|
||||
arc => arc,
|
||||
})
|
||||
.collect(),
|
||||
let snap_loop = |l: &NumberedLoop| {
|
||||
l.map_segs(|s| match *s {
|
||||
Seg2::Line { a, b } => Seg2::Line { a: snap(a), b: snap(b) },
|
||||
arc => arc,
|
||||
})
|
||||
};
|
||||
// Full circles start at an odd angle: their seam points then don't lie in the planes
|
||||
// through the profile's centre that other bodies tend to cut along. (Whether the loop
|
||||
// was re-split comes with it.)
|
||||
let reseam = |l: Loop2| -> (Loop2, bool) {
|
||||
// through the profile's centre that other bodies tend to cut along. Each new arc comes
|
||||
// from every given arc it overlaps.
|
||||
let reseam = |nl: NumberedLoop| -> NumberedLoop {
|
||||
let l = &nl.lp;
|
||||
let c = match l.segs.first() {
|
||||
Some(Seg2::Arc { center, radius, .. }) => Some((*center, *radius)),
|
||||
_ => None,
|
||||
@@ -306,45 +336,27 @@ pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir:
|
||||
{
|
||||
let ccw = l.signed_area() > 0.0;
|
||||
let n = Loop2::circle_from(center, radius, 0.4142);
|
||||
(if ccw { n } else { n.reversed() }, true)
|
||||
nl.resplit_circle(if ccw { n } else { n.reversed() })
|
||||
}
|
||||
_ => (l, false),
|
||||
_ => nl,
|
||||
}
|
||||
};
|
||||
// Each loop's profile segments as oriented; a circle the revolve re-split comes from
|
||||
// every input arc it overlaps.
|
||||
let mut origins = crate::record::loop_origins(std::slice::from_ref(r));
|
||||
let (outer, resplit) = reseam(snap_loop(&r.outer));
|
||||
let mut resplit = vec![resplit];
|
||||
let mut holes = Vec::with_capacity(r.holes.len());
|
||||
for h in &r.holes {
|
||||
let (l, again) = reseam(snap_loop(h));
|
||||
holes.push(l);
|
||||
resplit.push(again);
|
||||
}
|
||||
let given: Vec<&Loop2> = std::iter::once(&r.outer).chain(&r.holes).collect();
|
||||
let r = &Region2 { outer, holes };
|
||||
for (k, b) in std::iter::once(&r.outer).chain(&r.holes).enumerate() {
|
||||
if resplit.get(k) == Some(&true)
|
||||
&& let Some(g) = given.get(k)
|
||||
{
|
||||
let base: usize = origins.iter().take(k).map(Vec::len).sum();
|
||||
let oriented = if k == 0 { b.ccw() } else { b.ccw().reversed() };
|
||||
if let Some(slot) = origins.get_mut(k) {
|
||||
*slot = crate::record::arc_overlaps(&oriented.segs, &g.segs)
|
||||
.into_iter()
|
||||
.map(|js| js.into_iter().map(|j| base + j).collect())
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
let given = numbered_region(r)?.map_loops(|l| reseam(snap_loop(l)));
|
||||
let r = &given.region();
|
||||
// A half disc on the axis revolved fully is a sphere: build it without poles. Every
|
||||
// patch is swept from the half disc's arcs.
|
||||
if angle.abs() >= std::f64::consts::TAU - 1e-9
|
||||
&& r.holes.is_empty()
|
||||
&& let Some((c, rad)) = half_disc(&r.outer, axis_origin, d)
|
||||
{
|
||||
let arcs: Vec<usize> = r.outer.segs.iter().enumerate().filter(|(_, s)| matches!(s, Seg2::Arc { .. })).map(|(i, _)| i).collect();
|
||||
let arcs: Vec<usize> = r
|
||||
.outer
|
||||
.segs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, s)| matches!(s, Seg2::Arc { .. }))
|
||||
.flat_map(|(i, _)| given.outer.numbers(i).to_vec())
|
||||
.collect();
|
||||
out.push(sphere_solid(plane.to_world(c), rad, [Vec3::X, Vec3::Y, Vec3::Z], &arcs)?);
|
||||
continue;
|
||||
}
|
||||
@@ -352,13 +364,13 @@ pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir:
|
||||
// as a cone-like shell (no zero-area faces, which booleans can't handle).
|
||||
if angle.abs() >= std::f64::consts::TAU - 1e-9
|
||||
&& r.holes.is_empty()
|
||||
&& let Some(b) = revolve_touching_axis(plane, &r.outer, axis_origin, d)?
|
||||
&& let Some(b) = revolve_touching_axis(plane, &given.outer, axis_origin, d)?
|
||||
{
|
||||
out.push(b);
|
||||
continue;
|
||||
}
|
||||
let (wires, _, seeds) = region_wires(plane, r, &origins)?;
|
||||
let f = attach_profile(&wires)?;
|
||||
let profile = region_wires(plane, &given.oriented())?;
|
||||
let f = attach_profile(&wires_of(&profile))?;
|
||||
// Partial revolves sweep toward the side the sketch normal points to (as features
|
||||
// extrude toward it).
|
||||
let c = plane.to_world(r.centroid());
|
||||
@@ -367,15 +379,15 @@ pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir:
|
||||
let angle = if toward < 0.0 { -angle } else { angle };
|
||||
// A profile with an edge on the axis: no zero-area face where that edge turns on itself.
|
||||
if r.holes.is_empty()
|
||||
&& let Some(b) = revolve_partial_on_axis(plane, &r.outer, axis_origin, d, angle)?
|
||||
&& let Some(b) = revolve_partial_on_axis(plane, &given.outer, axis_origin, d, angle)?
|
||||
{
|
||||
out.push(b);
|
||||
continue;
|
||||
}
|
||||
let solid: Solid = guard("revolve", || Ok(builder::rsweep(&f, p3(o), v3(axis), mt::Rad(angle))))?;
|
||||
let closed = angle.abs() >= std::f64::consts::TAU;
|
||||
let history = guard("revolve history", || Ok(crate::record::swept_face(&solid, &f, &seeds, closed)))?;
|
||||
out.push(crate::record::attach_if(Body::new(solid)?, &[], history)?);
|
||||
let history = guard("revolve history", || Ok(record::swept_face(&solid, &f, &seeds(&profile), closed)))?;
|
||||
out.push(record::attach(Body::new(solid)?, &[], history, Default::default())?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -522,19 +534,22 @@ pub fn extrude_tapered(plane: &Plane, region: &Region2, length: f64, dir_sign: f
|
||||
region_ok(region)?;
|
||||
let d = length * taper.tan();
|
||||
let top_plane = plane.offset(length * dir_sign.signum());
|
||||
let loops0: Vec<Loop2> = std::iter::once(region.outer.ccw()).chain(region.holes.iter().map(|h| h.ccw().reversed())).collect();
|
||||
let loops1: Vec<Loop2> = loops0.iter().map(|l| offset_loop(l, d)).collect::<Result<_>>()?;
|
||||
let origins = crate::record::loop_origins(std::slice::from_ref(region));
|
||||
let loops0 = oriented(region)?;
|
||||
let loops1: Vec<Loop2> = loops0.loops().map(|l| offset_loop(&l.lp, d)).collect::<Result<_>>()?;
|
||||
guard("tapered extrude", || {
|
||||
let (w0, _, seeds) = region_wires(plane, region, &origins)?;
|
||||
let p0 = region_wires(plane, &loops0)?;
|
||||
let w0 = wires_of(&p0);
|
||||
let w1: Vec<mt::Wire> = loops1.iter().map(|l| wire(&top_plane, l)).collect::<Result<_>>()?;
|
||||
let mut faces: Vec<mt::Face> = Vec::new();
|
||||
for (a, b) in w0.iter().zip(&w1) {
|
||||
let sides = builder::try_wire_homotopy(a, b).map_err(|e| KernelError::Failed(format!("taper sides: {e}")))?;
|
||||
faces.extend(sides.face_iter().cloned());
|
||||
let mut sides: Vec<Side> = Vec::new();
|
||||
for (a, b) in p0.iter().zip(&w1) {
|
||||
let shell = builder::try_wire_homotopy(&a.wire, b).map_err(|e| KernelError::Failed(format!("taper sides: {e}")))?;
|
||||
// The offset loop keeps the loop's segments, edge for edge.
|
||||
let numbers = a.numbers();
|
||||
sides.extend(record::ruled(&shell, &a.wire, b, &numbers, &numbers));
|
||||
}
|
||||
let (faces, prov): (Vec<mt::Face>, Vec<Option<FaceProv>>) = sides.into_iter().unzip();
|
||||
// The sides, then the bottom and top as pushed below.
|
||||
let history = crate::record::capped_sides(&faces, &seeds, crate::record::Column::Steps(1));
|
||||
let history: Option<Vec<FaceProv>> = prov.into_iter().chain([Some(record::cap(false)), Some(record::cap(true))]).collect();
|
||||
let bottom = builder::try_attach_plane(&w0.iter().map(|w| w.inverse()).collect::<Vec<_>>())
|
||||
.map_err(|e| KernelError::Failed(format!("taper bottom: {e}")))?;
|
||||
let top = builder::try_attach_plane(&w1).map_err(|e| KernelError::Failed(format!("taper top: {e}")))?;
|
||||
@@ -547,7 +562,7 @@ pub fn extrude_tapered(plane: &Plane, region: &Region2, length: f64, dir_sign: f
|
||||
all.push(t);
|
||||
let shell: mt::Shell = all.into();
|
||||
match Solid::try_new(vec![shell]) {
|
||||
Ok(s) => return crate::record::attach_if(Body::new(s)?, &[], history),
|
||||
Ok(s) => return record::attach(Body::new(s)?, &[], history, Default::default()),
|
||||
Err(e) => last = e.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -576,8 +591,9 @@ fn half_disc(lp: &Loop2, axis_origin: Vec2, d: Vec2) -> Option<(Vec2, f64)> {
|
||||
/// A partial revolve of a loop with one edge on the axis: the other edges sweep their faces,
|
||||
/// the two caps (the profile at the start and at the end) share the axis edge. `None` when the
|
||||
/// loop isn't like that.
|
||||
fn revolve_partial_on_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2, angle: f64) -> Result<Option<Body>> {
|
||||
fn revolve_partial_on_axis(plane: &Plane, numbered: &NumberedLoop, axis_origin: Vec2, d: Vec2, angle: f64) -> Result<Option<Body>> {
|
||||
use mt::{BoundedCurve, Invertible, ParametricCurve, ParametricSurface3D, SearchNearestParameter, Transformed};
|
||||
let lp = &numbered.lp;
|
||||
let on_axis = |p: Vec2| d.cross(p - axis_origin).abs() < 1e-9;
|
||||
let n = lp.segs.len();
|
||||
let axis_segs: Vec<usize> =
|
||||
@@ -632,8 +648,9 @@ fn revolve_partial_on_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2
|
||||
*slot = Some(builder::circle_arc(a, b, p3(turn(&half, *p))));
|
||||
}
|
||||
}
|
||||
// Side faces: each edge's swept surface, oriented to agree with its boundary.
|
||||
let mut faces: Vec<mt::Face> = Vec::new();
|
||||
// Side faces: each edge's swept surface, oriented to agree with its boundary, from the
|
||||
// profile segment the edge was built from.
|
||||
let mut faces: Vec<(mt::Face, FaceProv)> = Vec::new();
|
||||
for (i, sg) in chain.iter().enumerate() {
|
||||
let (Some(ei), Some(eri)) = (e.get(i), er.get(i)) else { return Err(fail("edges")) };
|
||||
let mut ws: Vec<mt::Edge> = vec![ei.clone()];
|
||||
@@ -673,7 +690,7 @@ fn revolve_partial_on_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2
|
||||
}
|
||||
mt::Face::try_new(vec![wire], surface).map_err(|er| fail(&er.to_string()))?
|
||||
};
|
||||
faces.push(f);
|
||||
faces.push((f, record::profile(numbered.numbers((k + 1 + i) % n))));
|
||||
}
|
||||
// Caps: the profile at the start and at the end, sharing the axis edge.
|
||||
let (Some(a0), Some(an)) = (pv.first(), pv.get(last)) else { return Err(fail("ends")) };
|
||||
@@ -682,6 +699,8 @@ fn revolve_partial_on_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2
|
||||
let end: mt::Wire = er.iter().cloned().chain(std::iter::once(axis_edge.clone())).collect::<Vec<_>>().into();
|
||||
let cs = builder::try_attach_plane(&[start]).map_err(|er| fail(&er.to_string()))?;
|
||||
let ce = builder::try_attach_plane(&[end]).map_err(|er| fail(&er.to_string()))?;
|
||||
let (faces, mut history): (Vec<mt::Face>, Vec<FaceProv>) = faces.into_iter().unzip();
|
||||
history.extend([record::cap(false), record::cap(true)]);
|
||||
let mut last_err = String::new();
|
||||
for flip in 0..4 {
|
||||
let mut all = faces.clone();
|
||||
@@ -690,9 +709,7 @@ fn revolve_partial_on_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2
|
||||
match Solid::try_new(vec![all.into()]) {
|
||||
Ok(solid) => {
|
||||
let solid = if shift.len() > 0.0 { builder::translated(&solid, v3(shift)) } else { solid };
|
||||
// The faces as built: one per chain segment, then the start and end caps.
|
||||
let history = crate::record::walls_then_caps((0..chain.len()).map(|i| vec![(k + 1 + i) % n]));
|
||||
return crate::record::attach(Body::new(solid)?, &[], history).map(Some);
|
||||
return record::attach(Body::new(solid)?, &[], Some(history), Default::default()).map(Some);
|
||||
}
|
||||
Err(er) => last_err = er.to_string(),
|
||||
}
|
||||
@@ -701,15 +718,17 @@ fn revolve_partial_on_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2
|
||||
})
|
||||
}
|
||||
|
||||
fn revolve_touching_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2) -> Result<Option<Body>> {
|
||||
fn revolve_touching_axis(plane: &Plane, numbered: &NumberedLoop, axis_origin: Vec2, d: Vec2) -> Result<Option<Body>> {
|
||||
let lp = &numbered.lp;
|
||||
let on_axis = |p: Vec2| d.cross(p - axis_origin).abs() < 1e-9;
|
||||
let n = lp.segs.len();
|
||||
let axis_segs: Vec<usize> =
|
||||
(0..n).filter(|i| lp.segs.get(*i).is_some_and(|s| matches!(s, Seg2::Line { a, b } if on_axis(*a) && on_axis(*b)))).collect();
|
||||
let [k] = axis_segs[..] else { return Ok(None) };
|
||||
// The open chain after the axis segment, back around to it, with each piece's segment.
|
||||
// The open chain after the axis segment, back around to it, with the profile segments
|
||||
// under each piece.
|
||||
let mut chain: Vec<Seg2> = (1..n).filter_map(|j| lp.segs.get((k + j) % n).copied()).collect();
|
||||
let mut chain_src: Vec<usize> = (1..n).map(|j| (k + j) % n).collect();
|
||||
let mut chain_src: Vec<&[usize]> = (1..n).map(|j| numbered.numbers((k + j) % n)).collect();
|
||||
// A single arc (a sphere) is split off-centre: a seam on the equator would lie in the very
|
||||
// planes other bodies are often cut with.
|
||||
if let [Seg2::Arc { center, radius, start, sweep }] = chain[..] {
|
||||
@@ -718,7 +737,7 @@ fn revolve_touching_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2)
|
||||
Seg2::Arc { center, radius, start, sweep: sweep * f },
|
||||
Seg2::Arc { center, radius, start: start + sweep * f, sweep: sweep * (1.0 - f) },
|
||||
];
|
||||
chain_src = vec![(k + 1) % n; 2];
|
||||
chain_src = vec![numbered.numbers((k + 1) % n); 2];
|
||||
}
|
||||
if chain.is_empty() || chain.iter().skip(1).take(chain.len().saturating_sub(2)).any(|s| on_axis(s.start()) && on_axis(s.end())) {
|
||||
return Ok(None);
|
||||
@@ -731,7 +750,7 @@ fn revolve_touching_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2)
|
||||
// Radial lines at the ends become planar caps (no degenerate disc centres).
|
||||
let radial = |sg: &Seg2| matches!(*sg, Seg2::Line { a, b } if (b - a).normalized().is_some_and(|u| u.dot(d).abs() < 1e-9));
|
||||
let mut mid: &[Seg2] = &chain;
|
||||
let mut mid_src: &[usize] = &chain_src;
|
||||
let mut mid_src: &[&[usize]] = &chain_src;
|
||||
// The segments of the radial lines that become caps.
|
||||
let (mut start_cap, mut end_cap) = (None, None);
|
||||
if mid.first().is_some_and(|f| radial(f) && on_axis(f.start())) {
|
||||
@@ -767,6 +786,8 @@ fn revolve_touching_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2)
|
||||
},
|
||||
});
|
||||
}
|
||||
// Each edge with the profile segments under it, before the wire may be turned round.
|
||||
let seeds: Vec<(mt::EdgeID, Vec<usize>)> = edges.iter().map(|e| e.id()).zip(mid_src.iter().map(|s| s.to_vec())).collect();
|
||||
let mut w: mt::Wire = edges.into();
|
||||
let axis = plane.dir_to_world(d);
|
||||
let mut shell = if apex_start || apex_end {
|
||||
@@ -779,25 +800,15 @@ fn revolve_touching_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2)
|
||||
builder::rsweep(&w, p3(plane.to_world(axis_origin)), v3(axis), mt::Rad(std::f64::consts::TAU))
|
||||
};
|
||||
let sides = shell.len();
|
||||
// History. truck's full turn makes its faces division by division (three), each
|
||||
// division one face per wire edge in wire order; the wire runs backwards when it was
|
||||
// turned for the cone. Each cap is the disc a trimmed radial line sweeps, found by the
|
||||
// profile vertex on its rim.
|
||||
let per = sides / 3;
|
||||
let side_src = |i: usize| -> Option<usize> {
|
||||
let j = i % per.max(1);
|
||||
let j = if mid.len() == 1 {
|
||||
0
|
||||
} else if apex_end && !apex_start {
|
||||
mid.len().checked_sub(j + 1)?
|
||||
} else {
|
||||
j
|
||||
};
|
||||
mid_src.get(j).copied()
|
||||
};
|
||||
let mut history: Option<Vec<crate::history::FaceProv>> = (sides % 3 == 0 && (per == mid.len() || (mid.len() == 1 && per == 2)))
|
||||
.then(|| (0..sides).map(|i| side_src(i).map(|seg| crate::record::profile(&[seg]))).collect::<Option<Vec<_>>>())
|
||||
.flatten();
|
||||
// History. truck sweeps each wire edge round into a column of faces (triangles at an
|
||||
// apex, which stays put); each cap is the disc a trimmed radial line sweeps, found by
|
||||
// the profile vertex on its rim.
|
||||
let fixed: Vec<mt::VertexID> =
|
||||
[(apex_start, verts.first()), (apex_end, verts.last())].into_iter().filter_map(|(apex, v)| v.filter(|_| apex).map(|v| v.id())).collect();
|
||||
let mut history: Option<Vec<FaceProv>> = (seeds.len() == mid.len())
|
||||
.then(|| record::columns(shell.face_iter(), &seeds, None, record::Column::ToEnd, &fixed))
|
||||
.flatten()
|
||||
.and_then(|sides| sides.into_iter().collect());
|
||||
let (first_vertex, last_vertex) = (verts.first().map(|v| v.id()), verts.last().map(|v| v.id()));
|
||||
for b in shell.extract_boundaries() {
|
||||
let at = |v: Option<mt::VertexID>| v.is_some_and(|v| b.vertex_iter().any(|x| x.id() == v));
|
||||
@@ -808,8 +819,8 @@ fn revolve_touching_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
history = history.zip(src).map(|(mut h, seg)| {
|
||||
h.push(crate::record::profile(&[seg]));
|
||||
history = history.zip(src).map(|(mut h, numbers)| {
|
||||
h.push(record::profile(numbers));
|
||||
h
|
||||
});
|
||||
let cap = builder::try_attach_plane(&[b]).map_err(|e| KernelError::Failed(format!("revolve cap: {e}")))?;
|
||||
@@ -825,12 +836,16 @@ fn revolve_touching_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2)
|
||||
});
|
||||
let solid = solid.map_err(|e| KernelError::Failed(format!("revolve: {e}")))?;
|
||||
let solid = if shift.len() > 0.0 { builder::translated(&solid, v3(shift)) } else { solid };
|
||||
crate::record::attach_if(Body::new(solid)?, &[], history).map(Some)
|
||||
record::attach(Body::new(solid)?, &[], history, Default::default()).map(Some)
|
||||
})
|
||||
}
|
||||
|
||||
/// Close a ruled shell (side faces between matching wires) with planar caps into a body.
|
||||
pub(crate) fn cap_and_close(mut faces: Vec<mt::Face>, w0: &[mt::Wire], w1: &[mt::Wire]) -> Result<Body> {
|
||||
/// Close a ruled shell (side faces between matching wires, with their provenance as built)
|
||||
/// with planar caps into a body, whose history is the sides' and the caps': the profile at
|
||||
/// `w0` is the start cap and at `w1` the end cap (the other way round when `from_end`).
|
||||
pub(crate) fn cap_and_close(sides: Vec<Side>, w0: &[mt::Wire], w1: &[mt::Wire], from_end: bool) -> Result<Body> {
|
||||
let (mut faces, prov): (Vec<mt::Face>, Vec<Option<FaceProv>>) = sides.into_iter().unzip();
|
||||
let history: Option<Vec<FaceProv>> = prov.into_iter().chain([Some(record::cap(from_end)), Some(record::cap(!from_end))]).collect();
|
||||
let bottom =
|
||||
builder::try_attach_plane(&w0.iter().map(|w| w.inverse()).collect::<Vec<_>>()).map_err(|e| KernelError::Failed(format!("cap: {e}")))?;
|
||||
let top = builder::try_attach_plane(w1).map_err(|e| KernelError::Failed(format!("cap: {e}")))?;
|
||||
@@ -842,7 +857,7 @@ pub(crate) fn cap_and_close(mut faces: Vec<mt::Face>, w0: &[mt::Wire], w1: &[mt:
|
||||
all.push(t);
|
||||
let shell: mt::Shell = all.into();
|
||||
match Solid::try_new(vec![shell]) {
|
||||
Ok(s) => return Body::new(s),
|
||||
Ok(s) => return record::attach(Body::new(s)?, &[], history, Default::default()),
|
||||
Err(e) => last = e.to_string(),
|
||||
}
|
||||
faces = keep;
|
||||
@@ -850,48 +865,6 @@ pub(crate) fn cap_and_close(mut faces: Vec<mt::Face>, w0: &[mt::Wire], w1: &[mt:
|
||||
Err(KernelError::Failed(format!("closing the shell: {last}")))
|
||||
}
|
||||
|
||||
/// Re-split a loop into exactly `n` segments: a full circle gets breaks at `angles` (or evenly),
|
||||
/// other loops split their longest segments in half. With each new segment, the segments of
|
||||
/// `lp` it runs over.
|
||||
fn resample(lp: &Loop2, n: usize, angles: &[f64]) -> (Loop2, Vec<Vec<usize>>) {
|
||||
let circle = lp.segs.iter().all(|s| matches!(s, Seg2::Arc { .. }))
|
||||
&& lp.segs.windows(2).all(|w| match (w[0], w[1]) {
|
||||
(Seg2::Arc { center: c1, radius: r1, .. }, Seg2::Arc { center: c2, radius: r2, .. }) => c1.dist(c2) < 1e-9 && (r1 - r2).abs() < 1e-9,
|
||||
_ => false,
|
||||
});
|
||||
if circle && let Some(Seg2::Arc { center, radius, .. }) = lp.segs.first().copied() {
|
||||
let mut a: Vec<f64> = if angles.len() == n { angles.to_vec() } else { (0..n).map(|i| std::f64::consts::TAU * i as f64 / n as f64).collect() };
|
||||
// Keep the given order but make them increase.
|
||||
for i in 1..a.len() {
|
||||
while a[i] <= a[i - 1] {
|
||||
a[i] += std::f64::consts::TAU;
|
||||
}
|
||||
}
|
||||
let segs: Vec<Seg2> = (0..n)
|
||||
.map(|i| {
|
||||
let s = a[i];
|
||||
let e = if i + 1 < n { a[i + 1] } else { a[0] + std::f64::consts::TAU };
|
||||
Seg2::Arc { center, radius, start: s, sweep: e - s }
|
||||
})
|
||||
.collect();
|
||||
let src = crate::record::arc_overlaps(&segs, &lp.segs);
|
||||
return (Loop2 { segs }, src);
|
||||
}
|
||||
let mut segs = lp.segs.clone();
|
||||
let mut src: Vec<Vec<usize>> = (0..segs.len()).map(|i| vec![i]).collect();
|
||||
while segs.len() < n {
|
||||
let Some((k, _)) = segs.iter().enumerate().max_by(|a, b| a.1.length().total_cmp(&b.1.length())) else { break };
|
||||
let s = segs.remove(k);
|
||||
let (p, q) = s.split_half();
|
||||
segs.insert(k, q);
|
||||
segs.insert(k, p);
|
||||
if let Some(from) = src.get(k).cloned() {
|
||||
src.insert(k, from);
|
||||
}
|
||||
}
|
||||
(Loop2 { segs }, src)
|
||||
}
|
||||
|
||||
/// Ruled loft through two or more planar sections (outer loops only). The sections and the
|
||||
/// same sections in reverse order give the same body.
|
||||
pub fn loft(sections: &[(Plane, Loop2)]) -> Result<Body> {
|
||||
@@ -912,90 +885,66 @@ pub fn loft(sections: &[(Plane, Loop2)]) -> Result<Body> {
|
||||
let step = centre(last) - centre(first);
|
||||
let big = [step.x, step.y, step.z].into_iter().max_by(|a, b| a.abs().total_cmp(&b.abs())).unwrap_or(0.0);
|
||||
let reversed = big < 0.0;
|
||||
// (Each section with the profile number of its first segment: the sections' segments are
|
||||
// numbered in the order given.)
|
||||
let mut ordered: Vec<(usize, &(Plane, Loop2))> =
|
||||
sections.iter().scan(0, |acc, s| Some((std::mem::replace(acc, *acc + s.1.segs.len()), s))).collect();
|
||||
// The sections' segments are numbered in the order given.
|
||||
let mut ordered: Vec<(Plane, NumberedLoop)> =
|
||||
sections.iter().map(|(p, _)| *p).zip(numbered::number_loops(sections.iter().map(|(_, l)| l))).collect();
|
||||
if reversed {
|
||||
ordered.reverse();
|
||||
}
|
||||
// Orient every section counter-clockwise about the first section's normal (`Body::new`
|
||||
// turns the solid outward), then match segment counts and starting points to the section
|
||||
// with the most segments.
|
||||
let n0 = ordered.first().map(|(_, (p, _))| p.normal()).unwrap_or(Vec3::Z);
|
||||
// (With each section, the profile segment number of each of its segments as oriented.)
|
||||
let oriented: Vec<(Plane, Loop2, Vec<usize>)> = ordered
|
||||
let n0 = ordered.first().map(|(p, _)| p.normal()).unwrap_or(Vec3::Z);
|
||||
let oriented: Vec<(Plane, NumberedLoop)> = ordered
|
||||
.iter()
|
||||
.map(|(base, (p, l))| {
|
||||
.map(|(p, l)| {
|
||||
let ccw = p.normal().dot(n0) >= 0.0;
|
||||
let numbers = crate::record::ccw_numbers(l, !ccw, *base);
|
||||
let l = if ccw { l.ccw() } else { l.ccw().reversed() };
|
||||
(*p, l, numbers)
|
||||
(*p, l.oriented(!ccw))
|
||||
})
|
||||
.collect();
|
||||
let lead = oriented.iter().max_by_key(|(_, l, _)| l.segs.len()).cloned().ok_or_else(|| KernelError::Invalid("no sections".into()))?;
|
||||
let lead_c = lead.0.to_world(lead.1.centroid());
|
||||
let lead_dirs: Vec<Vec3> = lead.1.segs.iter().map(|s| lead.0.to_world(s.start()) - lead_c).collect();
|
||||
let lead = oriented.iter().max_by_key(|(_, l)| l.lp.segs.len()).ok_or_else(|| KernelError::Invalid("no sections".into()))?;
|
||||
let lead_c = lead.0.to_world(lead.1.lp.centroid());
|
||||
let lead_dirs: Vec<Vec3> = lead.1.lp.segs.iter().map(|s| lead.0.to_world(s.start()) - lead_c).collect();
|
||||
let mut wires: Vec<mt::Wire> = Vec::new();
|
||||
// Per section, the profile segments under each of its wire's edges.
|
||||
let mut edge_sources: Vec<Vec<Vec<usize>>> = Vec::new();
|
||||
for (p, l, numbers) in &oriented {
|
||||
let c = l.centroid();
|
||||
for (p, l) in &oriented {
|
||||
let c = l.lp.centroid();
|
||||
// Break angles: the lead section's vertex directions seen in this plane.
|
||||
let angles: Vec<f64> = lead_dirs.iter().map(|d| Vec2::new(d.dot(p.x), d.dot(p.y)).angle()).collect();
|
||||
let (mut r, mut src) = resample(l, n, &angles);
|
||||
let mut r = l.resample(n, &angles);
|
||||
// Start at the vertex closest in direction to the lead's first vertex.
|
||||
if let Some(d0) = lead_dirs.first() {
|
||||
let target = Vec2::new(d0.dot(p.x), d0.dot(p.y)).angle();
|
||||
let best = (0..r.segs.len()).min_by(|a, b| {
|
||||
let ang = |k: usize| {
|
||||
let v = r.segs[k].start() - c;
|
||||
let d = (v.angle() - target).rem_euclid(std::f64::consts::TAU);
|
||||
d.min(std::f64::consts::TAU - d)
|
||||
};
|
||||
ang(*a).total_cmp(&ang(*b))
|
||||
});
|
||||
let ang = |s: &Seg2| {
|
||||
let d = ((s.start() - c).angle() - target).rem_euclid(std::f64::consts::TAU);
|
||||
d.min(std::f64::consts::TAU - d)
|
||||
};
|
||||
let best = r.lp.segs.iter().enumerate().min_by(|a, b| ang(a.1).total_cmp(&ang(b.1))).map(|(k, _)| k);
|
||||
if let Some(k) = best {
|
||||
r.segs.rotate_left(k);
|
||||
src.rotate_left(k);
|
||||
r.rotate_left(k);
|
||||
}
|
||||
}
|
||||
edge_sources.push(src.iter().map(|js| js.iter().filter_map(|j| numbers.get(*j).copied()).collect()).collect());
|
||||
wires.push(uniform_wire(p, &r)?);
|
||||
wires.push(uniform_wire(p, &r.lp)?);
|
||||
edge_sources.push(r.from);
|
||||
}
|
||||
// The caps: the wires run from the first section given, or (turned round) the last.
|
||||
let caps = [crate::record::cap(reversed), crate::record::cap(!reversed)];
|
||||
// The wires run from the first section given, or (turned round) the last.
|
||||
// Three sections or more: one smooth surface through them all for each segment.
|
||||
if wires.len() >= 3
|
||||
&& let Ok(b) = guard("loft", || smooth_loft(&wires))
|
||||
&& let Ok(b) = guard("loft", || smooth_loft(&wires, &edge_sources, reversed))
|
||||
{
|
||||
// Its faces as built: one per edge position k, through the k-th edge of every
|
||||
// section, then the caps.
|
||||
let faces = (0..n)
|
||||
.map(|k| crate::record::profile_union(edge_sources.iter().filter_map(|s| s.get(k)).flatten().copied()))
|
||||
.chain(caps.clone())
|
||||
.collect();
|
||||
return crate::record::attach(b, &[], faces);
|
||||
return Ok(b);
|
||||
}
|
||||
guard("loft", || {
|
||||
let mut faces: Vec<mt::Face> = Vec::new();
|
||||
for w in wires.windows(2) {
|
||||
let sides = builder::try_wire_homotopy(&w[0], &w[1]).map_err(|e| KernelError::Failed(format!("loft: {e}")))?;
|
||||
faces.extend(sides.face_iter().cloned());
|
||||
// Each ruled face lies between two sections: it comes from the section edges it spans.
|
||||
let mut sides: Vec<Side> = Vec::new();
|
||||
for (w, src) in wires.windows(2).zip(edge_sources.windows(2)) {
|
||||
let (Some(a), Some(b), Some(na), Some(nb)) = (w.first(), w.get(1), src.first(), src.get(1)) else { continue };
|
||||
let shell = builder::try_wire_homotopy(a, b).map_err(|e| KernelError::Failed(format!("loft: {e}")))?;
|
||||
sides.extend(record::ruled(&shell, a, b, na, nb));
|
||||
}
|
||||
// Each ruled face lies between two sections: it comes from the section edges it holds.
|
||||
let under: std::collections::HashMap<mt::EdgeID, &Vec<usize>> =
|
||||
wires.iter().zip(&edge_sources).flat_map(|(w, src)| w.edge_iter().map(|e| e.id()).zip(src)).collect();
|
||||
let history: Option<Vec<crate::history::FaceProv>> = faces
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let held: Vec<&Vec<usize>> = f.absolute_boundaries().iter().flatten().filter_map(|e| under.get(&e.id()).copied()).collect();
|
||||
(held.len() == 2).then(|| crate::record::profile_union(held.iter().flat_map(|s| s.iter().copied())))
|
||||
})
|
||||
.chain(caps.iter().cloned().map(Some))
|
||||
.collect();
|
||||
let (Some(first), Some(last)) = (wires.first(), wires.last()) else { return Err(KernelError::Failed("loft".into())) };
|
||||
crate::record::attach_if(cap_and_close(faces, std::slice::from_ref(first), std::slice::from_ref(last))?, &[], history)
|
||||
cap_and_close(sides, std::slice::from_ref(first), std::slice::from_ref(last), reversed)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1013,8 +962,10 @@ fn across(pts: &[Vec3]) -> Option<mt::BSplineCurve<mt::Point3>> {
|
||||
|
||||
/// A loft whose side faces are B-spline surfaces through every section (smooth across the
|
||||
/// middle sections, which leave no edges). Every section wire comes from `uniform_wire`, so the
|
||||
/// k-th edges of all sections share one knot vector and their control points pair up.
|
||||
fn smooth_loft(wires: &[mt::Wire]) -> Result<Body> {
|
||||
/// k-th edges of all sections share one knot vector and their control points pair up. Face k
|
||||
/// comes from the profile segments under the k-th edge of every section (`numbers`: by
|
||||
/// section, by edge); the caps as [`cap_and_close`] takes them (`from_end`).
|
||||
fn smooth_loft(wires: &[mt::Wire], numbers: &[Vec<Vec<usize>>], from_end: bool) -> Result<Body> {
|
||||
let fail = |m: &str| KernelError::Failed(format!("smooth loft: {m}"));
|
||||
let edges: Vec<Vec<mt::Edge>> = wires.iter().map(|w| w.edge_iter().cloned().collect()).collect();
|
||||
let n = edges.first().map(Vec::len).ok_or_else(|| fail("no sections"))?;
|
||||
@@ -1061,10 +1012,12 @@ fn smooth_loft(wires: &[mt::Wire]) -> Result<Body> {
|
||||
};
|
||||
// u along the section edge, v across the sections: counter-clockwise in (u, v).
|
||||
let w: mt::Wire = vec![e0.clone(), s1.clone(), e1.inverse(), s0.inverse()].into();
|
||||
faces.push(mt::Face::try_new(vec![w], mt::Surface::BSplineSurface(surface)).map_err(|e| fail(&e.to_string()))?);
|
||||
let face = mt::Face::try_new(vec![w], mt::Surface::BSplineSurface(surface)).map_err(|e| fail(&e.to_string()))?;
|
||||
let under: Vec<&usize> = numbers.iter().filter_map(|s| s.get(k)).flatten().collect();
|
||||
faces.push((face, (under.len() >= numbers.len()).then(|| record::profile(under))));
|
||||
}
|
||||
let (Some(w0), Some(w1)) = (wires.first(), wires.last()) else { return Err(fail("sections")) };
|
||||
cap_and_close(faces, std::slice::from_ref(w0), std::slice::from_ref(w1))
|
||||
cap_and_close(faces, std::slice::from_ref(w0), std::slice::from_ref(w1), from_end)
|
||||
}
|
||||
|
||||
/// A 3D path segment for sweeps.
|
||||
@@ -1146,7 +1099,8 @@ pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result<Body>
|
||||
return crate::sweep_path::sweep_path(plane, region, &pts);
|
||||
}
|
||||
// The outer loop and each hole, swept along the path; the ends are capped with holes.
|
||||
let (starts, _, seeds) = region_wires(plane, region, &crate::record::loop_origins(std::slice::from_ref(region)))?;
|
||||
let profile = region_wires(plane, &oriented(region)?)?;
|
||||
let starts = wires_of(&profile);
|
||||
guard("sweep", || {
|
||||
let mut shell = mt::Shell::new();
|
||||
let mut ends = Vec::new();
|
||||
@@ -1170,10 +1124,13 @@ pub fn sweep(plane: &Plane, region: &Region2, path: &[PathSeg]) -> Result<Body>
|
||||
}
|
||||
ends.push(cur);
|
||||
}
|
||||
let faces: Vec<mt::Face> = shell.face_iter().cloned().collect();
|
||||
// Each profile edge's faces run along the whole path, path segment after segment.
|
||||
let history = crate::record::capped_sides(&faces, &seeds, crate::record::Column::ToEnd);
|
||||
crate::record::attach_if(cap_and_close(faces, &starts, &ends)?, &[], history)
|
||||
// truck sweeps the faces: each profile edge's run along the whole path, path segment
|
||||
// after segment.
|
||||
let sides: Vec<Side> = match record::columns(shell.face_iter(), &seeds(&profile), None, record::Column::ToEnd, &[]) {
|
||||
Some(prov) => shell.face_iter().cloned().zip(prov).collect(),
|
||||
None => shell.face_iter().map(|f| (f.clone(), None)).collect(),
|
||||
};
|
||||
cap_and_close(sides, &starts, &ends, false)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1342,6 +1299,6 @@ fn sphere_solid(center: Vec3, radius: f64, rot: [Vec3; 3], arcs: &[usize]) -> Re
|
||||
}
|
||||
let shell: mt::Shell = out.into();
|
||||
let solid = Solid::try_new(vec![shell]).map_err(|e| KernelError::Failed(format!("sphere: {e}")))?;
|
||||
crate::record::attach(Body::new(solid)?, &[], vec![crate::record::profile(arcs); 6])
|
||||
record::attach(Body::new(solid)?, &[], Some(vec![record::profile(arcs); 6]), Default::default())
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user