From c0ec866fac5d64b9bb4935c50531d20892b187b2 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 18 Jul 2026 00:31:27 +0300 Subject: [PATCH] feat(solid3d): draw the ring outline of torus faces in wireframe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A torus's ISOLINES were only its minor cross-section circles, so a ring read as a row of disconnected circles rather than a ring — the outer and inner profile arcs that make its outline were never drawn. They are the major (ring-direction) isolines: a circle around the ring at each tube angle, the outer (θ=0) and inner (θ=π) ones being the defining outline. Both directions now draw. The revolution is swept in full rather than clipped to the face: a tube's boundary is only its two end caps, clustered at the ends, so it never covers the revolution arc and can't say which way round the tube runs — the same limitation the arc range already had. Full is correct for a closed or nearly-closed ring (this Euro symbol is a ~345° torus) and only over-draws a short torus fillet by the small missing arc, which a later winding-aware extent would tighten. Co-Authored-By: Claude Opus 4.8 --- src/scene/convert/solid3d_tess.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index 32e116b63..1a90ede0c 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -372,10 +372,16 @@ fn torus_isolines(sat: &SatDocument, face: &SatFace, count: usize, out: &mut Vec let v = cross3(axis, u); let major = torus.major_radius(); let minor = torus.minor_radius(); - let poly = collect_face_polygon(sat, face, 48); - let (phi_min, phi_span, full) = torus_phi_range(&poly, [cx, cy, cz], u, v); + // The face boundary of a tube is just its two end caps, clustered at the + // ends — it never covers the revolution arc, so the boundary can't say which + // way round the tube runs (a limitation `torus_phi_range` shares). Draw the + // full revolution: correct for a closed or nearly-closed ring like this one, + // at the cost of over-drawing a short torus fillet, which a later + // winding-aware extent would tighten. + + // Minor (cross-section) circles — constant revolution angle, full tube. const M: usize = 20; - for phi in iso_params(phi_min, phi_span, full, count) { + for phi in iso_params(0.0, TAU, true, count) { for t in 0..M { let t0 = TAU * (t as f64 / M as f64); let t1 = TAU * ((t + 1) as f64 / M as f64); @@ -383,6 +389,22 @@ fn torus_isolines(sat: &SatDocument, face: &SatFace, count: usize, out: &mut Vec out.push(torus_pt(cx, cy, cz, axis, u, v, major, minor, t1, phi)); } } + + // Major (ring-direction) arcs — constant tube angle, swept along the full + // revolution. The outer (θ=0) and inner (θ=π) circles are the torus's + // defining profile — the ring outline; without them it reads as disconnected + // cross-sections. `count.max(2)` guarantees outer + inner even at ISOLINES=1. + const RING_SEGS: usize = 64; + let n_ring = count.max(2); + for k in 0..n_ring { + let theta = TAU * (k as f64 / n_ring as f64); + for s in 0..RING_SEGS { + let p0 = TAU * (s as f64 / RING_SEGS as f64); + let p1 = TAU * ((s + 1) as f64 / RING_SEGS as f64); + out.push(torus_pt(cx, cy, cz, axis, u, v, major, minor, theta, p0)); + out.push(torus_pt(cx, cy, cz, axis, u, v, major, minor, theta, p1)); + } + } } /// Longitude/colatitude span of a sphere face from its boundary polygon.