Fill: a patch's edges are the body's own edges again, circles and ellipses included

A record edge on a circle or ellipse carries the NURBS form SMLib holds it as,
its interval on that form. The patch built its loop edges from the form alone
(sew::PartEdge had no place for it), so a fill's rim came back from SMLib as a
plain B-spline rather than the body's ellipse. PartEdge now carries the form;
assemble keeps a given form and labels the edge with what it is.

Test helpers that sampled an edge on `curve` with the form's interval picked
points off the edge ("no edge at …"); they sample `parameters()`. The fill's
own-edge check compares the curve's kind and the exact form it is parameterised
on: SMLib renormalises a conic's frame axes in the last bit on a round trip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 22:06:06 -04:00
committed by Irakli Kourtis
parent 9095fce42d
commit b68f71a2de
7 changed files with 42 additions and 19 deletions
@@ -214,7 +214,7 @@ fn too_large_or_unsupported_is_refused() {
.record()
.edges
.iter()
.filter_map(|e| solidcraft_geom::brep::eval::CurveEval::new(&e.curve).ok()?.point(0.5 * (e.interval[0] + e.interval[1])))
.filter_map(|e| solidcraft_geom::brep::eval::CurveEval::new(e.parameters()).ok()?.point(0.5 * (e.interval[0] + e.interval[1])))
.find(|m| (m.z - 10.0).abs() < 1e-6)
.unwrap();
match chamfer_sides(&t, &[rim], 1.0, ChamferSide::Distance(2.0), false) {
+1
View File
@@ -1379,6 +1379,7 @@ mod tests {
let vertices: Vec<[f64; 3]> = corners.iter().map(|&(i, j)| net[j * cu + i]).collect();
let edges = (0..4)
.map(|k| PartEdge {
nurbs: None,
curve: b::Curve::Line { start: vertices[k], end: vertices[(k + 1) % 4] },
interval: [0.0, 1.0],
vertices: [k as u32, ((k + 1) % 4) as u32],
+14 -10
View File
@@ -709,7 +709,7 @@ fn patch_body(body: &Body, record: &b::Brep, chain: &[(usize, bool)], surface: b
})?),
Pcurves::Computed => None,
};
edges.push(PartEdge { curve: edge.parameters().clone(), interval: edge.interval, vertices: ends });
edges.push(PartEdge { curve: edge.curve.clone(), nurbs: edge.nurbs.clone(), interval: edge.interval, vertices: ends });
uses.push(PartUse { edge: k as u32, forward, given });
}
if !ccw {
@@ -797,9 +797,9 @@ fn quilt_body(body: &Body, record: &b::Brep, chain: &[(usize, bool)], fill: &Iso
};
}
let id = index(edges.len())?;
// The curve the interval and the pcurve's parameter are on: a conic's
// NURBS form.
edges.push(PartEdge { curve: edge.parameters().clone(), interval: [lo, hi], vertices: ends });
// The edge itself: the interval and the pcurve's parameter are on its
// NURBS form (a conic's), which it keeps.
edges.push(PartEdge { curve: edge.curve.clone(), nurbs: edge.nurbs.clone(), interval: [lo, hi], vertices: ends });
origin.push(Some(e));
piece_of.insert(key, id);
id
@@ -827,7 +827,7 @@ fn quilt_body(body: &Body, record: &b::Brep, chain: &[(usize, bool)], fill: &Iso
b::Curve::Spline { knots: curve.knots.clone(), points: curve.points.iter().map(|p| [p.x, p.y, p.z]).collect() }
}
};
edges.push(PartEdge { curve: spline, interval: [0.0, 1.0], vertices: [start, end] });
edges.push(PartEdge { curve: spline, nurbs: None, interval: [0.0, 1.0], vertices: [start, end] });
origin.push(None);
spoke_of.insert(spoke, id);
id
@@ -976,11 +976,14 @@ mod tests {
worst
}
/// Whether every edge of `made` is one of `of`'s own (the same curve record on the same
/// interval).
/// Whether every edge of `made` is one of `of`'s own: the same kind of curve, on the same
/// curve record it is parameterised on (its NURBS form where it has one), on the same
/// interval. (A circle's or ellipse's frame is SMLib's again after a round trip, its axes
/// renormalised to the last bit.)
fn own_edges(made: &Body, of: &Body) -> bool {
let own = record(of).edges;
record(made).edges.iter().all(|e| own.iter().any(|o| o.curve == e.curve && o.interval == e.interval))
let kind = |e: &b::Edge| std::mem::discriminant(&e.curve);
record(made).edges.iter().all(|e| own.iter().any(|o| kind(o) == kind(e) && o.parameters() == e.parameters() && o.interval == e.interval))
}
/// The fill's mesh: every normal finite, and no triangle folded over (all face the same way
@@ -1109,9 +1112,10 @@ mod tests {
let mut edges = Vec::new();
for k in 0..n {
let c = &top[k];
edges.push(PartEdge { curve: c.clone(), interval: interval(c), vertices: [k as u32, ((k + 1) % n) as u32] });
edges.push(PartEdge { curve: moved(c), interval: interval(c), vertices: [(n + k) as u32, (n + (k + 1) % n) as u32] });
edges.push(PartEdge { nurbs: None, curve: c.clone(), interval: interval(c), vertices: [k as u32, ((k + 1) % n) as u32] });
edges.push(PartEdge { nurbs: None, curve: moved(c), interval: interval(c), vertices: [(n + k) as u32, (n + (k + 1) % n) as u32] });
edges.push(PartEdge {
nurbs: None,
curve: b::Curve::Line { start: arr(corners[k]), end: arr(corners[k] + drop) },
interval: [0.0, 1.0],
vertices: [k as u32, (n + k) as u32],
+2 -2
View File
@@ -108,7 +108,7 @@ fn rim_points(body: &Body, per: usize) -> Vec<Vec3> {
let r = body.record();
let mut out = Vec::new();
for e in r.edges.iter().filter(|e| e.uses.len() == 1) {
let c = CurveEval::new(&e.curve).unwrap();
let c = CurveEval::new(e.parameters()).unwrap();
let [t0, t1] = e.interval;
for k in 0..per {
out.push(c.point(t0 + (t1 - t0) * (k as f64 + 0.5) / per as f64).unwrap());
@@ -238,7 +238,7 @@ fn free_edge_points(body: &Body) -> Vec<Vec3> {
.edges
.iter()
.filter(|e| e.uses.len() == 1)
.map(|e| CurveEval::new(&e.curve).unwrap().point(0.5 * (e.interval[0] + e.interval[1])).unwrap())
.map(|e| CurveEval::new(e.parameters()).unwrap().point(0.5 * (e.interval[0] + e.interval[1])).unwrap())
.collect::<Vec<_>>();
// In a fixed order.
points.sort_by(|a, b| (a.z, a.y, a.x).partial_cmp(&(b.z, b.y, b.x)).unwrap());
+14 -3
View File
@@ -286,7 +286,7 @@ impl Builder {
}
};
let id = u32::try_from(self.edges.len()).map_err(|_| KernelError::Invalid("sew: edge budget exceeded".into()))?;
self.edges.push(PartEdge { curve, interval, vertices: [va, vb] });
self.edges.push(PartEdge { nurbs: None, curve, interval, vertices: [va, vb] });
self.by_key.insert((ka, fa, kb, fb, mid, sheet), id);
Ok((id, true))
}
@@ -1303,6 +1303,10 @@ pub(crate) struct PartUse {
pub(crate) struct PartEdge {
pub curve: b::Curve,
/// The NURBS form the edge is parameterised on, as a record edge keeps it
/// ([`b::Edge::nurbs`]): given, `interval` is on it and `curve` only says what the edge is,
/// so an edge taken from a record goes back as that same edge.
pub nurbs: Option<b::Curve>,
pub interval: [f64; 2],
pub vertices: [u32; 2],
}
@@ -1366,8 +1370,8 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
g
};
// Each edge's label and NURBS form (`form::edge`): a circle about a revolved face's axis on
// that face's frame, so its form lies on the face's quarter grid; an edge the caller gave a
// pcurve for keeps the parameter that pcurve is on.
// that face's frame, so its form lies on the face's quarter grid; an edge given with its form
// keeps it, and an edge the caller gave a pcurve for keeps the parameter that pcurve is on.
let mut frames_of: Vec<Vec<b::Frame>> = vec![Vec::new(); edges.len()];
for f in &faces {
let Some(frame) = b::form::frame_of(&f.surface) else { continue };
@@ -1380,6 +1384,13 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
let mut labels: Vec<(b::Curve, [f64; 2])> = Vec::with_capacity(edges.len());
let mut forms: Vec<Option<(b::Curve, [f64; 2])>> = Vec::with_capacity(edges.len());
for ((e, frames), given) in edges.iter().zip(&frames_of).zip(&given_on) {
if let Some(form) = &e.nurbs {
let on_label =
b::curve_interval(&e.curve, Some(form), e.interval).ok_or_else(|| fail("an edge's NURBS form does not evaluate".into()))?;
labels.push((e.curve.clone(), on_label));
forms.push(Some((form.clone(), e.interval)));
continue;
}
if *given {
labels.push((e.curve.clone(), e.interval));
forms.push(None);
+8 -3
View File
@@ -784,11 +784,16 @@ fn a_face_bounded_by_two_short_arcs_keeps_an_area_to_mesh() {
let sq = [[3000.0, 0.0, 0.0], [5000.0, 0.0, 0.0], [5000.0, 2000.0, 0.0], [3000.0, 2000.0, 0.0]];
let plane = Surface::Plane { origin: [0.0; 3], u_axis: [1.0, 0.0, 0.0], v_axis: [0.0, 1.0, 0.0] };
let mut edges = vec![
PartEdge { curve: circle(-c), interval: [top, PI - top], vertices: [0, 1] },
PartEdge { curve: circle(c), interval: [PI + top, TAU - top], vertices: [1, 0] },
PartEdge { nurbs: None, curve: circle(-c), interval: [top, PI - top], vertices: [0, 1] },
PartEdge { nurbs: None, curve: circle(c), interval: [PI + top, TAU - top], vertices: [1, 0] },
];
for k in 0..4 {
edges.push(PartEdge { curve: line(sq[k], sq[(k + 1) % 4]), interval: [0.0, 1.0], vertices: [2 + k as u32, 2 + ((k + 1) % 4) as u32] });
edges.push(PartEdge {
nurbs: None,
curve: line(sq[k], sq[(k + 1) % 4]),
interval: [0.0, 1.0],
vertices: [2 + k as u32, 2 + ((k + 1) % 4) as u32],
});
}
let uses = |es: &[u32]| es.iter().map(|&edge| PartUse { edge, forward: true, given: None }).collect::<Vec<_>>();
let mut vertices = vec![[half, 0.0, 0.0], [-half, 0.0, 0.0]];
+2
View File
@@ -61,11 +61,13 @@ fn a_face_touching_its_own_edge_meshes() {
let (h, dip) = (0.3175, 2.8e-7);
let vertices = vec![[0.0, 0.0, 0.0], [5.0, 0.0, 0.0], [5.0, h, 0.0], [0.0, h, 0.0]];
let line = |a: usize, b: usize| PartEdge {
nurbs: None,
curve: Curve::Line { start: vertices[a], end: vertices[b] },
interval: [0.0, 1.0],
vertices: [a as u32, b as u32],
};
let arch = PartEdge {
nurbs: None,
curve: Curve::Spline { knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0], points: vec![[5.0, h, 0.0], [2.5, -h - 2.0 * dip, 0.0], [0.0, h, 0.0]] },
interval: [0.0, 1.0],
vertices: [2, 3],