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 <noreply@anthropic.com> 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
+93
-133
@@ -3,97 +3,39 @@
|
||||
//! topology identity inside the building call: which face a profile edge was swept into,
|
||||
//! which edges an operand still owns. Nothing is sampled.
|
||||
//!
|
||||
//! Profile numbering: the segments of the builder's regions in the caller's order (each region
|
||||
//! its outer loop, then its holes), as [`FaceOrigin::Profile`] counts them. Builders turn loops
|
||||
//! counter-clockwise (holes clockwise) before building; [`loop_origins`] undoes that.
|
||||
//! Faces our code builds are paired with their provenance as they are made (a [`Side`]); only
|
||||
//! where truck makes the faces itself (`tsweep`, `rsweep`, `cone`) are they found afterwards,
|
||||
//! by following each profile edge's column of swept faces ([`follow`]). Profile segments are
|
||||
//! numbered by [`crate::numbered`].
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use solvecraft_geom::{Loop2, Region2, Seg2};
|
||||
use truck_modeling as mt;
|
||||
|
||||
use crate::Result;
|
||||
use crate::body::{Body, edge_order};
|
||||
use crate::history::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, History};
|
||||
|
||||
/// The profile number of each segment of `lp` as `ccw()` orders it (as `ccw().reversed()` does
|
||||
/// for a hole), the loop's own first segment being number `base`.
|
||||
pub(crate) fn ccw_numbers(lp: &Loop2, hole: bool, base: usize) -> Vec<usize> {
|
||||
let n = lp.segs.len();
|
||||
let reversed = lp.is_ccw() == hole;
|
||||
(0..n).map(|j| base + if reversed { n - 1 - j } else { j }).collect()
|
||||
}
|
||||
/// A face as built, with its provenance (`None` where the construction could not be followed).
|
||||
pub(crate) type Side = (mt::Face, Option<FaceProv>);
|
||||
|
||||
/// For every loop of `regions` (each region's outer loop, then its holes), the profile
|
||||
/// segments under each of its segments in the order the builders orient the loop (one each,
|
||||
/// until a builder re-splits the loop).
|
||||
pub(crate) fn loop_origins(regions: &[Region2]) -> Vec<Vec<Vec<usize>>> {
|
||||
let mut base = 0;
|
||||
let mut out = Vec::new();
|
||||
for r in regions {
|
||||
for (k, lp) in std::iter::once(&r.outer).chain(&r.holes).enumerate() {
|
||||
out.push(ccw_numbers(lp, k > 0, base).into_iter().map(|n| vec![n]).collect());
|
||||
base += lp.segs.len();
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Which arcs of `originals` (arcs of one circle) each arc of `pieces` (arcs of the same
|
||||
/// circle) runs over: a circle re-split at other angles comes from every original arc it
|
||||
/// overlaps.
|
||||
pub(crate) fn arc_overlaps(pieces: &[Seg2], originals: &[Seg2]) -> Vec<Vec<usize>> {
|
||||
use std::f64::consts::TAU;
|
||||
// An arc as its counter-clockwise start angle and (positive) sweep.
|
||||
let span = |s: &Seg2| match *s {
|
||||
Seg2::Arc { start, sweep, .. } => Some(if sweep < 0.0 { (start + sweep, -sweep) } else { (start, sweep) }),
|
||||
_ => None,
|
||||
};
|
||||
let eps = 1e-9;
|
||||
pieces
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let Some((p0, pw)) = span(p) else { return Vec::new() };
|
||||
(0..originals.len())
|
||||
.filter(|&j| {
|
||||
originals.get(j).and_then(span).is_some_and(|(o0, ow)| {
|
||||
// The original's start, seen from the piece's start going round.
|
||||
let d = (o0 - p0).rem_euclid(TAU);
|
||||
d < pw - eps || d + ow > TAU + eps || ow >= TAU - eps
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A face made from profile segments `numbers`.
|
||||
pub(crate) fn profile(numbers: &[usize]) -> FaceProv {
|
||||
generated(numbers.iter().map(|&segment| FaceOrigin::Profile { segment }).collect())
|
||||
}
|
||||
|
||||
/// A face made from all of `numbers` (in any order, repeats allowed).
|
||||
pub(crate) fn profile_union(numbers: impl IntoIterator<Item = usize>) -> FaceProv {
|
||||
let numbers: std::collections::BTreeSet<usize> = numbers.into_iter().collect();
|
||||
/// A face made from all of profile segments `numbers` (in any order, repeats allowed).
|
||||
pub(crate) fn profile<'a>(numbers: impl IntoIterator<Item = &'a usize>) -> FaceProv {
|
||||
let numbers: BTreeSet<usize> = numbers.into_iter().copied().collect();
|
||||
generated(numbers.into_iter().map(|segment| FaceOrigin::Profile { segment }).collect())
|
||||
}
|
||||
|
||||
/// Walls from the given profile segments in order, then the start and end caps: the face
|
||||
/// order of every builder that closes its walls with [`crate::build::cap_and_close`].
|
||||
pub(crate) fn walls_then_caps(walls: impl IntoIterator<Item = Vec<usize>>) -> Vec<FaceProv> {
|
||||
walls.into_iter().map(|w| profile(&w)).chain([cap(false), cap(true)]).collect()
|
||||
}
|
||||
|
||||
/// The index of each of `body`'s edges by identity, in its edge order.
|
||||
pub(crate) fn edge_index(body: &Body) -> HashMap<mt::EdgeID, usize> {
|
||||
edge_order(body.solid.face_iter()).0.iter().enumerate().map(|(i, e)| (e.id(), i)).collect()
|
||||
}
|
||||
|
||||
/// A face of operand `operand` and how this one relates to it.
|
||||
pub(crate) fn from_face(operand: usize, face: usize, change: Change) -> FaceProv {
|
||||
FaceProv { origins: vec![FaceOrigin::Face { operand, face }], change }
|
||||
}
|
||||
|
||||
/// Every face of the one operand (with `count` faces) but `except`, kept as it is, in order:
|
||||
/// the faces a builder carries over unchanged when it rebuilds a body around one face.
|
||||
pub(crate) fn kept_except(count: usize, except: usize) -> impl Iterator<Item = FaceProv> {
|
||||
(0..count).filter(move |i| *i != except).map(|i| from_face(0, i, Change::Kept))
|
||||
}
|
||||
|
||||
/// A start (`end == false`) or end cap.
|
||||
pub(crate) fn cap(end: bool) -> FaceProv {
|
||||
generated(vec![FaceOrigin::Cap { end }])
|
||||
@@ -104,26 +46,62 @@ pub(crate) fn generated(origins: Vec<FaceOrigin>) -> FaceProv {
|
||||
FaceProv { origins, change: Change::Generated }
|
||||
}
|
||||
|
||||
/// Attach to `body`, made from `operands`, the history whose faces are `faces` (in its face
|
||||
/// order). Each edge's origin is the operand edge it is (the very edge, kept by the building
|
||||
/// code), if any.
|
||||
pub(crate) fn attach(body: Body, operands: &[&Body], faces: Vec<FaceProv>) -> Result<Body> {
|
||||
attach_edges(body, operands, faces, HashMap::new())
|
||||
/// The index of each of `body`'s edges by identity, in its edge order.
|
||||
fn edge_index(body: &Body) -> HashMap<mt::EdgeID, usize> {
|
||||
edge_order(body.solid.face_iter()).0.iter().enumerate().map(|(i, e)| (e.id(), i)).collect()
|
||||
}
|
||||
|
||||
/// [`attach`], with `copies`: result edges the building code made as exact copies of operand
|
||||
/// edges (the same curve, rebuilt), and which.
|
||||
pub(crate) fn attach_edges(body: Body, operands: &[&Body], faces: Vec<FaceProv>, copies: HashMap<mt::EdgeID, EdgeOrigin>) -> Result<Body> {
|
||||
/// An operation's operand, with its edges indexed by identity (in its edge order).
|
||||
pub(crate) struct Operand<'a> {
|
||||
pub body: &'a Body,
|
||||
pub edges: HashMap<mt::EdgeID, usize>,
|
||||
}
|
||||
|
||||
impl<'a> Operand<'a> {
|
||||
pub(crate) fn new(body: &'a Body) -> Operand<'a> {
|
||||
Operand { body, edges: edge_index(body) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach to `body`, made from `operands`, the history whose faces are `faces` (in its face
|
||||
/// order); a body whose faces could not be followed (`None`) has no history. Each edge's
|
||||
/// origin is the operand edge it is (the very edge, kept by the building code), or what
|
||||
/// `copies` says: result edges the building code made as exact copies of operand edges.
|
||||
pub(crate) fn attach(body: Body, operands: &[Operand], faces: Option<Vec<FaceProv>>, copies: HashMap<mt::EdgeID, EdgeOrigin>) -> Result<Body> {
|
||||
let Some(faces) = faces else { return Ok(body) };
|
||||
let mut owned = copies;
|
||||
for (operand, b) in operands.iter().enumerate() {
|
||||
for (id, edge) in edge_index(b) {
|
||||
owned.entry(id).or_insert(EdgeOrigin::Edge { operand, edge });
|
||||
for (operand, o) in operands.iter().enumerate() {
|
||||
for (id, edge) in &o.edges {
|
||||
owned.entry(*id).or_insert(EdgeOrigin::Edge { operand, edge: *edge });
|
||||
}
|
||||
}
|
||||
let (edges, bounds) = edge_order(body.solid.face_iter());
|
||||
let edges = edges.iter().zip(bounds).map(|(e, faces)| EdgeProv { faces, origins: owned.get(&e.id()).copied().into_iter().collect() }).collect();
|
||||
let history = History { operands: operands.len(), faces, edges };
|
||||
body.with_history(history, operands)
|
||||
let counts: Vec<(usize, usize)> = operands.iter().map(|o| (o.body.face_count(), o.edges.len())).collect();
|
||||
body.with_history_counts(history, &counts)
|
||||
}
|
||||
|
||||
/// Ruled faces (truck's `try_wire_homotopy` of wires `a` and `b`), each with the provenance of
|
||||
/// the edges it spans: every face holds one edge of each wire, and comes from the profile
|
||||
/// segments under both (`a_numbers`, `b_numbers`: by edge, in wire order).
|
||||
pub(crate) fn ruled(faces: &mt::Shell, a: &mt::Wire, b: &mt::Wire, a_numbers: &[Vec<usize>], b_numbers: &[Vec<usize>]) -> Vec<Side> {
|
||||
let under = |w: &mt::Wire, numbers: &[Vec<usize>]| -> HashMap<mt::EdgeID, Vec<usize>> {
|
||||
w.edge_iter().map(|e| e.id()).zip(numbers.iter().cloned()).collect()
|
||||
};
|
||||
let (ua, ub) = (under(a, a_numbers), under(b, b_numbers));
|
||||
faces
|
||||
.face_iter()
|
||||
.map(|f| {
|
||||
let held = |u: &HashMap<mt::EdgeID, Vec<usize>>| -> Option<Vec<usize>> {
|
||||
let mut found = f.absolute_boundaries().iter().flat_map(|w| w.edge_iter()).filter_map(|e| u.get(&e.id()));
|
||||
let one = found.next()?.clone();
|
||||
found.next().is_none().then_some(one)
|
||||
};
|
||||
let prov = held(&ua).zip(held(&ub)).map(|(x, y)| profile(x.iter().chain(&y)));
|
||||
(f.clone(), prov)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// How far a column of swept faces runs.
|
||||
@@ -136,17 +114,19 @@ pub(crate) enum Column {
|
||||
Steps(usize),
|
||||
}
|
||||
|
||||
/// Follow swept faces from the profile edges (`seeds`) they start at. A face swept from an
|
||||
/// edge is bounded by one loop of four edges with the edge and its moved copy opposite each
|
||||
/// other, and the copy is where the next step of the sweep starts (truck builds every swept,
|
||||
/// revolved and ruled side face that way). The face at `skip` is never entered. Returns, for each
|
||||
/// face, the seed whose column it lies in; `None` when the faces don't follow that pattern
|
||||
/// (a face reached twice, a column with a fork or of the wrong length).
|
||||
/// Follow the faces truck sweeps from the profile edges (`seeds`) they start at. A face swept
|
||||
/// from an edge is bounded by one loop: the edge, its moved copy, and the paths its ends sweep
|
||||
/// (none where an end stays put: a cone's apex, `fixed`). The copy is the loop's one other edge
|
||||
/// that shares no moving vertex with the edge, and is where the next step of the sweep starts.
|
||||
/// The face at `skip` is never entered. Returns, for each face, the seed whose column it lies
|
||||
/// in; `None` when the faces don't follow that pattern (a face reached twice, a column with a
|
||||
/// fork or of the wrong length).
|
||||
pub(crate) fn follow<'a>(
|
||||
faces: impl IntoIterator<Item = &'a mt::Face>,
|
||||
seeds: &[mt::EdgeID],
|
||||
skip: Option<usize>,
|
||||
run: Column,
|
||||
fixed: &[mt::VertexID],
|
||||
) -> Option<Vec<Option<usize>>> {
|
||||
let faces: Vec<&mt::Face> = faces.into_iter().collect();
|
||||
let mut by_edge: HashMap<mt::EdgeID, Vec<usize>> = HashMap::new();
|
||||
@@ -187,12 +167,11 @@ pub(crate) fn follow<'a>(
|
||||
}
|
||||
*slot = Some(seed);
|
||||
let [wire] = faces.get(next)?.absolute_boundaries().as_slice() else { return None };
|
||||
let ids: Vec<mt::EdgeID> = wire.edge_iter().map(|e| e.id()).collect();
|
||||
if ids.len() != 4 {
|
||||
return None;
|
||||
}
|
||||
let at = ids.iter().position(|id| *id == edge)?;
|
||||
edge = *ids.get((at + 2) % 4)?;
|
||||
let here = wire.edge_iter().find(|e| e.id() == edge)?;
|
||||
let moving: Vec<mt::VertexID> = [here.front().id(), here.back().id()].into_iter().filter(|v| !fixed.contains(v)).collect();
|
||||
let mut copies = wire.edge_iter().filter(|e| e.id() != edge && !moving.contains(&e.front().id()) && !moving.contains(&e.back().id()));
|
||||
let (Some(copy), None) = (copies.next(), copies.next()) else { return None };
|
||||
edge = copy.id();
|
||||
prev = Some(next);
|
||||
first.get_or_insert(next);
|
||||
count += 1;
|
||||
@@ -204,8 +183,21 @@ pub(crate) fn follow<'a>(
|
||||
Some(owner)
|
||||
}
|
||||
|
||||
/// The history of a solid swept from the planar face `profile` (truck's `tsweep` or `rsweep`
|
||||
/// of it), whose boundary edges `seeds` come from the profile segments given with them. Open
|
||||
/// Each face's provenance by the column it lies in ([`follow`]), from the profile segments
|
||||
/// under each seed edge.
|
||||
pub(crate) fn columns<'a>(
|
||||
faces: impl IntoIterator<Item = &'a mt::Face>,
|
||||
seeds: &[(mt::EdgeID, Vec<usize>)],
|
||||
skip: Option<usize>,
|
||||
run: Column,
|
||||
fixed: &[mt::VertexID],
|
||||
) -> Option<Vec<Option<FaceProv>>> {
|
||||
let ids: Vec<mt::EdgeID> = seeds.iter().map(|(id, _)| *id).collect();
|
||||
Some(follow(faces, &ids, skip, run, fixed)?.into_iter().map(|o| o.and_then(|s| seeds.get(s)).map(|(_, numbers)| profile(numbers))).collect())
|
||||
}
|
||||
|
||||
/// The history of a solid truck swept from the planar face `profile` (`rsweep` of it), whose
|
||||
/// boundary edges `seeds` come from the profile segments given with them. Open
|
||||
/// (`closed == false`): the profile face is the start cap, each edge's faces follow from it to
|
||||
/// the end cap (the one face left), the same number of faces for every edge. Closed (a full
|
||||
/// turn): no caps, each edge's faces go round.
|
||||
@@ -225,7 +217,7 @@ pub(crate) fn swept_face(solid: &crate::body::Solid, profile: &mt::Face, seeds:
|
||||
_ => return None,
|
||||
};
|
||||
// Every face but the caps lies in a column, so the one face left open is the end cap.
|
||||
columns(faces, seeds, start, run)?
|
||||
columns(faces, seeds, start, run, &[])?
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| match p {
|
||||
@@ -236,38 +228,6 @@ pub(crate) fn swept_face(solid: &crate::body::Solid, profile: &mt::Face, seeds:
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Each face's provenance by the column it lies in ([`follow`]).
|
||||
fn columns<'a>(
|
||||
faces: impl IntoIterator<Item = &'a mt::Face>,
|
||||
seeds: &[(mt::EdgeID, Vec<usize>)],
|
||||
skip: Option<usize>,
|
||||
run: Column,
|
||||
) -> Option<Vec<Option<FaceProv>>> {
|
||||
let ids: Vec<mt::EdgeID> = seeds.iter().map(|(id, _)| *id).collect();
|
||||
Some(follow(faces, &ids, skip, run)?.into_iter().map(|o| o.and_then(|s| seeds.get(s)).map(|(_, numbers)| profile(numbers))).collect())
|
||||
}
|
||||
|
||||
/// Side faces swept from `seeds` (edges with the profile segments under them; see [`follow`]),
|
||||
/// then the start and end caps, the order [`crate::build::cap_and_close`] leaves them in.
|
||||
/// `None` unless every side lies in some edge's column.
|
||||
pub(crate) fn capped_sides<'a>(
|
||||
faces: impl IntoIterator<Item = &'a mt::Face>,
|
||||
seeds: &[(mt::EdgeID, Vec<usize>)],
|
||||
run: Column,
|
||||
) -> Option<Vec<FaceProv>> {
|
||||
let mut out: Vec<FaceProv> = columns(faces, seeds, None, run)?.into_iter().collect::<Option<_>>()?;
|
||||
out.extend([cap(false), cap(true)]);
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Attach `faces` when recorded; a body whose faces could not be followed has no history.
|
||||
pub(crate) fn attach_if(body: Body, operands: &[&Body], faces: Option<Vec<FaceProv>>) -> Result<Body> {
|
||||
match faces {
|
||||
Some(faces) => attach(body, operands, faces),
|
||||
None => Ok(body),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "record_tests.rs"]
|
||||
mod tests;
|
||||
Reference in new issue
Block a user