forked from 1b-agent/opencadstudio-upstream
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:
1 parent
5bd5c884e0
commit
dde5ac07cf
3 files changed
+171
-601
No files matched your search
Generated
+1
-1
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user