SMLib export: a face's outer area reads only its own loop's edges

outer_area, which decides whether SMLib's normal is the record's for each
face, copied every edge of the body into a record for each face before
computing the pcurves of that face's outer loop: O(faces x edges) curve
clones, on every operand and every boolean result. pcurves now takes an
edge lookup instead of a slice of edge records, so outer_area reads the
loop's edges where they are. The pcurve routine gets the same curve,
interval, tolerance and trims as before, so every area, face turn, record
and numbering is bit for bit what it was.

On the level-4 Menger sponge with 3 tools, body_from_smlib_ordered goes
from 92.6 s to 1.3 s of self time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-29 22:42:10 -04:00
committed by Irakli Kourtis
parent 177eb207fe
commit 8d72cfa598
2 changed files with 115 additions and 14 deletions
+26 -14
View File
@@ -1259,6 +1259,10 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerances: &Tolerance
uses,
});
}
let record_geometry = |e: u32| {
let edge = edge_records.get(e as usize)?;
Some(EdgeGeometry { curve: edge.nurbs.as_ref().unwrap_or(&edge.curve), interval: edge.interval, tolerance: edge.tolerance })
};
let mut face_records = Vec::with_capacity(face_order.len());
for (fi, &f) in face_order.iter().enumerate() {
let face = faces.get(f as usize).ok_or_else(|| invalid("face"))?;
@@ -1271,7 +1275,7 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerances: &Tolerance
})
.collect::<Res<_>>()?;
let mut curves =
pcurves(face.nurbs.as_ref().unwrap_or(&face.surface), face.domain, &renumbered, &face.given, &edge_records, tolerance)?.into_iter();
pcurves(face.nurbs.as_ref().unwrap_or(&face.surface), face.domain, &renumbered, &face.given, record_geometry, tolerance)?.into_iter();
let mut loops = Vec::with_capacity(renumbered.len());
for (li, l) in renumbered.iter().enumerate() {
loops.push(match l {
@@ -1532,15 +1536,23 @@ fn split_touching(faces: &mut [Face], edges: &mut Vec<Edge>, edge_source: &mut V
Ok(())
}
/// What the pcurve routine needs of one edge: the curve its parameter is on, its interval and its
/// tolerance.
struct EdgeGeometry<'a> {
curve: &'a b::Curve,
interval: [f64; 2],
tolerance: f64,
}
/// A face's pcurves, edge loop by edge loop: SMLib's own UV trims where `given` (loop by loop as
/// `loops`) has them, the plain-record routine on the face's records for the rest; each loop moved
/// by whole periods into the face's domain.
fn pcurves(
/// by whole periods into the face's domain. `edge` looks up an edge of `loops` by its index.
fn pcurves<'a>(
surface: &b::Surface,
domain: [[f64; 2]; 2],
loops: &[Loop],
given: &[Vec<Option<b::Curve2>>],
edges: &[b::Edge],
given: &'a [Vec<Option<b::Curve2>>],
edge: impl Fn(u32) -> Option<EdgeGeometry<'a>>,
tolerance: f64,
) -> Res<Vec<Vec<b::Curve2>>> {
if given.len() != loops.len() {
@@ -1554,13 +1566,13 @@ fn pcurves(
}
let mut loop_uses = Vec::with_capacity(l.len());
for (&(e, forward), given) in l.iter().zip(g) {
let edge = edges.get(e as usize).ok_or_else(|| invalid("edge"))?;
let geometry = edge(e).ok_or_else(|| invalid("edge"))?;
loop_uses.push(b::pcurve::EdgeUse {
edge: e,
curve: edge.nurbs.as_ref().unwrap_or(&edge.curve),
interval: edge.interval,
curve: geometry.curve,
interval: geometry.interval,
forward,
tolerance: edge.tolerance,
tolerance: geometry.tolerance,
given: given.as_ref(),
});
}
@@ -1575,12 +1587,12 @@ fn pcurves(
/// (positive: counter-clockwise, the record's convention); 0 where it has no edge loop.
fn outer_area(face: &Face, edges: &[Edge], tolerance: f64) -> Res<f64> {
let Some(Loop::Edges(outer)) = face.loops.first() else { return Ok(0.0) };
let records: Vec<b::Edge> = edges
.iter()
.map(|e| b::Edge { nurbs: e.nurbs.clone(), curve: e.curve.clone(), interval: e.interval, vertices: e.vertices, tolerance, uses: Vec::new() })
.collect();
let geometry = |e: u32| {
let edge = edges.get(e as usize)?;
Some(EdgeGeometry { curve: edge.nurbs.as_ref().unwrap_or(&edge.curve), interval: edge.interval, tolerance })
};
let given = face.given.first().map(std::slice::from_ref).unwrap_or(&[]);
let curves = pcurves(face.nurbs.as_ref().unwrap_or(&face.surface), face.domain, &[Loop::Edges(outer.clone())], given, &records, tolerance)?;
let curves = pcurves(face.nurbs.as_ref().unwrap_or(&face.surface), face.domain, &[Loop::Edges(outer.clone())], given, geometry, tolerance)?;
let mut pts: Vec<[f64; 2]> = Vec::new();
for (pc, &(e, forward)) in curves.first().map(Vec::as_slice).unwrap_or(&[]).iter().zip(outer) {
let edge = edges.get(e as usize).ok_or_else(|| invalid("edge"))?;
+89
View File
@@ -281,6 +281,95 @@ mod native_tests {
assert!(native(&shifted(1e-3)).map(|(r, _)| r.validate()).map_or(true, |v| v.is_err()), "a real overshoot is not moved");
}
/// [`outer_area`] as it was: every edge of the body copied into a record for each face.
#[allow(clippy::unwrap_used)] // test reference
fn outer_area_copying_every_edge(face: &Face, edges: &[Edge], tolerance: f64) -> Res<f64> {
let Some(Loop::Edges(outer)) = face.loops.first() else { return Ok(0.0) };
let records: Vec<b::Edge> = edges
.iter()
.map(|e| b::Edge {
nurbs: e.nurbs.clone(),
curve: e.curve.clone(),
interval: e.interval,
vertices: e.vertices,
tolerance,
uses: Vec::new(),
})
.collect();
let lookup = |e: u32| {
let edge = records.get(e as usize)?;
Some(EdgeGeometry { curve: edge.nurbs.as_ref().unwrap_or(&edge.curve), interval: edge.interval, tolerance: edge.tolerance })
};
let given = face.given.first().map(std::slice::from_ref).unwrap_or(&[]);
let curves = pcurves(face.nurbs.as_ref().unwrap_or(&face.surface), face.domain, &[Loop::Edges(outer.clone())], given, lookup, tolerance)?;
let mut pts: Vec<[f64; 2]> = Vec::new();
for (pc, &(e, forward)) in curves.first().map(Vec::as_slice).unwrap_or(&[]).iter().zip(outer) {
let [a, z] = edges.get(e as usize).unwrap().interval;
let mut run: Vec<[f64; 2]> = (0..=32).filter_map(|i| point2(pc, a + (z - a) * f64::from(i) / 32.0)).collect();
if !forward {
run.reverse();
}
pts.extend(run);
}
let n = pts.len();
Ok(0.5 * (0..n).map(|k| (pts[k], pts[(k + 1) % n])).map(|(p, q)| p[0] * q[1] - q[0] * p[1]).sum::<f64>())
}
/// A face's outer area reads only its own loop's edges, and is bit for bit what copying every
/// edge of the body gave: the export's face turns, and so its records and numbering, are
/// unchanged. Each body's record faces are counter-clockwise by convention, so positive.
#[test]
fn a_faces_outer_area_reads_only_its_own_loop_and_is_unchanged() {
let block = Brep::make_box([0.0; 3], [31.0, 29.0, 10.0]).unwrap();
let hole = Brep::make_cylinder([12.0, 14.5, -1.0], 5.0, 12.0).unwrap();
let rounded = Brep::make_box([0.0; 3], [60.0, 40.0, 20.0]).unwrap().fillet(&[[60.0, 40.0, 10.0]], 0.1, 5.0).unwrap();
let mut bodies: Vec<(&str, Brep)> = primitives().into_iter().map(|(name, b, _)| (name, b)).collect();
bodies.push(("box minus cylinder", block.boolean(&hole, crate::BooleanOp::Subtract).unwrap().unwrap()));
bodies.push(("rounded box", rounded));
for (name, body) in bodies {
let record = body.to_native().unwrap_or_else(|e| panic!("{name}: {e}"));
let edges: Vec<Edge> = record
.edges
.iter()
.map(|e| Edge {
curve: e.curve.clone(),
nurbs: e.nurbs.clone(),
interval: e.interval,
vertices: e.vertices,
reversed: false,
trim_map: None,
})
.collect();
for (f, rf) in record.faces.iter().enumerate() {
// Without SMLib's trims (the pcurve routine) and with the record's pcurves as them.
for with_given in [false, true] {
let loops: Vec<Loop> = rf
.loops
.iter()
.map(|l| match l {
b::Loop::Edges { edges } => Loop::Edges(edges.iter().map(|u| (u.edge, u.forward)).collect()),
b::Loop::Vertex { vertex } => Loop::Vertex(*vertex),
})
.collect();
let given = rf
.loops
.iter()
.map(|l| match l {
b::Loop::Edges { edges } => edges.iter().map(|u| with_given.then(|| u.pcurve.clone())).collect(),
b::Loop::Vertex { .. } => Vec::new(),
})
.collect();
let records = loops.iter().map(|_| Vec::new()).collect();
let face = Face { nurbs: rf.nurbs.clone(), surface: rf.surface.clone(), domain: rf.domain, agrees: true, loops, records, given };
let now = outer_area(&face, &edges, record.tolerance).unwrap_or_else(|e| panic!("{name}: face {f}: {e}"));
let before = outer_area_copying_every_edge(&face, &edges, record.tolerance).unwrap();
assert_eq!(now.to_bits(), before.to_bits(), "{name}: face {f}, trims given {with_given}: {now} != {before}");
assert!(now > 0.0, "{name}: face {f}, trims given {with_given}: area {now}");
}
}
}
}
#[test]
fn hostile_arrays_are_errors_never_panics() {
let good = Brep::make_box([0.0; 3], [1.0, 2.0, 3.0]).unwrap().arrays().unwrap();