fix(solid3d): trim sphere caps and pierced cone legs

Sphere faces drew as complete balls. ACIS parametrises a sphere about
any pole, so a vessel's dished head is commonly bounded by a circle that
is no line of latitude in that frame, and a theta/phi bounding box of
such a boundary covers nearly the whole ball. Derive the window
analytically instead: when a face's loops are circles lying on the
sphere, re-pole the frame onto their common axis, where each seam is an
exact latitude at acos(d / R). One ring bounds a cap, two bound a zone;
the material side comes from the boundary winding, reusing the helper
the torus path already had.

Separately, the longitude wrap flag was resetting the colatitude window
to the whole meridian. A cap centred on the pole covers every longitude
while still ending at its seam, so that reset grew every cap back into a
ball. Trim the two angles independently and rename the flag theta_full
so the split reads at each use, including the silhouette generator,
which skipped the colatitude test the same way.

Cone faces took their height span from the first boundary loop alone. A
lateral face closed off by one rim and one pierce curve -- a pipe branch
meeting its run -- carries its two ends on different loops, so the tube
was sized to whichever came first and the leg above the junction went
undrawn. Read every loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5 committed 2026-08-06 21:44:49 +03:00
1 parent 8f009d0058
commit 01d8b87db1
2 files changed
+244 -43

No files matched your search

+11 -5
View File
@@ -2390,14 +2390,20 @@ impl Pipeline {
let a = std::f64::consts::TAU * (i as f64 / N as f64);
let dir = e1 * a.cos() + e2 * a.sin();
// Keep only the arc that lies on the actual face.
let on_face = *full || {
let phi = dir.dot(pole).clamp(-1.0, 1.0).acos();
// `full` is a *longitude* wrap flag: a dish cap sits
// on the pole and so covers every longitude while
// still ending at its seam, so the colatitude test
// always applies. A whole ball reports phi 0..π and
// passes it regardless.
let phi = dir.dot(pole).clamp(-1.0, 1.0).acos();
let in_phi =
phi >= *phi_min as f64 && phi <= *phi_max as f64;
let in_theta = *full || {
let th = dir.dot(v).atan2(dir.dot(u));
let toff = (th - *theta_min as f64).rem_euclid(std::f64::consts::TAU);
phi >= *phi_min as f64
&& phi <= *phi_max as f64
&& (toff <= *theta_span as f64)
toff <= *theta_span as f64
};
let on_face = in_phi && in_theta;
let p = if on_face { Some(c + dir * r) } else { None };
if let (Some(a), Some(b)) = (prev, p) {
verts.push(mk(a));