mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-10 18:55:14 +00:00
solidcore-step: a seam round the axis for a torus face winding round its tube
A torus face bounded by two circles round its tube (an elbow's wall between its two ends) was refused: "a loop winding around a surface more than once or in v". Its loops wind in v, so it gets a seam the other way about: a circle about the axis at a vertex's angle round the tube, running from the loop that winds up the tube towards −u (the face lies to the left of its loops) to the other. band_seam and crossing take the parameter the loops wind in. In STEP Tools' jack_in_the_box 14 bodies that were open shells close (34 -> 48 of 56). Test: a quarter elbow closes with a quarter-circle seam about its axis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+53
-7
@@ -80,6 +80,9 @@ enum Kind {
|
||||
major: f64,
|
||||
minor: f64,
|
||||
},
|
||||
/// A torus face winding round its tube (a band between two circles round the tube, an elbow's
|
||||
/// wall): its seam a circle about the axis, at a vertex's angle round the tube.
|
||||
TorusTube,
|
||||
/// A placed revolution with u the angle: its profile.
|
||||
Revolved(BCurve),
|
||||
/// A B-spline surface closed on itself around u: its boundary column at u = `u0` (the start
|
||||
@@ -321,9 +324,15 @@ impl Builder<'_, '_> {
|
||||
let found = axis(&surface).or_else(|| spline_axis(&surface, &eval));
|
||||
let mut up = Vec::new();
|
||||
let mut down = Vec::new();
|
||||
// On a torus a face may wind round its tube instead (its loops turn in v, not u).
|
||||
let mut tube = false;
|
||||
for (li, l) in face.loops.iter().enumerate() {
|
||||
let LoopB::Edges(es) = l else { continue };
|
||||
let w = self.winding(&eval, es, periods)?;
|
||||
let mut w = self.winding(&eval, es, periods)?;
|
||||
if matches!(surface, BSurface::Torus { .. }) && w[0] == 0 && w[1].abs() == 1 {
|
||||
tube = true;
|
||||
w = [w[1], 0];
|
||||
}
|
||||
if w[1] != 0 || w[0].abs() > 1 {
|
||||
return Err(format!("face #{}: {}", face.step, unsupported("a loop winding around a surface more than once or in v")));
|
||||
}
|
||||
@@ -343,6 +352,7 @@ impl Builder<'_, '_> {
|
||||
continue;
|
||||
}
|
||||
let step = face.step;
|
||||
let found = if tube { found.map(|ax| Axis { kind: Kind::TorusTube, ..ax }) } else { found };
|
||||
let Some(ax) = found else {
|
||||
return Err(format!("face #{step}: {}", unsupported("a face winding around a torus or a spline closed in v without a seam edge")));
|
||||
};
|
||||
@@ -521,6 +531,25 @@ impl Builder<'_, '_> {
|
||||
}
|
||||
(BCurve::Circle { frame: Frame { origin: arr(centre), x_axis: arr(x), y_axis: arr(ax.z) }, radius: *minor }, [ta, ta + up], true)
|
||||
}
|
||||
Kind::TorusTube => {
|
||||
// The circle about the axis through `a`: angle 0 at `a`, growing with the
|
||||
// torus's u. The face lies to the left of its loops in (u, v), so from the loop
|
||||
// running round the tube up (+v) it lies towards −u: the seam runs from `a` back
|
||||
// round the axis to `b`, the edge from `b` to `a`.
|
||||
let da = pa - ax.origin;
|
||||
let h = da.dot(ax.z);
|
||||
let radial = da - ax.z * h;
|
||||
let r = radial.len();
|
||||
let x = radial.normalized().ok_or("a torus seam on its axis")?;
|
||||
let y = ax.z.cross(x);
|
||||
let db = pb - ax.origin;
|
||||
let back = (-(db.dot(y).atan2(db.dot(x)))).rem_euclid(TAU);
|
||||
if !(back > 1e-9) {
|
||||
return Err("a seam of no length".into());
|
||||
}
|
||||
let centre = ax.origin + ax.z * h;
|
||||
(BCurve::Circle { frame: Frame { origin: arr(centre), x_axis: arr(x), y_axis: arr(y) }, radius: r }, [-back, 0.0], false)
|
||||
}
|
||||
Kind::Iso { column, .. } => {
|
||||
// Each vertex's v on the surface (a pole's from its end of the v range).
|
||||
let Some((v0, v1)) = eval.domain().range[1] else { return Err("a closed spline with no v range".into()) };
|
||||
@@ -869,9 +898,11 @@ impl Builder<'_, '_> {
|
||||
|
||||
/// Join loops `la` (running +u) and `lb` (running −u) of face `fi` by a seam.
|
||||
fn band_seam(&mut self, fi: usize, la: usize, lb: usize, ax: &Axis, eval: &SurfaceEval, splits: &mut usize) -> R<()> {
|
||||
// The parameter the loops wind in: u, or v round a torus's tube.
|
||||
let d = usize::from(ax.kind == Kind::TorusTube);
|
||||
let angle = |s: &Self, v: u32| -> R<f64> {
|
||||
let p = s.vpoint(v)?;
|
||||
eval.project(p, None, f64::INFINITY).map(|q| q[0]).ok_or_else(|| "a vertex off its surface".to_string())
|
||||
eval.project(p, None, f64::INFINITY).and_then(|q| q.get(d).copied()).ok_or_else(|| "a vertex off its surface".to_string())
|
||||
};
|
||||
let (Some(LoopB::Edges(a_es)), Some(LoopB::Edges(b_es))) =
|
||||
(self.faces.get(fi).and_then(|f| f.loops.get(la)), self.faces.get(fi).and_then(|f| f.loops.get(lb)))
|
||||
@@ -882,9 +913,16 @@ impl Builder<'_, '_> {
|
||||
// Angles agree within the tolerance at the radius they are at.
|
||||
let close = |s: &Self, va: u32, vb: u32| -> R<bool> {
|
||||
let (ua, ub) = (angle(s, va)?, angle(s, vb)?);
|
||||
let r = {
|
||||
let p = s.vpoint(va)? - ax.origin;
|
||||
(p - ax.z * p.dot(ax.z)).len()
|
||||
let r = match &ax.kind {
|
||||
// Round the tube the angle is the tube's, at the tube's radius at most.
|
||||
Kind::TorusTube => match &s.faces.get(fi).map(|f| &f.surface) {
|
||||
Some(BSurface::Torus { minor_radius, .. }) => *minor_radius,
|
||||
_ => 0.0,
|
||||
},
|
||||
_ => {
|
||||
let p = s.vpoint(va)? - ax.origin;
|
||||
(p - ax.z * p.dot(ax.z)).len()
|
||||
}
|
||||
};
|
||||
Ok(wrap(ua - ub).abs() * r <= s.tol)
|
||||
};
|
||||
@@ -918,7 +956,7 @@ impl Builder<'_, '_> {
|
||||
let target = angle(self, self.start(*a_es.first().ok_or("empty loop")?)?)?;
|
||||
let mut found = None;
|
||||
for &(e, _) in &b_es {
|
||||
if let Some(t) = self.crossing(e, target, ax.period, eval)? {
|
||||
if let Some(t) = self.crossing_in(e, d, target, ax.period, eval)? {
|
||||
found = Some((e, t));
|
||||
break;
|
||||
}
|
||||
@@ -963,10 +1001,18 @@ impl Builder<'_, '_> {
|
||||
/// The parameter inside edge `e`'s interval where its curve reaches the angle `target`
|
||||
/// about the axis, if it does.
|
||||
fn crossing(&self, e: u32, target: f64, period: f64, eval: &SurfaceEval) -> R<Option<f64>> {
|
||||
self.crossing_in(e, 0, target, period, eval)
|
||||
}
|
||||
|
||||
/// [`Self::crossing`] of the surface's parameter `d` (0: u, 1: v).
|
||||
fn crossing_in(&self, e: u32, d: usize, target: f64, period: f64, eval: &SurfaceEval) -> R<Option<f64>> {
|
||||
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
|
||||
let c = CurveEval::new(&edge.curve).map_err(|m| m.to_string())?;
|
||||
let [lo, hi] = edge.interval;
|
||||
let d = |t: f64| c.point(t).and_then(|p| eval.project(p, None, f64::INFINITY)).map(|q| wrap_by(q[0] - target, period));
|
||||
let at = d;
|
||||
let d = |t: f64| {
|
||||
c.point(t).and_then(|p| eval.project(p, None, f64::INFINITY)).and_then(|q| q.get(at).copied()).map(|x| wrap_by(x - target, period))
|
||||
};
|
||||
// A jump across the half period is the wrap, not a crossing.
|
||||
let near = period / TAU;
|
||||
let n = 64;
|
||||
|
||||
@@ -576,6 +576,52 @@ fn a_half_torus_between_its_rims_gets_a_seam_round_the_tube() {
|
||||
});
|
||||
}
|
||||
|
||||
/// A quarter elbow: a torus face winding round its tube between two tube circles (at u = 0 and
|
||||
/// u = π/2), closed by the two disks they bound. The tube face gets a seam round the axis at a
|
||||
/// vertex's angle round the tube, as a band round the axis gets one round the tube.
|
||||
#[test]
|
||||
fn a_quarter_elbow_gets_a_seam_round_the_axis() {
|
||||
let mut w = W::default();
|
||||
// The tube circle at u = 0 (in the plane y = 0, about (20, 0, 0)) and at u = π/2 (x = 0).
|
||||
let ring = |w: &mut W, c: V, n: V, x: V| {
|
||||
let v = w.vertex([c[0] + 5.0 * x[0], c[1] + 5.0 * x[1], c[2] + 5.0 * x[2]]);
|
||||
let k = w.circle(c, n, x, 5.0);
|
||||
w.add(format!("EDGE_CURVE('',#{v},#{v},#{k},.T.)"))
|
||||
};
|
||||
// Each circle runs with the torus's v about its normal −(the tube's direction there).
|
||||
let start = ring(&mut w, [20.0, 0.0, 0.0], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0]);
|
||||
let end = ring(&mut w, [0.0, 20.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
|
||||
let a = w.axis([0.0; 3], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]);
|
||||
let torus = w.add(format!("TOROIDAL_SURFACE('',#{a},20.,5.)"));
|
||||
// The face lies to the left of its loops in (u, v): from u = 0 it runs −v, from π/2 +v.
|
||||
let (x, y) = (w.oe(start, false), w.oe(end, true));
|
||||
let (bx, by) = (w.bound(&[x], true, true), w.bound(&[y], false, true));
|
||||
let tube = w.face(&[bx, by], torus, true);
|
||||
// The end disks, facing out of the elbow: −y at its start, −x at its end.
|
||||
let p0 = w.plane([20.0, 0.0, 0.0], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0]);
|
||||
let x = w.oe(start, true);
|
||||
let bx = w.bound(&[x], true, true);
|
||||
let d0 = w.face(&[bx], p0, true);
|
||||
let p1 = w.plane([0.0, 20.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
|
||||
let x = w.oe(end, false);
|
||||
let bx = w.bound(&[x], true, true);
|
||||
let d1 = w.face(&[bx], p1, true);
|
||||
let shell = closed_shell(&mut w, &[tube, d0, d1]);
|
||||
let solid = w.add(format!("MANIFOLD_SOLID_BREP('elbow',#{shell})"));
|
||||
let r = read_solids(&w.file(&[solid]));
|
||||
assert!(r.failures.is_empty(), "{:?}", r.failures);
|
||||
let s = r.solids.into_iter().next().unwrap();
|
||||
solidcore_ops::record_check::validate(&s.brep).unwrap();
|
||||
solidcore_ops::record_check::agreement(&s.brep).unwrap();
|
||||
assert!(s.notes.iter().any(|n| n.contains("1 seam edges added")), "{:?}", s.notes);
|
||||
// The seam: a circle about the axis (centred on it), a quarter turn long.
|
||||
let seam = s.brep.edges.iter().find(
|
||||
|e| matches!(&e.curve, solidcore_record::brep::Curve::Circle { frame, .. } if frame.origin[0].abs() < 1e-9 && frame.origin[1].abs() < 1e-9),
|
||||
);
|
||||
let [t0, t1] = solidcore_ops::label::edge_curve_interval(seam.expect("a seam about the axis")).unwrap();
|
||||
assert!((t1 - t0 - std::f64::consts::FRAC_PI_2).abs() < 1e-9, "{t0} {t1}");
|
||||
}
|
||||
|
||||
/// A ball whose one face is bounded by two vertex loops, at its poles: the whole sphere, closed
|
||||
/// by a meridian seam from pole to pole.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user