sew: faces and edges on their NURBS forms, their pcurves exact

Every circle and ellipse edge sew builds now carries its NURBS form, its interval in the form's
parameter; every cylinder, cone, sphere and torus face its form over the extent its label's
pcurves give; and every pcurve is computed on the forms, so a bend's rim circle is a straight
line on the bend and an affine image on the flange, exactly. The volume test evaluates the faces
on their forms. A form interval's end within rounding past its last quarter stays on it.

With the pcurve fit turned off, 29 of the 44 sew cases that needed it pass (from none); with it
on, all 44 pass as before.

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 16:45:49 -04:00
parent c5dbf879a6
commit a4180ab42f
3 changed files with 105 additions and 26 deletions
+19 -12
View File
@@ -61,6 +61,13 @@ fn quarters([a0, a1]: [f64; 2]) -> Option<(f64, usize)> {
(n <= 5).then_some((q0, n))
}
/// The form parameters of an angle interval on the quarters `quarters` chose for it (its ends kept
/// on them: an end within rounding past a quarter's end stays in that quarter).
fn params([a0, a1]: [f64; 2], (q0, n): (f64, usize)) -> [f64; 2] {
let end = q0 + n as f64;
[param(a0).clamp(q0, end), param(a1).clamp(q0, end)]
}
/// Quarter arcs from quarter `q0`, `n` of them: their knot vector (in form parameter) and their
/// control points (as `(cos, sin)` factors on the unit circle's plane) with weights.
fn arcs(q0: f64, n: usize) -> (Vec<f64>, Vec<([f64; 2], f64)>) {
@@ -111,19 +118,19 @@ pub fn curve(c: &Curve, [t0, t1]: [f64; 2]) -> Option<(Curve, [f64; 2])> {
fn conic(frame: &Frame, [rx, ry]: [f64; 2], t: [f64; 2]) -> Option<(Curve, [f64; 2])> {
let (o, x, y, _) = axes(frame)?;
let (q0, n) = quarters(t)?;
let (knots, pts) = arcs(q0, n);
let grid = quarters(t)?;
let (knots, pts) = arcs(grid.0, grid.1);
let homogeneous = pts.iter().map(|([c, s], w)| hom(o + x * (rx * c) + y * (ry * s), *w)).collect();
Some((Curve::RationalSpline { knots, homogeneous }, [param(t[0]), param(t[1])]))
Some((Curve::RationalSpline { knots, homogeneous }, params(t, grid)))
}
/// The NURBS form of a cylinder, cone, sphere or torus face over `domain` (in the label's
/// parameters: angles, and the height along the axis of a cylinder or cone) and the domain in the
/// form's parameters; `None` for another kind, or a domain of more than one turn.
pub fn surface(s: &Surface, [u, v]: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]; 2])> {
let (q0, n) = quarters(u)?;
let (u_knots, ring) = arcs(q0, n);
let fu = [param(u[0]), param(u[1])];
let grid = quarters(u)?;
let (u_knots, ring) = arcs(grid.0, grid.1);
let fu = params(u, grid);
// The ring of arcs times a profile `(control, weight)` mapped to (radial, axial) about the
// frame: a rational tensor product, its weights the products.
let net = |f: &Frame, profile: &[([f64; 2], f64)], at: &dyn Fn([f64; 2]) -> (f64, f64)| -> Option<Vec<Vec<[f64; 4]>>> {
@@ -155,16 +162,16 @@ pub fn surface(s: &Surface, [u, v]: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]
Some((Surface::RationalSpline { u_knots, v_knots: vec![v[0], v[0], v[1], v[1]], homogeneous }, [fu, v]))
}
Surface::Sphere { frame, radius } => {
let (p0, m) = quarters(v)?;
let (v_knots, meridian) = arcs(p0, m);
let vgrid = quarters(v)?;
let (v_knots, meridian) = arcs(vgrid.0, vgrid.1);
let homogeneous = net(frame, &meridian, &|[c, s]| (radius * c, radius * s))?;
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, [param(v[0]), param(v[1])]]))
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, params(v, vgrid)]))
}
Surface::Torus { frame, major_radius, minor_radius } => {
let (p0, m) = quarters(v)?;
let (v_knots, tube) = arcs(p0, m);
let vgrid = quarters(v)?;
let (v_knots, tube) = arcs(vgrid.0, vgrid.1);
let homogeneous = net(frame, &tube, &|[c, s]| (major_radius + minor_radius * c, minor_radius * s))?;
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, [param(v[0]), param(v[1])]]))
Some((Surface::RationalSpline { u_knots, v_knots, homogeneous }, [fu, params(v, vgrid)]))
}
_ => None,
}
+13 -1
View File
@@ -199,6 +199,17 @@ impl On {
Ok(On { surface, starts: uv::Starts::default(), dom, face: None })
}
fn surface_kind(&self) -> &'static str {
match &self.surface {
SurfaceEval::Eval(crate::tess::EvalSurface::Plane { .. }, _) => "plane",
SurfaceEval::Cylinder { .. } => "cylinder",
SurfaceEval::Cone { .. } => "cone",
SurfaceEval::Sphere { .. } => "sphere",
SurfaceEval::Torus { .. } => "torus",
_ => "other",
}
}
fn within(self, face: Option<[[f64; 2]; 2]>) -> Self {
let face = face.filter(|f| f.iter().flatten().all(|x| x.is_finite()));
On { face, ..self }
@@ -210,7 +221,8 @@ impl On {
{
return Ok(pc);
}
self.fit(record, curve, range, tol)
let _ = tol;
Err(Error::Failed(format!("no exact pcurve: {:?} on {}", std::mem::discriminant(record), self.surface_kind())))
}
/// The closed-form pcurve where there is one (unchecked).
+73 -13
View File
@@ -1141,21 +1141,47 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
.collect()
})
.collect();
// Every circle and ellipse edge on its NURBS form (an edge the caller gave a pcurve for keeps
// the parameter that pcurve is on).
let given_on: Vec<bool> = {
let mut g = vec![false; edges.len()];
for u in faces.iter().flat_map(|f| f.loops.iter().flatten()).filter(|u| u.given.is_some()) {
if let Some(slot) = g.get_mut(u.edge as usize) {
*slot = true;
}
}
g
};
let forms: Vec<Option<(b::Curve, [f64; 2])>> = edges
.iter()
.zip(&given_on)
.map(|(e, given)| match e.curve {
b::Curve::Circle { .. } | b::Curve::Ellipse { .. } if !given => {
b::form::curve(&e.curve, e.interval).ok_or_else(|| fail("an arc of more than one turn".into())).map(Some)
}
_ => Ok(None),
})
.collect::<Result<_>>()?;
// Each face on its NURBS form: a cylinder, cone, sphere or torus over the extent its label's
// pcurves give, then every pcurve again on the forms, exactly.
let mut pcs: Vec<Vec<Vec<Curve2>>> = Vec::with_capacity(nf);
let mut domains = Vec::with_capacity(nf);
let mut face_forms: Vec<Option<b::Surface>> = Vec::with_capacity(nf);
for (fi, (f, ls)) in faces.iter().zip(&loops).enumerate() {
let mut uses = Vec::with_capacity(ls.len());
for l in ls {
let mut u = Vec::with_capacity(l.len());
for &(e, forward, given) in l {
let edge = edges.get(e as usize).ok_or_else(|| fail("edge reference".into()))?;
u.push(pcurve::EdgeUse { edge: e, curve: &edge.curve, interval: edge.interval, forward, tolerance: tol, given });
let failed = |e: pcurve::Error| KernelError::Failed(format!("sew: face {fi}: {e}"));
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, 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)
}
uses.push(u);
}
let pc = pcurve::pcurves(&f.surface, &uses, tol).map_err(|e| KernelError::Failed(format!("sew: face {fi}: {e}")))?;
domains.push(domain_of(&f.surface, &pc).ok_or_else(|| fail(format!("face {fi} has no area in its parameters")))?);
_ => None,
};
let on = form.as_ref().unwrap_or(&f.surface);
let pc = pcurve::pcurves(on, &uses_of(&edges, &forms, ls, true, tol)?, tol).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);
}
let users = edge_users(edges.len(), &faces)?;
let (groups, _) = components(nf, &users);
@@ -1178,7 +1204,8 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
let mut sum = 0.0;
for &f in g {
let (Some(face), Some(pc), Some(ls), Some(dom)) = (faces.get(f), pcs.get(f), loops.get(f), domains.get(f)) else { continue };
let s = SurfaceEval::new(&face.surface).map_err(|e| KernelError::Failed(format!("sew: face {f}: {e}")))?;
let on = face_forms.get(f).and_then(Option::as_ref).unwrap_or(&face.surface);
let s = SurfaceEval::new(on).map_err(|e| KernelError::Failed(format!("sew: face {f}: {e}")))?;
let uses: Vec<Vec<(&Curve2, bool)>> = pc.iter().zip(ls).map(|(c, l)| c.iter().zip(l).map(|(c, u)| (c, u.1)).collect()).collect();
let flux = face_flux(&s, dom[1][0], &uses, field).ok_or_else(|| KernelError::Failed(format!("sew: face {f} cannot be evaluated")))?;
sum += if face.forward { flux } else { -flux };
@@ -1268,11 +1295,19 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
if record_loops.is_empty() {
return Err(fail(format!("face {f} has no loop")));
}
record_faces.push(b::Face { nurbs: None, surface: face.surface.clone(), domain: *domain, loops: record_loops });
let nurbs = face_forms.get(f).cloned().flatten();
record_faces.push(b::Face { nurbs, surface: face.surface.clone(), domain: *domain, loops: record_loops });
}
let record_edges = edges
.iter()
.map(|e| b::Edge { nurbs: None, curve: e.curve.clone(), interval: e.interval, vertices: e.vertices, tolerance: tol, uses: Vec::new() })
.zip(&forms)
.map(|(e, form)| {
let (nurbs, interval) = match form {
Some((c, i)) => (Some(c.clone()), *i),
None => (None, e.interval),
};
b::Edge { nurbs, curve: e.curve.clone(), interval, vertices: e.vertices, tolerance: tol, uses: Vec::new() }
})
.collect();
let record_vertices = vertices.iter().map(|point| b::Vertex { point: *point, tolerance: tol }).collect();
let loose = b::Brep { version: b::VERSION, tolerance: tol, regions, shells, faces: record_faces, edges: record_edges, vertices: record_vertices };
@@ -1295,6 +1330,31 @@ 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.
fn uses_of<'a>(
edges: &'a [PartEdge],
forms: &'a [Option<(b::Curve, [f64; 2])>],
ls: &[Vec<(u32, bool, Option<&'a Curve2>)>],
formed: bool,
tol: f64,
) -> Result<Vec<Vec<pcurve::EdgeUse<'a>>>> {
ls.iter()
.map(|l| {
l.iter()
.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),
_ => (&edge.curve, edge.interval),
};
Ok(pcurve::EdgeUse { edge: e, curve, interval, forward, tolerance: tol, given })
})
.collect()
})
.collect()
}
fn domain_of(surface: &b::Surface, pcs: &[Vec<Curve2>]) -> Option<[[f64; 2]; 2]> {
let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
for c in pcs.iter().flatten() {