From f3c2e061e051298eeb067ab4fe34b53021b9b267 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 9 Aug 2026 10:02:23 +0300 Subject: [PATCH] refactor(trim): find an extension's boundary with the kernel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EXTEND asked where a line reaches by matching over every Geo variant and running its own line-line, line-circle and line-ellipse test — twice over, once for a LINE and once for a polyline's terminal segment, about two hundred and sixty lines between them. A boundary spline was walked as sixty-four chords there rather than intersected, so an extend landed on the chord nearest the spline instead of on the spline. Both are one call now: shoot the segment's own infinite line and take the nearest crossing on the side being extended. That is `cut_params`, which every other cut in the file already goes through. With those two gone, the sampled copy carried beside each boundary spline had no remaining reader, so a spline boundary is now only ever the exact NURBS. The line-ellipse adapter lost its last caller with it. Co-Authored-By: Claude Opus 5 --- src/modules/draw/modify/geom.rs | 31 +-- src/modules/draw/modify/trim.rs | 343 +++++--------------------------- 2 files changed, 50 insertions(+), 324 deletions(-) diff --git a/src/modules/draw/modify/geom.rs b/src/modules/draw/modify/geom.rs index 64569cc05..f4cf3a95f 100644 --- a/src/modules/draw/modify/geom.rs +++ b/src/modules/draw/modify/geom.rs @@ -15,7 +15,7 @@ use acadrust::kernel::geom2d::{self, Ellipse}; /// Re-exported unchanged: these already speak in plain `f64`, so there is no /// call-shape difference for this module to absorb. -pub use acadrust::kernel::geom2d::{angle_within_arc, arc_parameter, lerp, normalize_angle}; +pub use acadrust::kernel::geom2d::{arc_parameter, lerp, normalize_angle}; /// Preview geometry keeps the density the commands have always used. const SEGMENTS_PER_RADIAN: f64 = geom2d::DEFAULT_SEGMENTS_PER_RADIAN; @@ -51,35 +51,6 @@ pub fn line_circle(px: f64, py: f64, dx: f64, dy: f64, cx: f64, cy: f64, r: f64) geom2d::line_circle([px, py], [dx, dy], [cx, cy], r) } -/// `(s_on_line, t_on_ellipse)` pairs where a line meets an ellipse. -/// -/// `nx, ny` is the unit major axis. -#[allow(clippy::too_many_arguments)] -pub fn line_ellipse( - px: f64, - py: f64, - dx: f64, - dy: f64, - cx: f64, - cy: f64, - a: f64, - b: f64, - nx: f64, - ny: f64, -) -> Vec<(f64, f64)> { - geom2d::line_ellipse( - [px, py], - [dx, dy], - &Ellipse { - centre: [cx, cy], - major_radius: a, - minor_radius: b, - major_axis: [nx, ny], - }, - ) -} - -/// The two endpoints of a straight segment, as render vertices. pub fn line_points(start: [f64; 3], end: [f64; 3]) -> Vec<[f32; 3]> { narrow(vec![start, end]) } diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index ccf16c37f..cbca3a13d 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -14,9 +14,8 @@ use std::f64::consts::TAU; // its call shapes to the loose scalars and f32 render vertices used here. use super::geom; use super::geom::{ - angle_within_arc as in_arc, arc_parameter as arc_t, arc_points as arc_pts, - ellipse_points as ellipse_pts, lerp as lerp2, - line_circle as lc, line_ellipse as le, line_line as ll, normalize_angle as norm, + arc_parameter as arc_t, arc_points as arc_pts, ellipse_points as ellipse_pts, + lerp as lerp2, normalize_angle as norm, }; use crate::modules::draw::fence::{crossing_box_preview, FencePick}; @@ -131,14 +130,9 @@ enum Geo { t1: f64, // end parameter (may be > 2π if wrapped) }, /// A NURBS boundary, carried exactly. - /// - /// `segs` is a sampling of the same curve, kept only for the two - /// hand-rolled ray walks in the polyline-extend paths. Every crossing - /// that decides a trim uses `curve`. Spline { handle: Handle, curve: Box, - segs: Vec<([f64; 2], [f64; 2])>, }, } @@ -250,17 +244,10 @@ fn geo_from_entity(h: Handle, e: &EntityType) -> Option { t1, }) } - Curve::Nurbs(curve) => { - // Carried exactly. The sampled polyline beside it is only for the - // two hand-rolled ray walks in the polyline-extend paths; every - // crossing that decides a trim goes through the curve itself. - let sampled = curve.tessellate(16); - (sampled.len() >= 2).then(|| Geo::Spline { - handle: h, - curve: Box::new(curve), - segs: sampled.windows(2).map(|w| (w[0], w[1])).collect(), - }) - } + Curve::Nurbs(curve) => Some(Geo::Spline { + handle: h, + curve: Box::new(curve), + }), // A polyline reaches here already taken apart into its segments. Curve::Polyline(_) => None, } @@ -987,6 +974,47 @@ fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option< /// Extend the first or last segment of an LwPolyline to the nearest boundary. /// Click point (DXF XY) determines which end to extend. +/// The nearest boundary crossing past one end of a segment. +/// +/// `t` runs on the infinite line through `from` → `to`: zero at `from`, one +/// at `to`. Extending the far end wants the smallest `t` above one; extending +/// the near end wants the largest `t` below zero. `None` when nothing lies +/// that way. +/// +/// The crossings come from `cut_params`, which is the same kernel call every +/// other cut in this file uses. What was here instead was a match over every +/// `Geo` variant with its own line-line, line-circle and line-ellipse test — +/// twice, once for a line and once for a polyline's terminal segment — and a +/// boundary spline was walked as sixty-four chords rather than intersected. +fn extension_hit( + from: [f64; 2], + to: [f64; 2], + handle: Handle, + geos: &[Geo], + beyond_end: bool, +) -> Option { + let direction = [to[0] - from[0], to[1] - from[1]]; + if direction[0].hypot(direction[1]) < 1e-9 { + return None; + } + // An infinite line rather than the segment: the crossing being looked for + // is by definition off the end of the drawn part. + let ray = Curve::XLine(KernelXLine { + base: from, + direction, + }); + let hits = cut_params(&ray, handle, geos); + if beyond_end { + hits.into_iter() + .filter(|t| *t > 1.0 + 1e-6) + .min_by(f64::total_cmp) + } else { + hits.into_iter() + .filter(|t| *t < -1e-6) + .max_by(f64::total_cmp) + } +} + fn extend_lwpoly( poly: &LwPolyline, click_x: f64, @@ -1030,124 +1058,7 @@ fn extend_lwpoly( return None; } - // t_click on the segment: 0 = ax/ay, 1 = bx/by. We're extending beyond t=1. - let target = poly.common.handle; - let mut best_t = f64::INFINITY; - - for geo in geos { - match geo { - Geo::Line { handle, p1, p2 } => { - if *handle == target { - continue; - } - let (ex, ey) = (p2[0] - p1[0], p2[1] - p1[1]); - if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) { - if (-1e-9..=1.0 + 1e-9).contains(&u) && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - } - } - Geo::Arc { - handle, - cx, - cy, - r, - a0, - a1, - } => { - if *handle == target { - continue; - } - for t in lc(ax, ay, dx, dy, *cx, *cy, *r) { - let ix = ax + t * dx; - let iy = ay + t * dy; - if in_arc((iy - cy).atan2(ix - cx), *a0, *a1) && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - } - } - Geo::Circle { handle, cx, cy, r } => { - if *handle == target { - continue; - } - for t in lc(ax, ay, dx, dy, *cx, *cy, *r) { - if t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - } - } - Geo::Ray { - handle, - bx: rbx, - by: rby, - dx: rdx, - dy: rdy, - } => { - if *handle == target { - continue; - } - if let Some((t, u)) = ll(ax, ay, dx, dy, *rbx, *rby, *rdx, *rdy) { - if u >= -1e-9 && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - } - } - Geo::InfLine { - handle, - bx: ibx, - by: iby, - dx: idx, - dy: idy, - } => { - if *handle == target { - continue; - } - if let Some((t, _)) = ll(ax, ay, dx, dy, *ibx, *iby, *idx, *idy) { - if t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - } - } - Geo::Ellipse { - handle, - cx, - cy, - a, - b, - nx, - ny, - t0, - t1, - } => { - if *handle == target { - continue; - } - for (t, t_ell) in le(ax, ay, dx, dy, *cx, *cy, *a, *b, *nx, *ny) { - if in_arc(t_ell, *t0, *t1) && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - } - } - Geo::Spline { handle, segs, .. } => { - if *handle == target { - continue; - } - for (p1, p2) in segs { - let ex = p2[0] - p1[0]; - let ey = p2[1] - p1[1]; - if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) { - if (-1e-9..=1.0 + 1e-9).contains(&u) && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - } - } - } - } - } - - if !best_t.is_finite() { - return None; - } + let best_t = extension_hit([ax, ay], [bx, by], poly.common.handle, geos, true)?; let new_x = ax + best_t * dx; let new_y = ay + best_t * dy; @@ -1173,165 +1084,9 @@ fn extend_line(orig: &LineEnt, t_click: f64, geos: &[Geo]) -> Option let bx = orig.end.x; let by = orig.end.y; let (dx, dy) = (bx - ax, by - ay); - let target = orig.common.handle; let extend_end = t_click >= 0.5; + let best_t = extension_hit([ax, ay], [bx, by], orig.common.handle, geos, extend_end)?; - let mut best_t = if extend_end { - f64::INFINITY - } else { - f64::NEG_INFINITY - }; - - for geo in geos { - match geo { - Geo::Line { handle, p1, p2 } => { - if *handle == target { - continue; - } - let (ex, ey) = (p2[0] - p1[0], p2[1] - p1[1]); - if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) { - if !(-1e-9..=1.0 + 1e-9).contains(&u) { - continue; - } - if extend_end && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - if !extend_end && t < -1e-6 && t > best_t { - best_t = t; - } - } - } - Geo::Arc { - handle, - cx, - cy, - r, - a0, - a1, - } => { - if *handle == target { - continue; - } - for t in lc(ax, ay, dx, dy, *cx, *cy, *r) { - let ix = ax + t * dx; - let iy = ay + t * dy; - if !in_arc((iy - cy).atan2(ix - cx), *a0, *a1) { - continue; - } - if extend_end && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - if !extend_end && t < -1e-6 && t > best_t { - best_t = t; - } - } - } - Geo::Circle { handle, cx, cy, r } => { - if *handle == target { - continue; - } - for t in lc(ax, ay, dx, dy, *cx, *cy, *r) { - if extend_end && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - if !extend_end && t < -1e-6 && t > best_t { - best_t = t; - } - } - } - Geo::Ray { - handle, - bx: rbx, - by: rby, - dx: rdx, - dy: rdy, - } => { - if *handle == target { - continue; - } - if let Some((t, u)) = ll(ax, ay, dx, dy, *rbx, *rby, *rdx, *rdy) { - if u >= -1e-9 { - // only forward along the Ray - if extend_end && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - if !extend_end && t < -1e-6 && t > best_t { - best_t = t; - } - } - } - } - Geo::InfLine { - handle, - bx: ibx, - by: iby, - dx: idx, - dy: idy, - } => { - if *handle == target { - continue; - } - if let Some((t, _u)) = ll(ax, ay, dx, dy, *ibx, *iby, *idx, *idy) { - if extend_end && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - if !extend_end && t < -1e-6 && t > best_t { - best_t = t; - } - } - } - Geo::Ellipse { - handle, - cx: ecx, - cy: ecy, - a, - b, - nx, - ny, - t0: et0, - t1: et1, - } => { - if *handle == target { - continue; - } - for (t, t_ell) in le(ax, ay, dx, dy, *ecx, *ecy, *a, *b, *nx, *ny) { - if !in_arc(t_ell, *et0, *et1) { - continue; - } - if extend_end && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - if !extend_end && t < -1e-6 && t > best_t { - best_t = t; - } - } - } - Geo::Spline { handle, segs, .. } => { - if *handle == target { - continue; - } - for (p1, p2) in segs { - let ex = p2[0] - p1[0]; - let ey = p2[1] - p1[1]; - if let Some((t, u)) = ll(ax, ay, dx, dy, p1[0], p1[1], ex, ey) { - if !(-1e-9..=1.0 + 1e-9).contains(&u) { - continue; - } - if extend_end && t > 1.0 + 1e-6 && t < best_t { - best_t = t; - } - if !extend_end && t < -1e-6 && t > best_t { - best_t = t; - } - } - } - } - } - } - - if !best_t.is_finite() { - return None; - } let mut line = orig.clone(); line.common.handle = Handle::NULL; let new_x = ax + best_t * dx;