refactor(trim): find boundary crossings with cadkernel

`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 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5 committed 2026-08-08 19:53:55 +03:00
1 parent 5bd5c884e0
commit dde5ac07cf
3 files changed
+171 -601

No files matched your search

Generated
+1 -1
View File
@@ -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",
]
-5
View File
@@ -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<f64> {
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.
+170 -595
View File
@@ -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<Geo> {
// ── 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<f64> {
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<Curve> {
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<KernelVertex> = 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<f64> {
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<f64> = 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::<Vec<_>>()
})
.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<f64> {
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<f64> {
let mut ts = vec![];
for geo in geos {
let angles: Vec<f64> = 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<f64> {
// 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<f64> {
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<f64> {
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<Entit
// ── Spline trim / extend ──────────────────────────────────────────────────
/// Find normalised t-params ∈ [0,1] where a Spline intersects boundary geos.
/// Uses sampled polyline segments for intersection detection.
fn spline_seg_ts(spl: &SplineEnt, target: Handle, geos: &[Geo]) -> Vec<f64> {
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<EntityType> {
let Some((t0, t1)) = spline_range(spl) else {
@@ -1031,15 +601,20 @@ fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option<EntityTy
}
let (dx, dy) = (dx / len, dy / len);
// Shoot a ray from the endpoint along the tangent and find nearest boundary.
let ray_end_x = ep[0] + dx * TRIM_EXTENT;
let ray_end_y = ep[1] + dy * TRIM_EXTENT;
let seg_ts = line_seg_ts(ep[0], ep[1], ray_end_x, ray_end_y, spl.common.handle, geos);
// Shoot a real ray from the endpoint along the tangent. The direction is a
// unit vector, so the parameter that comes back is the distance to the
// boundary — no stand-in length to pick, and nothing beyond it to miss.
let shot = Curve::Ray(KernelRay {
origin: ep,
direction: [dx, dy],
});
let best_t = cut_params(&shot, spl.common.handle, geos)
.into_iter()
.filter(|&t| t > 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);