From dde5ac07cfcdd9b1d9c614cbbda8c34cfc0e8a12 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 8 Aug 2026 19:53:55 +0300 Subject: [PATCH] refactor(trim): find boundary crossings with cadkernel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `line_seg_ts`, `arc_seg_ts`, `ellipse_seg_ts` and `spline_seg_ts` each walked every boundary kind and solved that pair by hand — four nested matches over the same seven cases. They are now four descriptions of the target as a `Curve` handed to one `cut_params`, which asks the kernel. Their signatures are unchanged, so the twenty call sites are untouched. Three things follow from it. Boundary splines and target splines stop being sampled. A cut on a spline now lands where the boundary actually crosses rather than where a fixed sampling said it did, because the kernel subdivides and converges to a tolerance instead. Rays and infinite lines as boundaries are handled by the kernel's extent rather than by a case here — a ray rejecting what lies behind its origin is the kernel's job now. `extend_spline` shoots a real ray. It used to build a line a million units long and cut against that, which both invents a limit and, at survey coordinates, is comparable to the coordinates themselves. The ray's direction is a unit vector, so what comes back is the distance to the boundary. That also fixes the hit position, which multiplied by the million-unit extent a second time and landed the extended end far past where it was supposed to. `cut_params` clamps to 0..=1 only for a bounded target; a ray has to keep the parameter it comes back with, since the distance is the answer. Co-Authored-By: Claude Opus 5 --- Cargo.lock | 2 +- src/modules/draw/modify/geom.rs | 5 - src/modules/draw/modify/trim.rs | 765 +++++++------------------------- 3 files changed, 171 insertions(+), 601 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbc4f1b5a..e0e800513 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -945,7 +945,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git#e76334b7697a3c39292c8a6494d9b837bba78a6e" +source = "git+https://github.com/HakanSeven12/cadkernel.git#5294a45d19508c22726c01b91215d3ca3fd4c815" dependencies = [ "cavalier_contours", ] diff --git a/src/modules/draw/modify/geom.rs b/src/modules/draw/modify/geom.rs index ca8eacb87..64569cc05 100644 --- a/src/modules/draw/modify/geom.rs +++ b/src/modules/draw/modify/geom.rs @@ -51,11 +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) } -/// Angles **on the first circle** where two circles meet. -pub fn circle_circle_angles(cx1: f64, cy1: f64, r1: f64, cx2: f64, cy2: f64, r2: f64) -> Vec { - geom2d::circle_circle_angles([cx1, cy1], r1, [cx2, cy2], r2) -} - /// `(s_on_line, t_on_ellipse)` pairs where a line meets an ellipse. /// /// `nx, ny` is the unit major axis. diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index d43214ae6..4b976296b 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -15,7 +15,7 @@ use std::f64::consts::TAU; use super::geom; use super::geom::{ angle_within_arc as in_arc, arc_parameter as arc_t, arc_points as arc_pts, - circle_circle_angles as cc_angles, ellipse_points as ellipse_pts, lerp as lerp2, + ellipse_points as ellipse_pts, lerp as lerp2, line_circle as lc, line_ellipse as le, line_line as ll, normalize_angle as norm, }; @@ -29,6 +29,13 @@ use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::DVec3; use acadrust::kernel::geom2d::nurbs::clamped_uniform_knots; +use acadrust::kernel::geom2d::{ + intersect as kernel_intersect, Arc as KernelArc, Circle as KernelCircle, Curve, + Extent as KernelExtent, + Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc, Line as KernelLine, + Polyline as KernelPolyline, PolylineVertex as KernelVertex, Ray as KernelRay, + Tolerance as KernelTolerance, XLine as KernelXLine, +}; use crate::command::{CadCommand, CmdResult}; use crate::modules::draw::modify::spline_ops::{ @@ -250,134 +257,126 @@ fn geo_from_entity(h: Handle, e: &EntityType) -> Option { // ── Intersection helpers ────────────────────────────────────────────────── -/// Sorted, deduped t-params ∈ [0,1] where LINE segment (ax,ay)→(bx,by) intersects boundaries. -fn line_seg_ts(ax: f64, ay: f64, bx: f64, by: f64, target: Handle, geos: &[Geo]) -> Vec { - let (dx, dy) = (bx - ax, by - ay); - let mut ts = vec![]; - 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) && (-1e-9..=1.0 + 1e-9).contains(&t) { - ts.push(t.clamp(0.0, 1.0)); - } - } - } - Geo::Arc { - handle, - cx, - cy, - r, - a0, - a1, - } => { - if *handle == target { - continue; - } - for t in lc(ax, ay, dx, dy, *cx, *cy, *r) { - if !(-1e-9..=1.0 + 1e-9).contains(&t) { - continue; - } - let ix = ax + t * dx; - let iy = ay + t * dy; - if in_arc((iy - cy).atan2(ix - cx), *a0, *a1) { - ts.push(t.clamp(0.0, 1.0)); - } - } - } - Geo::Circle { handle, cx, cy, r } => { - if *handle == target { - continue; - } - for t in lc(ax, ay, dx, dy, *cx, *cy, *r) { - if (-1e-9..=1.0 + 1e-9).contains(&t) { - ts.push(t.clamp(0.0, 1.0)); - } - } - } - 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) { - // Ray: u >= 0 (semi-infinite) - if u >= -1e-9 && (-1e-9..=1.0 + 1e-9).contains(&t) { - ts.push(t.clamp(0.0, 1.0)); - } - } - } - 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) { - // XLine: any u accepted - if (-1e-9..=1.0 + 1e-9).contains(&t) { - ts.push(t.clamp(0.0, 1.0)); - } - } - } - Geo::Ellipse { - handle, - cx, - cy, - a, - b, - nx, - ny, - t0, - t1, - } => { - if *handle == target { - continue; - } - for (s, t_ell) in le(ax, ay, dx, dy, *cx, *cy, *a, *b, *nx, *ny) { - if !(-1e-9..=1.0 + 1e-9).contains(&s) { - continue; - } - if in_arc(t_ell, *t0, *t1) { - ts.push(s.clamp(0.0, 1.0)); - } - } - } - 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) && (-1e-9..=1.0 + 1e-9).contains(&t) { - ts.push(t.clamp(0.0, 1.0)); - } - } - } - } - } +// ── Boundary crossings ──────────────────────────────────────────────────── +// +// Every cut a trim or extend makes comes from one question: where does the +// entity being edited meet the boundaries picked as cutting edges. The kernel +// answers it for any pair of curves, so these four just describe the target as +// a `Curve` and hand it over. + +/// The entity a boundary came from, so a curve is never cut by itself. +fn geo_handle(geo: &Geo) -> Handle { + match geo { + Geo::Line { handle, .. } + | Geo::Arc { handle, .. } + | Geo::Circle { handle, .. } + | Geo::Ray { handle, .. } + | Geo::InfLine { handle, .. } + | Geo::Ellipse { handle, .. } + | Geo::Spline { handle, .. } => *handle, } - ts.sort_by(|a, b| a.partial_cmp(b).unwrap()); +} + +/// A boundary as a kernel curve. +fn geo_to_curve(geo: &Geo) -> Option { + Some(match geo { + Geo::Line { p1, p2, .. } => Curve::Line(KernelLine { + start: *p1, + end: *p2, + }), + Geo::Arc { cx, cy, r, a0, a1, .. } => Curve::Arc(KernelArc { + centre: [*cx, *cy], + radius: *r, + start_angle: *a0, + end_angle: *a1, + }), + Geo::Circle { cx, cy, r, .. } => Curve::Circle(KernelCircle { + centre: [*cx, *cy], + radius: *r, + }), + Geo::Ray { bx, by, dx, dy, .. } => Curve::Ray(KernelRay { + origin: [*bx, *by], + direction: [*dx, *dy], + }), + Geo::InfLine { bx, by, dx, dy, .. } => Curve::XLine(KernelXLine { + base: [*bx, *by], + direction: [*dx, *dy], + }), + Geo::Ellipse { + cx, cy, a, b, nx, ny, t0, t1, .. + } => Curve::Ellipse(KernelEllipseArc { + ellipse: KernelEllipse { + centre: [*cx, *cy], + major_radius: *a, + minor_radius: *b, + major_axis: [*nx, *ny], + }, + start_parameter: *t0, + end_parameter: *t1, + }), + // Boundary splines arrive already sampled into segments, so they stay + // a chain of straight pieces here. + Geo::Spline { segs, .. } => { + let mut vertices: Vec = segs + .iter() + .map(|(from, _)| KernelVertex::straight(*from)) + .collect(); + let last = segs.last()?.1; + vertices.push(KernelVertex::straight(last)); + if vertices.len() < 2 { + return None; + } + Curve::Polyline(KernelPolyline { + vertices, + closed: false, + }) + } + }) +} + +/// Where `target` is cut by every boundary except its own entity, as +/// parameters along it in `0..=1`. +fn cut_params(target: &Curve, handle: Handle, geos: &[Geo]) -> Vec { + let tolerance = KernelTolerance::new(CUT_TOLERANCE); + // A bounded target's parameters are pinned to its own span, as every + // caller here expects. An unbounded one — a ray shot out to find the + // nearest boundary — has to keep whatever it comes back with, since the + // whole point is how far away the crossing is. + let bounded = matches!(target.extent(), KernelExtent::Bounded); + let mut ts: Vec = geos + .iter() + .filter(|geo| geo_handle(geo) != handle) + .filter_map(geo_to_curve) + .flat_map(|boundary| { + kernel_intersect(target, &boundary, tolerance) + .into_iter() + .map(|hit| if bounded { hit.t_a.clamp(0.0, 1.0) } else { hit.t_a }) + .collect::>() + }) + .collect(); + ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); ts.dedup_by(|a, b| (*a - *b).abs() < 1e-6); ts } -/// Sorted, deduped t-params ∈ [0,1] where ARC (cx,cy,r,a0→a1) intersects boundaries. +/// How close two points have to be to count as a crossing. +/// +/// Loose enough that a boundary drawn to meet an entity still registers when +/// its endpoint is a rounding step away, which is the usual case in a drawing +/// that was snapped together rather than computed. +const CUT_TOLERANCE: f64 = 1e-7; + +fn line_seg_ts(ax: f64, ay: f64, bx: f64, by: f64, target: Handle, geos: &[Geo]) -> Vec { + cut_params( + &Curve::Line(KernelLine { + start: [ax, ay], + end: [bx, by], + }), + target, + geos, + ) +} + fn arc_seg_ts( cx: f64, cy: f64, @@ -387,213 +386,19 @@ fn arc_seg_ts( target: Handle, geos: &[Geo], ) -> Vec { - let mut ts = vec![]; - for geo in geos { - let angles: Vec = match geo { - Geo::Line { handle, p1, p2 } => { - if *handle == target { - continue; - } - let (ldx, ldy) = (p2[0] - p1[0], p2[1] - p1[1]); - lc(p1[0], p1[1], ldx, ldy, cx, cy, r) - .into_iter() - .filter(|&u| (-1e-9..=1.0 + 1e-9).contains(&u)) - .map(|u| (p1[1] + u * ldy - cy).atan2(p1[0] + u * ldx - cx)) - .collect() - } - Geo::Arc { - handle, - cx: cx2, - cy: cy2, - r: r2, - a0: a02, - a1: a12, - } => { - if *handle == target { - continue; - } - cc_angles(cx, cy, r, *cx2, *cy2, *r2) - .into_iter() - .filter(|&a| { - // `a` lies on the TARGET circle — re-express the - // intersection point as an angle on the BOUNDARY - // circle before testing that arc's span (#370). - let px = cx + r * a.cos(); - let py = cy + r * a.sin(); - in_arc((py - cy2).atan2(px - cx2), *a02, *a12) - }) - .collect() - } - Geo::Circle { - handle, - cx: cx2, - cy: cy2, - r: r2, - } => { - if *handle == target { - continue; - } - cc_angles(cx, cy, r, *cx2, *cy2, *r2) - } - Geo::Ray { - handle, - bx: rbx, - by: rby, - dx: rdx, - dy: rdy, - } => { - if *handle == target { - continue; - } - // Intersect arc circle with the Ray direction - lc(*rbx, *rby, *rdx, *rdy, cx, cy, r) - .into_iter() - .filter(|&u| u >= -1e-9) // Ray: u >= 0 - .map(|u| (rby + u * rdy - cy).atan2(rbx + u * rdx - cx)) - .collect() - } - Geo::InfLine { - handle, - bx: ibx, - by: iby, - dx: idx, - dy: idy, - } => { - if *handle == target { - continue; - } - // XLine: any u accepted - lc(*ibx, *iby, *idx, *idy, cx, cy, r) - .into_iter() - .map(|u| (iby + u * idy - cy).atan2(ibx + u * idx - cx)) - .collect() - } - Geo::Ellipse { - handle, - cx: ecx, - cy: ecy, - a: ea, - b: eb, - nx, - ny, - t0: et0, - t1: et1, - } => { - if *handle == target { - continue; - } - // Sample the arc and find where it crosses the ellipse boundary. - ellipse_boundary_angles_for_arc( - cx, cy, r, a0, a1, *ecx, *ecy, *ea, *eb, *nx, *ny, *et0, *et1, - ) - } - Geo::Spline { handle, segs } => { - if *handle == target { - continue; - } - // Intersect arc circle with each spline segment. - let mut hit_angles = vec![]; - for (p1, p2) in segs { - let ldx = p2[0] - p1[0]; - let ldy = p2[1] - p1[1]; - for u in lc(p1[0], p1[1], ldx, ldy, cx, cy, r) { - if !(-1e-9..=1.0 + 1e-9).contains(&u) { - continue; - } - let ix = p1[0] + u * ldx; - let iy = p1[1] + u * ldy; - hit_angles.push((iy - cy).atan2(ix - cx)); - } - } - hit_angles - } - }; - for a in angles { - if in_arc(a, a0, a1) { - ts.push(arc_t(a, a0, a1)); - } - } - } - ts.sort_by(|a, b| a.partial_cmp(b).unwrap()); - ts.dedup_by(|a, b| (*a - *b).abs() < 1e-6); - ts + cut_params( + &Curve::Arc(KernelArc { + centre: [cx, cy], + radius: r, + start_angle: a0, + end_angle: a1, + }), + target, + geos, + ) } -/// Find angles on a circular arc where it crosses an ellipse-arc boundary. -/// Uses 64-sample sign-change detection + bisection. -fn ellipse_boundary_angles_for_arc( - cx: f64, - cy: f64, - r: f64, - a0: f64, - a1: f64, - ecx: f64, - ecy: f64, - ea: f64, - eb: f64, - nx: f64, - ny: f64, - et0: f64, - et1: f64, -) -> Vec { - // f(α) = (x_local/ea)² + (y_local/eb)² – 1 where (x_local, y_local) is the arc - // point projected onto ellipse local axes. - let f = |alpha: f64| { - let px = cx + r * alpha.cos() - ecx; - let py = cy + r * alpha.sin() - ecy; - let xl = px * nx + py * ny; - let yl = -px * ny + py * nx; - (xl / ea).powi(2) + (yl / eb).powi(2) - 1.0 - }; - let span = { - let s = norm(a1) - norm(a0); - if s <= 0.0 { - s + TAU - } else { - s - } - }; - let n = 128usize; - let mut hits = vec![]; - let mut prev = f(norm(a0)); - for i in 1..=n { - let alpha = norm(a0) + span * (i as f64 / n as f64); - let cur = f(alpha); - if prev * cur <= 0.0 { - // Bisect - let alpha_lo = norm(a0) + span * ((i - 1) as f64 / n as f64); - let alpha_hi = alpha; - let mut lo = alpha_lo; - let mut hi = alpha_hi; - let mut flo = prev; - for _ in 0..32 { - let mid = (lo + hi) * 0.5; - let fm = f(mid); - if flo * fm <= 0.0 { - hi = mid; - } else { - lo = mid; - flo = fm; - } - } - let alpha_hit = (lo + hi) * 0.5; - // Check that the intersection point is on the ellipse ARC (not outside t0..t1) - let px = cx + r * alpha_hit.cos() - ecx; - let py = cy + r * alpha_hit.sin() - ecy; - let xl = px * nx + py * ny; - let yl = -px * ny + py * nx; - let t_ell = yl.atan2(xl); - if in_arc(t_ell, et0, et1) { - hits.push(alpha_hit); - } - } - prev = cur; - } - hits -} - -/// Sorted t-params ∈ [0,1] where an ELLIPSE arc intersects boundary geometries. -/// t is the normalised eccentric-anomaly parameter along [t0, t1]. +#[allow(clippy::too_many_arguments)] fn ellipse_seg_ts( cx: f64, cy: f64, @@ -606,235 +411,30 @@ fn ellipse_seg_ts( target: Handle, geos: &[Geo], ) -> Vec { - let span = t1 - t0; // always positive (build_geos ensures t1 > t0) - let ellipse_pt = |t: f64| -> [f64; 2] { - [ - cx + a * t.cos() * nx - b * t.sin() * ny, - cy + a * t.cos() * ny + b * t.sin() * nx, - ] - }; - // f_boundary(t) > 0 means "outside this boundary segment" - let mut ts = vec![]; + cut_params( + &Curve::Ellipse(KernelEllipseArc { + ellipse: KernelEllipse { + centre: [cx, cy], + major_radius: a, + minor_radius: b, + major_axis: [nx, ny], + }, + start_parameter: t0, + end_parameter: t1, + }), + target, + geos, + ) +} - for geo in geos { - match geo { - Geo::Line { handle, p1, p2 } => { - if *handle == target { - continue; - } - // Find t values where ellipse crosses the infinite line p1→p2, - // then filter to the finite segment [p1,p2]. - let ldx = p2[0] - p1[0]; - let ldy = p2[1] - p1[1]; - for (s, t_ell) in le(p1[0], p1[1], ldx, ldy, cx, cy, a, b, nx, ny) { - if !(-1e-9..=1.0 + 1e-9).contains(&s) { - continue; - } - if in_arc(t_ell, t0, t1) { - let t_norm = arc_t(t_ell, t0, t0 + span); - ts.push(t_norm); - } - } - } - Geo::Arc { - handle, - cx: acx, - cy: acy, - r, - a0: aa0, - a1: aa1, - } => { - if *handle == target { - continue; - } - // 64-sample sign-change on (dist_to_arc_circle - r) - let n = 64usize; - let mut prev_sign = { - let [px, py] = ellipse_pt(t0); - (px - acx).hypot(py - acy) - r - }; - for i in 1..=n { - let t_ell = t0 + span * (i as f64 / n as f64); - let [px, py] = ellipse_pt(t_ell); - let cur_sign = (px - acx).hypot(py - acy) - r; - if prev_sign * cur_sign <= 0.0 { - let t_lo = t0 + span * ((i - 1) as f64 / n as f64); - let t_hi = t_ell; - let mut lo = t_lo; - let mut hi = t_hi; - let mut flo = prev_sign; - for _ in 0..32 { - let mid = (lo + hi) * 0.5; - let [px2, py2] = ellipse_pt(mid); - let fm = (px2 - acx).hypot(py2 - acy) - r; - if flo * fm <= 0.0 { - hi = mid; - } else { - lo = mid; - flo = fm; - } - } - let t_hit = (lo + hi) * 0.5; - let [phx, phy] = ellipse_pt(t_hit); - let ang = (phy - acy).atan2(phx - acx); - if in_arc(ang, *aa0, *aa1) { - ts.push(arc_t(t_hit, t0, t0 + span)); - } - } - prev_sign = cur_sign; - } - } - Geo::Circle { - handle, - cx: acx, - cy: acy, - r, - } => { - if *handle == target { - continue; - } - let n = 64usize; - let mut prev_sign = { - let [px, py] = ellipse_pt(t0); - (px - acx).hypot(py - acy) - r - }; - for i in 1..=n { - let t_ell = t0 + span * (i as f64 / n as f64); - let [px, py] = ellipse_pt(t_ell); - let cur_sign = (px - acx).hypot(py - acy) - r; - if prev_sign * cur_sign <= 0.0 { - let t_lo = t0 + span * ((i - 1) as f64 / n as f64); - let t_hi = t_ell; - let mut lo = t_lo; - let mut hi = t_hi; - let mut flo = prev_sign; - for _ in 0..32 { - let mid = (lo + hi) * 0.5; - let [px2, py2] = ellipse_pt(mid); - let fm = (px2 - acx).hypot(py2 - acy) - r; - if flo * fm <= 0.0 { - hi = mid; - } else { - lo = mid; - flo = fm; - } - } - ts.push(arc_t((lo + hi) * 0.5, t0, t0 + span)); - } - prev_sign = cur_sign; - } - } - Geo::Ray { - handle, - bx: rbx, - by: rby, - dx: rdx, - dy: rdy, - } => { - if *handle == target { - continue; - } - for (s, t_ell) in le(*rbx, *rby, *rdx, *rdy, cx, cy, a, b, nx, ny) { - if s >= -1e-9 && in_arc(t_ell, t0, t1) { - ts.push(arc_t(t_ell, t0, t0 + span)); - } - } - } - Geo::InfLine { - handle, - bx: ibx, - by: iby, - dx: idx, - dy: idy, - } => { - if *handle == target { - continue; - } - for (_s, t_ell) in le(*ibx, *iby, *idx, *idy, cx, cy, a, b, nx, ny) { - if in_arc(t_ell, t0, t1) { - ts.push(arc_t(t_ell, t0, t0 + span)); - } - } - } - Geo::Ellipse { handle, .. } => { - if *handle == target { - continue; - } - // Ellipse-ellipse: numerical 64-sample - if let Geo::Ellipse { - cx: ecx2, - cy: ecy2, - a: ea2, - b: eb2, - nx: nx2, - ny: ny2, - t0: et02, - t1: et12, - .. - } = geo - { - let n = 64usize; - let f = |t: f64| -> f64 { - let [px, py] = ellipse_pt(t); - let xl = (px - ecx2) * nx2 + (py - ecy2) * ny2; - let yl = -(px - ecx2) * ny2 + (py - ecy2) * nx2; - (xl / ea2).powi(2) + (yl / eb2).powi(2) - 1.0 - }; - let mut prev_f = f(t0); - for i in 1..=n { - let t_ell = t0 + span * (i as f64 / n as f64); - let cur_f = f(t_ell); - if prev_f * cur_f <= 0.0 { - let t_lo = t0 + span * ((i - 1) as f64 / n as f64); - let mut lo = t_lo; - let mut hi = t_ell; - let mut flo = prev_f; - for _ in 0..32 { - let mid = (lo + hi) * 0.5; - let fm = f(mid); - if flo * fm <= 0.0 { - hi = mid; - } else { - lo = mid; - flo = fm; - } - } - let t_hit = (lo + hi) * 0.5; - let [phx, phy] = ellipse_pt(t_hit); - let xl = (phx - ecx2) * nx2 + (phy - ecy2) * ny2; - let yl = -(phx - ecx2) * ny2 + (phy - ecy2) * nx2; - let t_ell2 = yl.atan2(xl); - if in_arc(t_ell2, *et02, *et12) { - ts.push(arc_t(t_hit, t0, t0 + span)); - } - } - prev_f = cur_f; - } - } - } - Geo::Spline { handle, segs } => { - if *handle == target { - continue; - } - // Ellipse × Spline: sign-change detection on each spline segment - for (p1, p2) in segs { - let ldx = p2[0] - p1[0]; - let ldy = p2[1] - p1[1]; - for (s, t_ell) in le(p1[0], p1[1], ldx, ldy, cx, cy, a, b, nx, ny) { - if !(-1e-9..=1.0 + 1e-9).contains(&s) { - continue; - } - if in_arc(t_ell, t0, t1) { - ts.push(arc_t(t_ell, t0, t0 + span)); - } - } - } - } - } - } - ts.sort_by(|a, b| a.partial_cmp(b).unwrap()); - ts.dedup_by(|a, b| (*a - *b).abs() < 1e-6); - ts +/// Cuts on a spline, solved against the true curve rather than a sampling of +/// it. The kernel subdivides and converges, so a cut lands where the boundary +/// actually crosses instead of where a fixed number of samples said it did. +fn spline_seg_ts(spl: &SplineEnt, target: Handle, geos: &[Geo]) -> Vec { + let Some(curve) = spline_to_nurbs(spl) else { + return vec![]; + }; + cut_params(&Curve::Nurbs(curve), target, geos) } /// Trim an Ellipse entity. Returns the surviving ellipse-arc segments. @@ -940,36 +540,6 @@ fn extend_ellipse(orig: &EllipseEnt, t_click: f64, geos: &[Geo]) -> Option Vec { - let Some((t0, t1)) = spline_range(spl) else { - return vec![]; - }; - let range = t1 - t0; - if range < 1e-12 { - return vec![]; - } - - let (ts_spl, pts) = spline_sample_xy(spl, 64); - let mut out = vec![]; - for i in 0..pts.len().saturating_sub(1) { - let ax = pts[i][0]; - let ay = pts[i][1]; - let bx = pts[i + 1][0]; - let by = pts[i + 1][1]; - let seg_ts = line_seg_ts(ax, ay, bx, by, target, geos); - for u in seg_ts { - // u is a t-param on this polyline segment; map to spline knot range, then normalise. - let t_spline = ts_spl[i] + u * (ts_spl[i + 1] - ts_spl[i]); - out.push(t_to_rel(t_spline, t0, t1)); - } - } - out.sort_by(|a, b| a.partial_cmp(b).unwrap()); - out.dedup_by(|a, b| (*a - *b).abs() < 1e-4); - out -} - /// Trim a Spline entity. Returns surviving spline pieces (one or two). fn trim_spline(spl: &SplineEnt, ts: &[f64], t_click: f64) -> Vec { let Some((t0, t1)) = spline_range(spl) else { @@ -1031,15 +601,20 @@ fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option 1e-6) + .reduce(f64::min)?; - let best_t = seg_ts.into_iter().filter(|&t| t > 1e-6).reduce(f64::min)?; - - let hit_x = ep[0] + best_t * (ray_end_x - ep[0]) * TRIM_EXTENT; - let hit_y = ep[1] + best_t * (ray_end_y - ep[1]) * TRIM_EXTENT; + let hit_x = ep[0] + best_t * dx; + let hit_y = ep[1] + best_t * dy; // Add a new control point at the hit location by appending/prepending. let z = spl.control_points.first().map(|v| v.z).unwrap_or(0.0);