diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 37db81855..ecb0e3eb8 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -1108,6 +1108,161 @@ fn extract_sub_polyline(poly: &LwPolyline, s0: f64, s1: f64) -> Option Option<(Curve, bool)> { + let n = poly.vertices.len(); + if n < 2 { + return None; + } + + let a = &poly.vertices[segment % n]; + let b = &poly.vertices[(segment + 1) % n]; + + let p0 = [a.location.x, a.location.y]; + let p1 = [b.location.x, b.location.y]; + + if (p1[0] - p0[0]).hypot(p1[1] - p0[1]) < 1e-12 { + return None; + } + + if let Some(arc) = BulgeArc::from_bulge(p0, p1, a.bulge) { + if arc.sweep >= 0.0 { + Some(( + Curve::Arc(KernelArc { + centre: arc.center, + radius: arc.radius, + start_angle: arc.start_angle, + end_angle: arc.start_angle + arc.sweep, + }), + false, + )) + } else { + Some(( + Curve::Arc(KernelArc { + centre: arc.center, + radius: arc.radius, + start_angle: arc.end_angle, + end_angle: arc.end_angle - arc.sweep, + }), + true, + )) + } + } else { + Some(( + Curve::Line(KernelLine { + start: p0, + end: p1, + }), + false, + )) + } +} + +/// Global polyline parameters at which non-adjacent segments of the same +/// polyline intersect. +/// +/// Segment neighbours are deliberately excluded: their common vertex is a +/// normal polyline vertex, not a self-intersection. +fn lwpoly_self_cut_params(poly: &LwPolyline) -> Vec { + let n = poly.vertices.len(); + if n < 3 { + return Vec::new(); + } + + let closed = poly.is_closed; + let segment_count = if closed { n } else { n - 1 }; + let total = segment_count as f64; + + let mut cuts = Vec::new(); + + for i in 0..segment_count { + let Some((curve_a, reverse_a)) = lwpoly_segment_curve(poly, i) else { + continue; + }; + + for j in (i + 1)..segment_count { + // Ordinary neighbouring segments meet at their common vertex. + let adjacent = j == i + 1 + || (closed && i == 0 && j + 1 == segment_count); + + if adjacent { + continue; + } + + let Some((curve_b, reverse_b)) = lwpoly_segment_curve(poly, j) else { + continue; + }; + + let hits = kernel_intersect( + &curve_a, + &curve_b, + KernelTolerance::new(CUT_TOLERANCE), + ); + + for hit in hits { + let mut ua = hit.t_a; + let mut ub = hit.t_b; + + if reverse_a { + ua = 1.0 - ua; + } + if reverse_b { + ub = 1.0 - ub; + } + + if !ua.is_finite() + || !ub.is_finite() + || ua < -1e-7 + || ua > 1.0 + 1e-7 + || ub < -1e-7 + || ub > 1.0 + 1e-7 + { + continue; + } + + ua = ua.clamp(0.0, 1.0); + ub = ub.clamp(0.0, 1.0); + + // If both sides only touch at existing vertices, those + // vertices already act as trim limits below. + let a_endpoint = ua <= 1e-7 || ua >= 1.0 - 1e-7; + let b_endpoint = ub <= 1e-7 || ub >= 1.0 - 1e-7; + + if a_endpoint && b_endpoint { + continue; + } + + let pa = i as f64 + ua; + let pb = j as f64 + ub; + + cuts.push(if closed { + pa.rem_euclid(total) + } else { + pa + }); + + cuts.push(if closed { + pb.rem_euclid(total) + } else { + pb + }); + } + } + } + + cuts.sort_by(f64::total_cmp); + cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-6); + cuts +} + /// Trim a clicked LwPolyline: remove the portion containing the click, bounded /// by the nearest boundary intersections on each side. A closed polyline needs /// ≥2 cuts and becomes an open polyline (the surviving arc); an open one yields @@ -1132,22 +1287,46 @@ fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option< }; // Boundary cuts as global params (segment index + local u). + // External boundary cuts as global params (segment index + local u). let mut cuts: Vec = Vec::new(); + for i in 0..seg_count { let p0 = vx(i); let p1 = vx(i + 1); let b = seg_bulge(i); + for u in polyline_seg_ts(p0, p1, b, handle, geos) { let param = i as f64 + u.clamp(0.0, 1.0); - cuts.push(if closed { param.rem_euclid(total) } else { param }); + + cuts.push(if closed { + param.rem_euclid(total) + } else { + param + }); } } - cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); - cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-6); - if cuts.is_empty() { - return None; + + // A polyline can also cut itself. These are intersections between + // non-adjacent segments belonging to this same LwPolyline. + cuts.extend(lwpoly_self_cut_params(poly)); + + // Every polyline vertex is also a valid trim boundary. This allows an entire + // segment between two vertices to be removed even when no external entity + // crosses that particular segment. + if closed { + for i in 0..seg_count { + cuts.push(i as f64); + } + } else { + // 0 and `total` are added explicitly below as the open polyline ends. + for i in 1..seg_count { + cuts.push(i as f64); + } } + cuts.sort_by(f64::total_cmp); + cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-6); + // Click param: nearest point on the polyline. let mut best = (f64::INFINITY, 0.0_f64); for i in 0..seg_count { @@ -1251,11 +1430,7 @@ fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option< } } - if out.is_empty() { - None - } else { - Some(out) - } + Some(out) } // ── Extend helpers ──────────────────────────────────────────────────────── @@ -1650,6 +1825,11 @@ fn crossing_trim_lwpolyline( }; let mut removed = Vec::<(f64, f64)>::new(); + + // Compute the polyline's own non-adjacent segment intersections once. + // These global parameters will later be mapped back into each source segment. + let self_cuts = lwpoly_self_cut_params(poly); + for i in 0..seg_count { let a = vertex_xy(i); let b = vertex_xy(i + 1); @@ -1678,42 +1858,55 @@ fn crossing_trim_lwpolyline( let Some((inside_lo, inside_hi)) = window_range else { continue; }; - let cuts = polyline_seg_ts(a, b, bulge, handle, geos); - if cuts.is_empty() { - continue; + // External intersections on this segment. + // External intersections on this segment. + let mut cuts = polyline_seg_ts(a, b, bulge, handle, geos); + + // Also include self-intersections belonging to this source segment. + // `lwpoly_self_cut_params()` stores them as global polyline parameters: + // segment_index + local_parameter + // + // Convert the ones belonging to segment `i` back to local 0..1. + let seg_start = i as f64; + let seg_end = seg_start + 1.0; + + for global_t in self_cuts.iter().copied() { + if global_t > seg_start + 1e-6 + && global_t < seg_end - 1e-6 + { + cuts.push(global_t - seg_start); + } } - let pick_t = if let Some(ba) = BulgeArc::from_bulge(a, b, bulge) { - let angle = (window.pick[1] - ba.center[1]).atan2(window.pick[0] - ba.center[0]); - let travelled = if ba.sweep >= 0.0 { - (angle - ba.start_angle).rem_euclid(TAU) - } else { - -((ba.start_angle - angle).rem_euclid(TAU)) - }; - (travelled / ba.sweep).clamp(inside_lo, inside_hi) - } else { - let dx = b[0] - a[0]; - let dy = b[1] - a[1]; - let len2 = dx * dx + dy * dy; - let projected = if len2 > 1e-12 { - ((window.pick[0] - a[0]) * dx + (window.pick[1] - a[1]) * dy) / len2 - } else { - (inside_lo + inside_hi) * 0.5 - }; - projected.clamp(inside_lo, inside_hi) - }; + // The source segment endpoints are also valid trim limits. + cuts.push(0.0); + cuts.push(1.0); - let mut bounds = vec![0.0]; - bounds.extend(cuts); - bounds.push(1.0); - bounds.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); - bounds.dedup_by(|a, b| (*a - *b).abs() < 1e-6); - if let Some(span) = bounds - .windows(2) - .find(|span| pick_t >= span[0] - 1e-6 && pick_t <= span[1] + 1e-6) - { - if span[1] - span[0] > 1e-6 { - removed.push((i as f64 + span[0], i as f64 + span[1])); + cuts.sort_by(f64::total_cmp); + cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-6); + + // Remove every trim span that is actually touched by the selection box. + // + // A single source segment may contain several sub-spans when external + // boundaries cross it. Only the sub-spans overlapping the window are removed. + for span in cuts.windows(2) { + let lo = span[0].clamp(0.0, 1.0); + let hi = span[1].clamp(0.0, 1.0); + + if hi - lo <= 1e-6 { + continue; + } + + // This trim span participates when its parameter interval overlaps the + // portion of the source segment lying inside the crossing rectangle. + let overlaps_window = + hi >= inside_lo - 1e-6 && lo <= inside_hi + 1e-6; + + if overlaps_window { + removed.push(( + i as f64 + lo, + i as f64 + hi, + )); } } } diff --git a/src/snap.rs b/src/snap.rs index 53bc6a779..39917f1a6 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -1755,6 +1755,53 @@ impl Snapper { if self.is_on(SnapType::Intersection) && (local_segments.is_some() || allow_unindexed_pairwise) { + // Unindexed fallback: also test non-adjacent segments belonging to the + // same wire. A polyline is one entity but its own segments may cross. + if local_segments.is_none() { + for wire in in_range_wires.iter() { + let src = wire_source(wire); + let segment_count = wire.points.len().saturating_sub(1); + + for ai in 0..segment_count { + let a0 = wp_f64(wire, ai); + let a1 = wp_f64(wire, ai + 1); + + let a_min_x = a0.x.min(a1.x); + let a_max_x = a0.x.max(a1.x); + let a_min_y = a0.y.min(a1.y); + let a_max_y = a0.y.max(a1.y); + + for bi in (ai + 1)..segment_count { + let b0 = wp_f64(wire, bi); + let b1 = wp_f64(wire, bi + 1); + + // Consecutive polyline segments meet at a normal vertex. + // That is already an Endpoint snap, not a self-intersection. + if segments_share_endpoint(a0, a1, b0, b1) { + continue; + } + + // Cheap AABB rejection before the exact 3-D intersection. + if a_max_x < b0.x.min(b1.x) + || a_min_x > b0.x.max(b1.x) + || a_max_y < b0.y.min(b1.y) + || a_min_y > b0.y.max(b1.y) + { + continue; + } + + if let Some(pt) = seg_intersect_3d(a0, a1, b0, b1) { + try_pt( + pt, + SnapType::Intersection, + src, + None, + ); + } + } + } + } + } if let Some(segments) = &local_segments { let local_wires: Vec<_> = wires.iter().filter(|wire| wire_in_range(wire)).collect(); let mut resolved_pairs = rustc_hash::FxHashSet::default(); @@ -1789,7 +1836,13 @@ impl Snapper { if b.min_x() > a.max_x() { break; } - if a.wire == b.wire || a.max_y() < b.min_y() || a.min_y() > b.max_y() { + let same_wire_neighbours = a.wire == b.wire + && segments_share_endpoint(a.a, a.b, b.a, b.b); + + if same_wire_neighbours + || a.max_y() < b.min_y() + || a.min_y() > b.max_y() + { continue; } if wires.source_wire(a.wire).zip(wires.source_wire(b.wire)) @@ -1801,11 +1854,23 @@ impl Snapper { world_to_screen(pt, view_rot, eye, bounds), cursor_screen, ) <= f32::EPSILON; + let primary = wires + .source_wire(a.wire) + .and_then(wire_source); + + let secondary = if a.wire == b.wire { + None + } else { + wires + .source_wire(b.wire) + .and_then(wire_source) + }; + try_pt( pt, SnapType::Intersection, - wires.source_wire(a.wire).and_then(wire_source), - wires.source_wire(b.wire).and_then(wire_source), + primary, + secondary, ); if exact_cursor { break 'intersection_sweep; @@ -3151,6 +3216,24 @@ pub(crate) fn exact_curve_intersections( Some(points) } +/// Two segments of the same polyline that merely meet at a shared endpoint +/// are neighbours, not a self-intersection. +/// +/// Non-adjacent segments are allowed to intersect even when they belong to +/// the same WireModel. +fn segments_share_endpoint( + a0: DVec3, + a1: DVec3, + b0: DVec3, + b1: DVec3, +) -> bool { + const EPS2: f64 = 1.0e-18; + + a0.distance_squared(b0) <= EPS2 + || a0.distance_squared(b1) <= EPS2 + || a1.distance_squared(b0) <= EPS2 + || a1.distance_squared(b1) <= EPS2 +} /// XY-plane segment-segment intersection. Returns `None` if parallel or outside. /// True 3D intersection of two segments: the point where their plan (XY) /// projections cross **and** both segments are at the same height there. Returns