From 2c5b7e76fb820b7f6d46d2c64a226ef0952da600 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 3 Aug 2026 19:45:15 +0300 Subject: [PATCH] fix(acis): complete curved solid faces Prevent partial sweeps and reversed boundary sampling from dropping surface segments and connector corners. --- src/scene/convert/acis_to_truck.rs | 9 ++++++--- src/scene/convert/solid3d_tess.rs | 27 +++++++++++++++++---------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/scene/convert/acis_to_truck.rs b/src/scene/convert/acis_to_truck.rs index fe1e28c1f..3e7f85c3d 100644 --- a/src/scene/convert/acis_to_truck.rs +++ b/src/scene/convert/acis_to_truck.rs @@ -374,9 +374,9 @@ fn loop_normal(pts: &[[f64; 3]]) -> [f64; 3] { // ── Cone / cylinder face ───────────────────────────────────────────────────── -/// Build a cone/cylinder lateral surface as revolution faces. A plain closed -/// revolution uses the parametric sampler; a pierced one stays here so its -/// additional boundary loops can trim holes into the curved surface. +/// Build a pierced cone/cylinder lateral surface as revolution faces. A +/// single-loop face uses the parametric sampler, avoiding partial revolution +/// segments that can disappear when the profile crosses a coordinate plane. fn cone_faces( sat: &SatDocument, face: &SatFace, @@ -411,6 +411,9 @@ fn cone_faces( }; let loops = collect_face_loops(sat, face, BOUNDARY_CHORD_FRAC); + if loops.len() == 1 { + return None; + } let arcs: Vec<(f64, f64)> = loops .iter() .map(|lp| cone_boundary_arc(lp, [cx, cy, cz], axis, udir, vdir)) diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index b37eb7b44..947e27e50 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -1424,17 +1424,23 @@ pub(crate) fn append_coedge_points( // The edge's own sense (relative to its curve) decides the ellipse // winding; a reversed edge samples the opposite handedness. let reversed = matches!(edge.sense(), Sense::Reversed); - let mut sampled = sample_ellipse_arc( + // Orient the parameter range before sampling. Reversing an + // endpoint-exclusive sample afterwards would drop the traversal + // start and retain its end, duplicating the next coedge's point and + // cutting the connector corner out of the face boundary. + let (start, end) = if fwd { + (edge.start_param(), edge.end_param()) + } else { + (edge.end_param(), edge.start_param()) + }; + let sampled = sample_ellipse_arc( &ellipse, - edge.start_param(), - edge.end_param(), + start, + end, chord_frac, reversed, ); if !sampled.is_empty() { - if !fwd { - sampled.reverse(); - } pts.extend(sampled); return; } @@ -1452,12 +1458,13 @@ pub(crate) fn append_coedge_points( .into_iter() .map(|(x, y, z)| [x, y, z]) .collect(); - // Drop the shared end point so adjacent coedges don't double up. + // Orient the inclusive sample first, then drop the traversal end so + // the adjacent coedge contributes that shared point. + if !fwd { + sampled.reverse(); + } sampled.pop(); if sampled.len() >= 2 { - if !fwd { - sampled.reverse(); - } pts.extend(sampled); return; }