From a60b52a462229372ab7a865c0c84cd2117669b83 Mon Sep 17 00:00:00 2001 From: Afonsop Date: Thu, 17 Sep 2026 13:01:01 +0100 Subject: [PATCH] fix(trim): don't panic on degenerate polyline segments (#1318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sample_entity_xy stitches exploded segment samples with pts[sp[1..]] guarded only by last()==first(); a segment that samples to EMPTY (zero-length edge, or an edge-on-OCS arc whose entity_curve_xy returns None) while the accumulator is still empty made None==None true and sliced a length-0 vec — 'range start index 1 out of range' — and on web the unwind then tripped winit's held RefCell borrow. Guard both stitching sites (the EXTRIM preview sibling had the same pattern) and pin the join semantics plus the degenerate cases with tests. --- src/modules/draw/modify/trim.rs | 90 ++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index b18573986..6cd0ae3a2 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3810,6 +3810,84 @@ mod tests { other => panic!("expected LwPolyline, got {other:?}"), } } + + /// #1318: a segment that samples to nothing — here a zero-length edge, + /// which has no curve — must contribute no points instead of panicking on + /// the `sp[1..]` join when the accumulator is empty too. + #[test] + fn sample_entity_xy_skips_degenerate_segments() { + use acadrust::entities::Polyline3D; + // First edge (0,0,0)→(0,0,0) is zero-length; the second runs to (5,0,0). + let p3d = Polyline3D::from_points(vec![ + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(5.0, 0.0, 0.0), + ]); + let pts = sample_entity_xy(&EntityType::Polyline3D(p3d)); + assert!(!pts.is_empty(), "the real segment still samples"); + assert!(pts[0][0].abs() < 1e-9 && pts[0][1].abs() < 1e-9, "starts at the shared vertex"); + assert!( + (pts[pts.len() - 1][0] - 5.0).abs() < 1e-9 && pts[pts.len() - 1][1].abs() < 1e-9, + "ends at the far vertex" + ); + + // All-degenerate: no segment has a curve — nothing to sample, no panic. + let degenerate = Polyline3D::from_points(vec![ + Vector3::new(1.0, 1.0, 1.0), + Vector3::new(1.0, 1.0, 1.0), + ]); + assert!(sample_entity_xy(&EntityType::Polyline3D(degenerate)).is_empty()); + } + + /// #1318 in the preview sampler: an arc on an edge-on OCS has no plan + /// view, so it samples empty — the same join must skip it rather than + /// slice an empty vec. + #[test] + fn preview_sample_xy_skips_unsampleable_segments() { + use acadrust::entities::{Polyline2D, Vertex2D}; + let mut p2d = Polyline2D::new(); + p2d.normal = Vector3::new(0.0, 1.0, 0.0); // edge-on: no plan view + let mut v0 = Vertex2D::new(Vector3::new(0.0, 0.0, 0.0)); + v0.bulge = 1.0; // first segment is an arc — it samples to nothing + p2d.vertices = vec![ + v0, + Vertex2D::new(Vector3::new(2.0, 0.0, 0.0)), + Vertex2D::new(Vector3::new(4.0, 0.0, 0.0)), + ]; + let pts = preview_sample_xy(&EntityType::Polyline2D(p2d)); + assert!(pts.len() >= 2, "the straight segment still samples"); + // Whatever the OCS maps onto world XY, these vertices stay on y = 0. + for p in &pts { + assert!(p[1].abs() < 1e-9, "sampled off the segment: {p:?}"); + } + } + + /// Healthy input still stitches: the vertex shared by two segments is + /// sampled once, not twice — the dedup the `sp[1..]` join exists for. + #[test] + fn polyline_samples_stitch_shared_vertex_once() { + let mut poly = LwPolyline::new(); + poly.common.handle = Handle::new(95); + poly.vertices = vec![ + LwVertex::from_coords(0.0, 0.0), + LwVertex::from_coords(3.0, 0.0), + LwVertex::from_coords(3.0, 4.0), + ]; + let pts = sample_entity_xy(&EntityType::LwPolyline(poly)); + assert!(pts.len() >= 2); + assert!(pts[0][0].abs() < 1e-9 && pts[0][1].abs() < 1e-9); + assert!( + (pts[pts.len() - 1][0] - 3.0).abs() < 1e-9 + && (pts[pts.len() - 1][1] - 4.0).abs() < 1e-9 + ); + for w in pts.windows(2) { + assert!( + (w[1][0] - w[0][0]).hypot(w[1][1] - w[0][1]) > 1e-9, + "duplicated consecutive sample at {:?}", + w[0] + ); + } + } } // ══════════════════════════════════════════════════════════════════════════ @@ -3978,7 +4056,11 @@ fn sample_entity_xy(e: &EntityType) -> Vec<[f64; 2]> { let mut pts: Vec<[f64; 2]> = Vec::new(); for seg in crate::modules::draw::modify::explode::explode_polyline_segments(e) { let sp = sample_entity_xy(&seg); - if pts.last() == sp.first() { + // A segment can sample to nothing — a zero-length edge has no + // curve, and one on an edge-on OCS has no plan view. It joins + // onto nothing either way; slicing `sp[1..]` when both sides are + // empty is the #1318 panic. + if !sp.is_empty() && pts.last() == sp.first() { pts.extend_from_slice(&sp[1..]); } else { pts.extend(sp); @@ -4030,7 +4112,11 @@ fn preview_sample_xy(e: &EntityType) -> Vec<[f64; 2]> { } _ => sample_entity_xy(&seg), }; - if pts.last() == sp.first() { + // As in `sample_entity_xy`: an empty sample (an arc on an + // edge-on OCS) contributes nothing and must not reach the + // `sp[1..]` join — that is the #1318 panic when `pts` is + // still empty too. + if !sp.is_empty() && pts.last() == sp.first() { pts.extend_from_slice(&sp[1..]); } else { pts.extend(sp);