sew: a circle on a revolved face is labelled on that face's frame

The bend's rim arcs carried frames of their own (x toward the arc's start), a fraction of a
quarter from the bend cylinder's, so their forms were off the cylinder's quarter grid and no
straight pcurve lay along them. `form::on_frame` relabels such a circle on the face's frame (the
same circle, its interval shifted); its form is then on the face's grid, its pcurve u = ±s + c.
`along` takes u or v as the edge's parameter either way along, offset. The volume test steps to
a pcurve's ends exactly (t0(1-f) + t1 f), as an interval's end may now carry rounding.

With the fit off, 38 of the 44 sew cases pass (from 29). With it on, the rig is unchanged
(333/372, ladder 101/116, no case changed).

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 17:04:39 -04:00
parent 57080f88ce
commit 76a254137c
3 changed files with 58 additions and 10 deletions
+19
View File
@@ -176,3 +176,22 @@ pub fn surface(s: &Surface, [u, v]: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]
_ => None,
}
}
/// A circle edge labelled on another frame about the same axis: the same circle, its angle
/// measured from `frame`'s x axis (its y axis taken so the circle keeps its own direction), and
/// its interval shifted to match, so its form lies on `frame`'s quarter grid. `None` when the
/// circle is not about `frame`'s axis (centre on it, normal along it, within `tol`).
pub fn on_frame(c: &Curve, [t0, t1]: [f64; 2], frame: &Frame, tol: f64) -> Option<(Curve, [f64; 2])> {
let Curve::Circle { frame: own, radius } = c else { return None };
let (o, x, _, z) = axes(own)?;
let (fo, fx, _, fz) = axes(frame)?;
let d = o - fo;
if !((d - fz * d.dot(fz)).len() <= tol && (z.dot(fz).abs() - 1.0).abs() <= 1e-9) {
return None;
}
let nx = (fx - z * fx.dot(z)).normalized()?;
let ny = z.cross(nx);
let shift = x.dot(ny).atan2(x.dot(nx));
let relabel = Frame { origin: [o.x, o.y, o.z], x_axis: [nx.x, nx.y, nx.z], y_axis: [ny.x, ny.y, ny.z] };
Some((Curve::Circle { frame: relabel, radius: *radius }, [t0 + shift, t1 + shift]))
}
+8 -2
View File
@@ -317,9 +317,15 @@ impl On {
if let (Some(a), Some(b)) = (self.project(p0, None, exact), self.project(p1, None, exact)) {
candidates.push(line(a, b));
}
// u or v the edge's own parameter, either way along and offset (forms on one frame's
// quarter grid, or on the same grid turned over), the other fixed.
if let Some([u, v]) = self.project(pm, None, exact) {
candidates.push(line([t0, v], [t1, v]));
candidates.push(line([u, t0], [u, t1]));
let tm = 0.5 * (t0 + t1);
for sign in [1.0, -1.0] {
let (cu, cv) = (u - sign * tm, v - sign * tm);
candidates.push(line([cu + sign * t0, v], [cu + sign * t1, v]));
candidates.push(line([u, cv + sign * t0], [u, cv + sign * t1]));
}
}
candidates.into_iter().find(|pc| self.lifts(pc, curve, [t0, t1], exact))
}
+31 -8
View File
@@ -1152,12 +1152,34 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
}
g
};
let forms: Vec<Option<(b::Curve, [f64; 2])>> = edges
// A circle on a cylinder, cone, sphere or torus about its axis is labelled on that face's
// frame, so its form lies on the face's quarter grid and its pcurve there is a straight line.
let mut frame_of: Vec<Option<&b::Frame>> = vec![None; edges.len()];
for f in &faces {
let (b::Surface::Cylinder { frame, .. }
| b::Surface::Cone { frame, .. }
| b::Surface::Sphere { frame, .. }
| b::Surface::Torus { frame, .. }) = &f.surface
else {
continue;
};
for u in f.loops.iter().flatten() {
if let Some(slot) = frame_of.get_mut(u.edge as usize).filter(|s| s.is_none()) {
*slot = Some(frame);
}
}
}
let labels: Vec<(b::Curve, [f64; 2])> = edges
.iter()
.zip(&frame_of)
.map(|(e, frame)| frame.and_then(|f| b::form::on_frame(&e.curve, e.interval, f, tol)).unwrap_or_else(|| (e.curve.clone(), e.interval)))
.collect();
let forms: Vec<Option<(b::Curve, [f64; 2])>> = labels
.iter()
.zip(&given_on)
.map(|(e, given)| match e.curve {
.map(|((curve, interval), given)| match 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)
b::form::curve(curve, *interval).ok_or_else(|| fail("an arc of more than one turn".into())).map(Some)
}
_ => Ok(None),
})
@@ -1300,13 +1322,13 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
}
let record_edges = edges
.iter()
.zip(&forms)
.map(|(e, form)| {
.zip(forms.iter().zip(&labels))
.map(|(e, (form, (label, label_interval)))| {
let (nurbs, interval) = match form {
Some((c, i)) => (Some(c.clone()), *i),
None => (None, e.interval),
None => (None, *label_interval),
};
b::Edge { nurbs, curve: e.curve.clone(), interval, vertices: e.vertices, tolerance: tol, uses: Vec::new() }
b::Edge { nurbs, curve: label.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();
@@ -1424,7 +1446,8 @@ fn face_flux(s: &SurfaceEval, v0: f64, loops: &[Vec<(&Curve2, bool)>], field: &d
let [t0, t1] = c.interval()?;
let at = |k: f64| {
let f = k / FLUX_PIECES as f64;
c.eval(if *forward { t0 + (t1 - t0) * f } else { t1 - (t1 - t0) * f })
// Exactly the ends at f = 0 and 1, whatever the interval's rounding.
c.eval(if *forward { t0 * (1.0 - f) + t1 * f } else { t1 * (1.0 - f) + t0 * f })
};
for k in 0..FLUX_PIECES {
let (a, m, z) = (at(k as f64)?, at(k as f64 + 0.5)?, at(k as f64 + 1.0)?);