sew, step: face forms from the boundary's extent; trimmed profiles exact

sew and the STEP reader build a revolved face's NURBS form over `form::extent` (its boundary on
its label) instead of a pass of pcurves on the label, and compute its pcurves within the form's
box. The STEP reader gives revolutions their form too, and a trimmed swept profile its file's
trim range exactly (points on the axis fit either side of it). The STEP seam tests read edge
angles through `Edge::curve_interval`, intervals and domains being in the forms' parameters.
step 37/37.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BnQawySUFx57iaJZq7z1Fm
This commit is contained in:
jyviko
2026-10-23 19:26:11 -04:00
parent 2883981a02
commit 6c385c8439
4 changed files with 74 additions and 30 deletions
+20 -13
View File
@@ -1365,12 +1365,12 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
// 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.
let mut frames_of: Vec<Vec<&b::Frame>> = vec![Vec::new(); edges.len()];
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 };
for u in f.loops.iter().flatten() {
if let Some(slot) = frames_of.get_mut(u.edge as usize) {
slot.push(frame);
slot.push(frame.clone());
}
}
}
@@ -1383,7 +1383,7 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
continue;
}
let (label, interval, form) =
b::form::edge(&e.curve, e.interval, frames.iter().copied(), tol).ok_or_else(|| fail("an arc of more than one turn".into()))?;
b::form::edge(&e.curve, e.interval, frames.iter(), tol).ok_or_else(|| fail("an arc of more than one turn".into()))?;
labels.push((label, interval));
forms.push(form);
}
@@ -1405,18 +1405,26 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
})
.collect()
};
let folded_label = folded_on(None);
// A cylinder, cone, sphere or torus on its NURBS form, over the extent of its boundary on
// its label.
let form = match f.surface {
b::Surface::Cylinder { .. } | b::Surface::Cone { .. } | b::Surface::Sphere { .. } | b::Surface::Torus { .. } => {
let label = pcurve::pcurves(&f.surface, &uses_of(&edges, &forms, ls, false, &folded_label, tol)?, tol).map_err(failed)?;
let extent = domain_of(&f.surface, &label).ok_or_else(|| fail(format!("face {fi} has no area in its parameters")))?;
Some(b::form::surface(&f.surface, extent).ok_or_else(|| fail(format!("face {fi}: more than one turn")))?.0)
let boundary: Vec<Vec<(&b::Curve, [f64; 2], bool)>> = ls
.iter()
.map(|l| l.iter().filter_map(|&(e, forward, _)| labels.get(e as usize).map(|(c, i)| (c, *i, forward))).collect())
.collect();
let extent = b::form::extent(&f.surface, &boundary).ok_or_else(|| fail(format!("face {fi}: its boundary is not on its surface")))?;
Some(b::form::surface(&f.surface, extent).ok_or_else(|| fail(format!("face {fi}: more than one turn")))?)
}
_ => None,
};
let (form, form_box) = match form {
Some((s, d)) => (Some(s), Some(d)),
None => (None, None),
};
let on = form.as_ref().unwrap_or(&f.surface);
let folded_form = if form.is_some() { folded_on(form.as_ref()) } else { Vec::new() };
let pc = pcurve::pcurves(on, &uses_of(&edges, &forms, ls, true, &folded_form, tol)?, tol).map_err(failed)?;
let pc = pcurve::pcurves_within(on, &uses_of(&edges, &forms, ls, &folded_form, tol)?, tol, form_box).map_err(failed)?;
domains.push(domain_of(on, &pc).ok_or_else(|| fail(format!("face {fi} has no area in its parameters")))?);
pcs.push(pc);
face_forms.push(form);
@@ -1568,14 +1576,13 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
/// A face's parameter box: around its pcurves' control points (a B-spline lies in their hull),
/// held inside the surface's natural domain (one turn where periodic). None when it has no
/// width in either direction.
/// A face's edge uses for `pcurve::pcurves`: on each edge's NURBS form where it has one and
/// `formed`, else on its curve as given; `folded` gives the pcurves of edges folded across the
/// face (see [`folded_pcurve`]).
/// A face's edge uses for `pcurve::pcurves`: on each edge's NURBS form where it has one, else on
/// its curve as given; `folded` gives the pcurves of edges folded across the face (see
/// [`folded_pcurve`]).
fn uses_of<'a>(
edges: &'a [PartEdge],
forms: &'a [Option<(b::Curve, [f64; 2])>],
ls: &[Vec<(u32, bool, Option<&'a Curve2>)>],
formed: bool,
folded: &'a [(u32, Curve2)],
tol: f64,
) -> Result<Vec<Vec<pcurve::EdgeUse<'a>>>> {
@@ -1585,7 +1592,7 @@ fn uses_of<'a>(
.map(|&(e, forward, given)| {
let edge = edges.get(e as usize).ok_or_else(|| KernelError::Invalid("sew: edge reference".into()))?;
let (curve, interval) = match forms.get(e as usize).and_then(Option::as_ref) {
Some((c, i)) if formed => (c, *i),
Some((c, i)) => (c, *i),
_ => (&edge.curve, edge.interval),
};
let given = given.or_else(|| folded.iter().find(|(f, _)| *f == e).map(|(_, c)| c));
+22 -9
View File
@@ -965,34 +965,47 @@ impl<'b, 'a> Builder<'b, 'a> {
// Which loop index in `face.loops` each edge loop is.
let positions: Vec<usize> = face.loops.iter().enumerate().filter(|(_, l)| matches!(l, LoopB::Edges(_))).map(|(i, _)| i).collect();
let tol = self.tol;
// The face's pcurves on `on`, each edge on its NURBS form when `formed`.
let pcurves_on = |on: &BSurface, formed: bool| -> R<Vec<Vec<Curve2>>> {
// The face's pcurves on `on`, each edge on its NURBS form.
let pcurves_on = |on: &BSurface, domain: Option<[[f64; 2]; 2]>| -> R<Vec<Vec<Curve2>>> {
let mut uses = Vec::with_capacity(edge_loops.len());
for l in &edge_loops {
let mut u = Vec::with_capacity(l.len());
for &(e, forward) in l {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
let (curve, interval) = match &edge.form {
Some((c, i)) if formed => (c, *i),
Some((c, i)) => (c, *i),
_ => (&edge.curve, edge.interval),
};
u.push(EdgeUse { edge: e, curve, interval, forward, tolerance: 0.0, given: None });
}
uses.push(u);
}
pcurve::pcurves(on, &uses, tol).map_err(|e| e.to_string())
pcurve::pcurves_within(on, &uses, tol, domain).map_err(|e| e.to_string())
};
// A cylinder, cone, sphere or torus on its NURBS form, over the extent its label's pcurves
// give; then every pcurve on the forms.
// A cylinder, cone, sphere, torus or revolution on its NURBS form, over the extent of its
// boundary on its label; then every pcurve on the forms.
let form = match brep::form::frame_of(&label) {
Some(_) => {
let extent = face_domain(&label, &pcurves_on(&label, false)?)?;
Some(brep::form::surface(&label, extent).ok_or_else(|| unsupported("a face of more than one turn"))?.0)
let mut boundary = Vec::with_capacity(edge_loops.len());
for l in &edge_loops {
let mut b = Vec::with_capacity(l.len());
for &(e, forward) in l {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
b.push((&edge.curve, edge.interval, forward));
}
boundary.push(b);
}
let extent = brep::form::extent(&label, &boundary).ok_or_else(|| unsupported("a face whose boundary does not lie on its surface"))?;
Some(brep::form::surface(&label, extent).ok_or_else(|| unsupported("a face of more than one turn"))?)
}
None => None,
};
let (form, box_on_form) = match form {
Some((f, d)) => (Some(f), Some(d)),
None => (None, None),
};
let surface = form.clone().unwrap_or(label);
let pcs = pcurves_on(&surface, true)?;
let pcs = pcurves_on(&surface, box_on_form)?;
// Signed areas of the loops that close in (u, v).
let periods = natural_periods(&surface);
// Each use starts near where the one before it ends, along a bounded direction that
+21 -4
View File
@@ -237,6 +237,14 @@ fn whole(c: &Curve) -> R<Option<BCurve>> {
/// The middle parameter of a TRIMMED_CURVE trimmed by parameters (on a closed conic, the
/// middle of the arc its sense runs over).
fn trimmed_middle(c: &Curve) -> Option<f64> {
let (a, b) = trimmed_range(c)?;
let m = 0.5 * (a + b);
m.is_finite().then_some(m)
}
/// A trimmed profile's range on its basis, low end first, where both trims are parameters: on
/// a closed basis (a circle, an ellipse) the way round `sense` gives.
fn trimmed_range(c: &Curve) -> Option<(f64, f64)> {
let Curve::Trimmed { basis, trim_1, trim_2, sense, .. } = c else { return None };
let param = |t: &[crate::geom::Trim]| {
t.iter().find_map(|x| match x {
@@ -248,8 +256,8 @@ fn trimmed_middle(c: &Curve) -> Option<f64> {
if is_closed(basis) {
b = if *sense { a + (b - a).rem_euclid(TAU) } else { a - (a - b).rem_euclid(TAU) };
}
let m = 0.5 * (a + b);
m.is_finite().then_some(m)
let (lo, hi) = (a.min(b), a.max(b));
(lo.is_finite() && hi.is_finite() && lo < hi).then_some((lo, hi))
}
fn is_closed(c: &Curve) -> bool {
@@ -309,7 +317,12 @@ fn revolution(profile: &Curve, axis: &Axis1, ext: &Extent) -> R<BSurface> {
// The profile's side of the axis where the face is.
let mid = ts.get(ts.len() / 2).copied().ok_or("a face without boundary points")?;
let e = probe(mid).map(radial).and_then(|r| r.normalized()).unwrap_or(e);
let s = span(&ts, &seam, is_closed(c))?;
// A trimmed profile is the file's range of its basis exactly; otherwise the face's
// reach along it.
let s = match trimmed_range(profile) {
Some(r) => Span::Open { inner: r, outer: r },
None => span(&ts, &seam, is_closed(c))?,
};
// Stop where the profile crosses the axis outside the face.
let side = |t: f64| probe(t).map(|q| (q - o).dot(e));
let s = match s {
@@ -394,7 +407,11 @@ fn extrusion(profile: &Curve, direction: P3, magnitude: f64, ext: &Extent) -> R<
return Err(unsupported("an extrusion along its profile's plane"));
}
let seam: Vec<f64> = ext.seam.iter().filter_map(|q| onto(*q)).collect();
bounded(c, span(&ts, &seam, is_closed(c))?)?
let s = match trimmed_range(profile) {
Some(r) => Span::Open { inner: r, outer: r },
None => span(&ts, &seam, is_closed(c))?,
};
bounded(c, s)?
}
};
let (knots, homogeneous, rational) = match curve {
+11 -4
View File
@@ -373,14 +373,20 @@ fn a_cylinder_has_a_seam_used_twice_by_its_side() {
assert_eq!(seam.uses[0].face, seam.uses[1].face);
for e in &s.brep.edges {
if matches!(e.curve, BCurve::Circle { .. }) {
assert!((e.interval[1] - e.interval[0] - TAU).abs() < 1e-12);
// On its NURBS form: a whole turn in angle, its interval in the form's parameter.
let [a0, a1] = e.curve_interval().unwrap();
assert!(e.nurbs.is_some() && (a1 - a0 - TAU).abs() < 1e-12);
assert_eq!(e.vertices[0], e.vertices[1]);
}
}
faces_away(&s.brep, 0, |p| [1.0, 2.0, p[2].clamp(1.0, 9.0)]);
// The side face spans one turn of the cylinder and its height.
// The side face spans one turn of the cylinder (four quarters of its NURBS form) and its height.
let f = s.brep.faces.iter().find(|f| matches!(f.surface, BSurface::Cylinder { .. })).unwrap();
assert!((f.domain[0][1] - f.domain[0][0] - TAU).abs() < 1e-9 && f.domain[1][0] < 1e-6 && f.domain[1][1] > 10.0 - 1e-6, "{:?}", f.domain);
assert!(
f.nurbs.is_some() && (f.domain[0][1] - f.domain[0][0] - 4.0).abs() < 1e-9 && f.domain[1][0] < 1e-6 && f.domain[1][1] > 10.0 - 1e-6,
"{:?}",
f.domain
);
}
#[test]
@@ -402,7 +408,8 @@ fn a_ball_bounded_by_its_seam_alone() {
let s = one_solid(&w.file(&[solid]));
assert_eq!(counts(&s.brep), [2, 1, 1, 1, 2]);
let e = &s.brep.edges[0];
assert!((e.interval[1] - e.interval[0] - PI).abs() < 1e-12, "{:?}", e.interval);
let [a0, a1] = e.curve_interval().unwrap();
assert!((a1 - a0 - PI).abs() < 1e-12, "{:?}", e.interval);
faces_away(&s.brep, 0, |_| [0.0; 3]);
}
}