cad-editor/src/modules/draw/modify/trim.rs
Hakan Seven 5bd683cf4e feat(mline): GPU-dash multiline elements with shared A-type alignment
MLINE dashed elements were fully CPU-expanded (apply_along) — one wire per
dash — to keep the parallel lines in phase. Move pure-dash elements onto the
GPU dash shader and end-align every element to the multiline's centre-line so
they share one interior phase.

- wire_distances now accumulates arc-length from the double-single (high+low)
  points, so the dash phase stays precise at UTM coordinates. f32-high-only
  deltas quantised ~0.1 there and drifted the parallel lines apart — the
  original reason MLINE dashes were CPU-side.
- New WireModel.dash_align_end carries a shared begin/end dash length. The
  shader uses it as align_end for every parallel element (the interior phase
  depends on align_end, not the wire's own length) while align_total stays the
  element's own length, so each still ends on a dash at its own endpoint.
- MLINE tessellation derives that length once from the centre-line, routes
  pure dash/space/dot elements to the GPU pattern and text-bearing elements to
  apply_along.
- apply_along gained an "A"-type mode (shared reference length): a solid
  begin/end dash and a phased interior, so its dashes — and embedded text —
  line up with the GPU-dashed sibling elements instead of tiling from start.

Pure-dash MLINEs now cost one WireModel + pattern per element instead of N
segments; single (non-MLINE) entities keep the from-start tiling unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:59:11 +03:00

3312 lines
114 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Trim / Extend — ribbon definitions + full command implementations.
//
// TRIM (TR): Click the segment you want to remove. The command finds all
// intersections of that entity with every other entity and trims
// out the clicked interval. Stays active — click more segments,
// press Enter to finish.
//
// EXTEND (EX): Click near one end of an entity. The command extends that
// endpoint to the nearest intersecting boundary. Stays active.
use std::f64::consts::TAU;
use acadrust::entities::{
Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline,
LwVertex, Ray as RayEnt, Spline as SplineEnt, XLine as XLineEnt,
};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use glam::DVec3;
use truck_modeling::base::{BoundedCurve, Cut, ParametricCurve};
use crate::command::{CadCommand, CmdResult};
use crate::modules::draw::modify::spline_ops::{
bspline_to_spline, spline_nearest_t, spline_pts_wire, spline_sample_xy, spline_to_bspline,
t_to_rel,
};
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;
// ── Dropdown constants ─────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "trim_extend";
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../../assets/icons/trim.svg"));
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
(
"TRIM",
"Trim",
IconKind::Svg(include_bytes!("../../../../assets/icons/trim.svg")),
),
(
"EXTEND",
"Extend",
IconKind::Svg(include_bytes!("../../../../assets/icons/extend.svg")),
),
];
// ══════════════════════════════════════════════════════════════════════════
// Geometry helpers
// ══════════════════════════════════════════════════════════════════════════
/// Normalize angle to [0, 2π).
fn norm(a: f64) -> f64 {
((a % TAU) + TAU) % TAU
}
/// Is angle `a` within the arc from `s` to `e` (CCW, radians)?
fn in_arc(a: f64, s: f64, e: f64) -> bool {
let (a, s, e) = (norm(a), norm(s), norm(e));
if (e - s).abs() < 1e-9 || (e - s - TAU).abs() < 1e-9 {
return true;
}
if s <= e {
a >= s - 1e-9 && a <= e + 1e-9
} else {
a >= s - 1e-9 || a <= e + 1e-9
}
}
/// Parametric t ∈ [0,1] on arc (a0→a1 CCW) for angle `a`.
fn arc_t(a: f64, a0: f64, a1: f64) -> f64 {
let span = {
let s = norm(a1) - norm(a0);
if s <= 0.0 {
s + TAU
} else {
s
}
};
let da = {
let d = norm(a) - norm(a0);
if d < 0.0 {
d + TAU
} else {
d
}
};
(da / span).clamp(0.0, 1.0)
}
/// Intersect infinite lines (p+t·d) and (q+u·e). Returns (t, u).
fn ll(
px: f64,
py: f64,
dx: f64,
dy: f64,
qx: f64,
qy: f64,
ex: f64,
ey: f64,
) -> Option<(f64, f64)> {
let det = dx * ey - dy * ex;
if det.abs() < 1e-10 {
return None;
}
let t = ((qx - px) * ey - (qy - py) * ex) / det;
let u = ((qx - px) * dy - (qy - py) * dx) / det;
Some((t, u))
}
/// Intersect infinite line (p+t·d) with circle (cx,cy,r). Returns t values.
fn lc(px: f64, py: f64, dx: f64, dy: f64, cx: f64, cy: f64, r: f64) -> Vec<f64> {
let fx = px - cx;
let fy = py - cy;
let a = dx * dx + dy * dy;
let b = 2.0 * (fx * dx + fy * dy);
let c = fx * fx + fy * fy - r * r;
let disc = b * b - 4.0 * a * c;
if disc < 0.0 {
return vec![];
}
let sq = disc.sqrt();
if disc < 1e-14 {
vec![(-b) / (2.0 * a)]
} else {
vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
}
}
/// Circle-circle intersection: angles on circle 1 where they meet.
fn cc_angles(cx1: f64, cy1: f64, r1: f64, cx2: f64, cy2: f64, r2: f64) -> Vec<f64> {
let d = ((cx2 - cx1).powi(2) + (cy2 - cy1).powi(2)).sqrt();
if d < 1e-9 || d > r1 + r2 + 1e-9 || d < (r1 - r2).abs() - 1e-9 {
return vec![];
}
let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
let h2 = r1 * r1 - a * a;
if h2 < 0.0 {
return vec![];
}
let h = h2.sqrt();
let mx = cx1 + a * (cx2 - cx1) / d;
let my = cy1 + a * (cy2 - cy1) / d;
let px = h * (cy2 - cy1) / d;
let py = -h * (cx2 - cx1) / d;
let a1 = ((my + py) - cy1).atan2((mx + px) - cx1);
let a2 = ((my - py) - cy1).atan2((mx - px) - cx1);
if h < 1e-9 {
vec![a1]
} else {
vec![a1, a2]
}
}
/// Line (px+s·d) vs ellipse (cx,cy,a,b,nx,ny). Returns (s_on_line, t_on_ellipse) pairs.
/// nx,ny: unit major-axis; perp = (-ny, nx). Parametric ellipse: P(t) = center + a·cos(t)·n + b·sin(t)·v.
fn le(
px: f64,
py: f64,
dpx: f64,
dpy: f64,
cx: f64,
cy: f64,
a: f64,
b: f64,
nx: f64,
ny: f64,
) -> Vec<(f64, f64)> {
// Transform line origin to ellipse local frame
let rx = px - cx;
let ry = py - cy;
// Project onto major/minor axes
let xl0 = rx * nx + ry * ny;
let yl0 = -rx * ny + ry * nx;
let dxl = dpx * nx + dpy * ny;
let dyl = -dpx * ny + dpy * nx;
// Scale by 1/a, 1/b → circle equation
let xa = xl0 / a;
let xda = dxl / a;
let yb = yl0 / b;
let ydb = dyl / b;
let big_a = xda * xda + ydb * ydb;
if big_a < 1e-20 {
return vec![];
}
let big_b = 2.0 * (xa * xda + yb * ydb);
let big_c = xa * xa + yb * yb - 1.0;
let disc = big_b * big_b - 4.0 * big_a * big_c;
if disc < 0.0 {
return vec![];
}
let sq = disc.sqrt();
let s_vals: Vec<f64> = if disc < 1e-14 {
vec![(-big_b) / (2.0 * big_a)]
} else {
vec![(-big_b - sq) / (2.0 * big_a), (-big_b + sq) / (2.0 * big_a)]
};
s_vals
.into_iter()
.map(|s| {
let xl = xl0 + s * dxl;
let yl = yl0 + s * dyl;
let t = yl.atan2(xl); // ≡ atan2(yl/b, xl/a) but faster since sign is preserved
(s, t)
})
.collect()
}
// ── Boundary geometry ─────────────────────────────────────────────────────
/// Virtual extent used to represent infinite ends of Ray / XLine.
const TRIM_EXTENT: f64 = 1_000_000.0;
/// If a trim interval endpoint is beyond this threshold it is treated as "infinite".
const INF_T: f64 = 0.9999;
#[derive(Clone)]
enum Geo {
Line {
handle: Handle,
p1: [f64; 2],
p2: [f64; 2],
},
Arc {
handle: Handle,
cx: f64,
cy: f64,
r: f64,
a0: f64,
a1: f64,
},
Circle {
handle: Handle,
cx: f64,
cy: f64,
r: f64,
},
/// Semi-infinite line from base in +direction.
Ray {
handle: Handle,
bx: f64,
by: f64,
dx: f64,
dy: f64,
},
/// Fully-infinite line through base along direction.
InfLine {
handle: Handle,
bx: f64,
by: f64,
dx: f64,
dy: f64,
},
/// Ellipse arc: center, semi-axes, unit major-axis direction, parameter range [t0,t1].
Ellipse {
handle: Handle,
cx: f64,
cy: f64,
a: f64, // semi-major
b: f64, // semi-minor
nx: f64, // unit major-axis X
ny: f64, // unit major-axis Y
t0: f64, // start parameter
t1: f64, // end parameter (may be > 2π if wrapped)
},
/// Spline represented as sampled polyline segments (DXF XY).
Spline {
handle: Handle,
segs: Vec<([f64; 2], [f64; 2])>,
},
}
fn build_geos(entities: &[EntityType]) -> Vec<Geo> {
let mut out = Vec::new();
for e in entities {
let h = e.common().handle;
match e {
// A polyline acts as a boundary through its constituent edges, so
// a Line/Arc/… can be trimmed against it. Explode into Line + Arc
// segments and tag each with the polyline's own handle (so trim
// still excludes it as the click target).
EntityType::LwPolyline(_)
| EntityType::Polyline(_)
| EntityType::Polyline2D(_)
| EntityType::Polyline3D(_) => {
for seg in crate::modules::draw::modify::explode::explode_polyline_segments(e) {
if let Some(g) = geo_from_entity(h, &seg) {
out.push(g);
}
}
}
_ => {
if let Some(g) = geo_from_entity(h, e) {
out.push(g);
}
}
}
}
out
}
/// Convert a simple boundary entity (Line / Arc / Circle / Ray / XLine /
/// Ellipse / Spline) into a `Geo`, tagged with `h`. Returns `None` for types
/// that do not act as trim boundaries.
fn geo_from_entity(h: Handle, e: &EntityType) -> Option<Geo> {
match e {
EntityType::Line(l) => Some(Geo::Line {
handle: h,
p1: [l.start.x, l.start.y],
p2: [l.end.x, l.end.y],
}),
EntityType::Arc(a) => Some(Geo::Arc {
handle: h,
cx: a.center.x,
cy: a.center.y,
r: a.radius,
a0: a.start_angle,
a1: a.end_angle,
}),
EntityType::Circle(c) => Some(Geo::Circle {
handle: h,
cx: c.center.x,
cy: c.center.y,
r: c.radius,
}),
EntityType::Ray(r) => Some(Geo::Ray {
handle: h,
bx: r.base_point.x,
by: r.base_point.y,
dx: r.direction.x,
dy: r.direction.y,
}),
EntityType::XLine(x) => Some(Geo::InfLine {
handle: h,
bx: x.base_point.x,
by: x.base_point.y,
dx: x.direction.x,
dy: x.direction.y,
}),
EntityType::Ellipse(e) => {
let mx = e.major_axis.x;
let my = e.major_axis.y;
let a = (mx * mx + my * my).sqrt();
if a < 1e-9 {
return None;
}
let (nx, ny) = (mx / a, my / a);
let b = a * e.minor_axis_ratio;
let t0 = e.start_parameter;
let mut t1 = e.end_parameter;
if t1 <= t0 {
t1 += TAU;
}
Some(Geo::Ellipse {
handle: h,
cx: e.center.x,
cy: e.center.y,
a,
b,
nx,
ny,
t0,
t1,
})
}
EntityType::Spline(s) => {
let (_, pts) = spline_sample_xy(s, 64);
if pts.len() < 2 {
return None;
}
let segs = pts
.windows(2)
.map(|w| ([w[0][0], w[0][1]], [w[1][0], w[1][1]]))
.collect();
Some(Geo::Spline { handle: h, segs })
}
_ => None,
}
}
// ── 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));
}
}
}
}
}
}
ts.sort_by(|a, b| a.partial_cmp(b).unwrap());
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.
fn arc_seg_ts(
cx: f64,
cy: f64,
r: f64,
a0: f64,
a1: f64,
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| in_arc(a, *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
}
/// 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].
fn ellipse_seg_ts(
cx: f64,
cy: f64,
a: f64,
b: f64,
nx: f64,
ny: f64,
t0: f64,
t1: f64,
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![];
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
}
/// Trim an Ellipse entity. Returns the surviving ellipse-arc segments.
fn trim_ellipse(orig: &EllipseEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
let t0 = orig.start_parameter;
let mut t1 = orig.end_parameter;
if t1 <= t0 {
t1 += TAU;
}
let span = t1 - t0;
let angle_at = |t: f64| t0 + span * t;
trim_intervals(ts, t_click)
.into_iter()
.filter_map(|(ta, tb)| {
if (tb - ta).abs() < 1e-6 {
return None;
}
let mut e = orig.clone();
e.common.handle = Handle::NULL;
e.start_parameter = angle_at(ta);
e.end_parameter = angle_at(tb);
Some(EntityType::Ellipse(e))
})
.collect()
}
/// Extend an Ellipse arc to the nearest boundary (along the arc direction).
fn extend_ellipse(orig: &EllipseEnt, t_click: f64, geos: &[Geo]) -> Option<EntityType> {
let t0 = orig.start_parameter;
let mut t1 = orig.end_parameter;
if t1 <= t0 {
t1 += TAU;
}
let span = t1 - t0;
let a = (orig.major_axis.x.powi(2) + orig.major_axis.y.powi(2)).sqrt();
if a < 1e-9 {
return None;
}
let b = a * orig.minor_axis_ratio;
let (nx, ny) = (orig.major_axis.x / a, orig.major_axis.y / a);
let cx = orig.center.x;
let cy = orig.center.y;
let ts = ellipse_seg_ts(cx, cy, a, b, nx, ny, t0, t1, orig.common.handle, geos);
let extend_end = t_click >= 0.5;
let best = if extend_end {
ts.into_iter()
.filter(|&t| t > 1.0 + 1e-6)
.min_by(|x, y| x.partial_cmp(y).unwrap())
} else {
ts.into_iter()
.filter(|&t| t < -1e-6)
.max_by(|x, y| x.partial_cmp(y).unwrap())
};
let best_t = best?;
let new_param = t0 + span * best_t;
let mut e = orig.clone();
e.common.handle = Handle::NULL;
if extend_end {
e.end_parameter = new_param;
} else {
e.start_parameter = new_param;
}
Some(EntityType::Ellipse(e))
}
/// Generate preview points for an ellipse arc.
fn ellipse_pts(
cx: f64,
cy: f64,
a: f64,
b: f64,
nx: f64,
ny: f64,
t0: f64,
t1: f64,
z: f64,
) -> Vec<[f32; 3]> {
let span = t1 - t0;
let steps = (span.abs() * 20.0).ceil().max(4.0) as usize;
(0..=steps)
.map(|i| {
let t = t0 + span * (i as f64 / steps as f64);
let lx = a * t.cos();
let ly = b * t.sin();
[
(cx + lx * nx - ly * ny) as f32,
z as f32,
(cy + lx * ny + ly * nx) as f32,
]
})
.collect()
}
// ── 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 bs = match spline_to_bspline(spl) {
Some(b) => b,
None => return vec![],
};
let (t0, t1) = bs.range_tuple();
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 bs = match spline_to_bspline(spl) {
Some(b) => b,
None => return vec![],
};
let (t0, t1) = bs.range_tuple();
trim_intervals(ts, t_click)
.into_iter()
.filter_map(|(ta, tb)| {
let t_lo = t0 + ta * (t1 - t0);
let t_hi = t0 + tb * (t1 - t0);
if t_hi - t_lo < 1e-9 {
return None;
}
let mut piece = bs.clone();
let right = piece.cut(t_lo); // piece = [t0..t_lo] (discarded), right = [t_lo..t1]
let mut right = right;
let _tail = right.cut(t_hi); // right = [t_lo..t_hi], _tail discarded
Some(EntityType::Spline(bspline_to_spline(&right, spl)))
})
.collect()
}
/// Extend a Spline toward the nearest boundary (nearest endpoint to pick).
fn extend_spline(spl: &SplineEnt, t_click: f64, geos: &[Geo]) -> Option<EntityType> {
// Sample spline and treat it like a polyline; look for intersections beyond
// the current start (t<0 virtual) or end (t>1 virtual).
// For splines we simply find whether the start (t=0) or end (t=1) is closer
// to the click, then walk along that tangent direction to the nearest boundary.
let bs = spline_to_bspline(spl)?;
let (t0, t1) = bs.range_tuple();
let extend_end = t_click >= 0.5;
// Tangent at the endpoint (numerical, Δ = 1e-4 of range)
let delta = (t1 - t0) * 1e-4;
let (ep_t, tang_dir) = if extend_end {
let p0 = bs.subs(t1 - delta);
let p1 = bs.subs(t1);
(t1, [p1.x - p0.x, p1.y - p0.y])
} else {
let p0 = bs.subs(t0);
let p1 = bs.subs(t0 + delta);
(t0, [p0.x - p1.x, p0.y - p1.y]) // reverse for "before start"
};
let ep = bs.subs(ep_t);
let (dx, dy) = (tang_dir[0], tang_dir[1]);
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-12 {
return None;
}
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.x + dx * TRIM_EXTENT;
let ray_end_y = ep.y + dy * TRIM_EXTENT;
let seg_ts = line_seg_ts(ep.x, ep.y, ray_end_x, ray_end_y, spl.common.handle, geos);
let best_t = seg_ts.into_iter().filter(|&t| t > 1e-6).reduce(f64::min)?;
let hit_x = ep.x + best_t * (ray_end_x - ep.x) * TRIM_EXTENT;
let hit_y = ep.y + best_t * (ray_end_y - ep.y) * TRIM_EXTENT;
// 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);
let mut new_spl = spl.clone();
new_spl.common.handle = Handle::NULL;
new_spl.fit_points.clear();
if extend_end {
new_spl
.control_points
.push(acadrust::types::Vector3::new(hit_x, hit_y, z));
} else {
new_spl
.control_points
.insert(0, acadrust::types::Vector3::new(hit_x, hit_y, z));
}
// Rebuild knots (uniform) for the extended control polygon.
let degree = new_spl.degree as usize;
let n = new_spl.control_points.len();
let kv = truck_modeling::KnotVec::uniform_knot(degree, n - 1);
new_spl.knots = kv.iter().copied().collect();
Some(EntityType::Spline(new_spl))
}
// ── Trim helpers ──────────────────────────────────────────────────────────
/// Remove the t-interval containing `t_click` from sorted ts. Returns surviving pieces.
fn trim_intervals(ts: &[f64], t_click: f64) -> Vec<(f64, f64)> {
let mut bounds = vec![0.0f64];
bounds.extend_from_slice(ts);
bounds.push(1.0);
bounds.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
let remove = bounds
.windows(2)
.position(|w| t_click >= w[0] - 1e-6 && t_click <= w[1] + 1e-6);
bounds
.windows(2)
.enumerate()
.filter(|(idx, _)| Some(*idx) != remove)
.filter(|(_, w)| (w[1] - w[0]) > 1e-6)
.map(|(_, w)| (w[0], w[1]))
.collect()
}
fn lerp2(p1: [f64; 2], p2: [f64; 2], t: f64) -> [f64; 2] {
[p1[0] + t * (p2[0] - p1[0]), p1[1] + t * (p2[1] - p1[1])]
}
/// Trim a Line entity. Returns the surviving line segments.
fn trim_line(orig: &LineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
let p1 = [orig.start.x, orig.start.y];
let p2 = [orig.end.x, orig.end.y];
let z = orig.start.z;
trim_intervals(ts, t_click)
.into_iter()
.filter_map(|(ta, tb)| {
let a = lerp2(p1, p2, ta);
let b = lerp2(p1, p2, tb);
if (b[0] - a[0]).hypot(b[1] - a[1]) < 1e-6 {
return None;
}
let mut l = orig.clone();
l.common.handle = Handle::NULL;
l.start = Vector3::new(a[0], a[1], z);
l.end = Vector3::new(b[0], b[1], z);
Some(EntityType::Line(l))
})
.collect()
}
/// Trim an Arc entity. Returns the surviving arc segments.
fn trim_arc(orig: &ArcEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
let a0 = orig.start_angle;
let a1 = orig.end_angle;
let span = {
let s = norm(a1) - norm(a0);
if s <= 0.0 {
s + TAU
} else {
s
}
};
let angle_at = |t: f64| norm(a0) + span * t;
trim_intervals(ts, t_click)
.into_iter()
.filter_map(|(ta, tb)| {
if (tb - ta).abs() < 1e-6 {
return None;
}
let mut a = orig.clone();
a.common.handle = Handle::NULL;
a.start_angle = angle_at(ta);
a.end_angle = angle_at(tb);
Some(EntityType::Arc(a))
})
.collect()
}
/// Trim a clicked Circle. A full circle has no endpoints, so it needs ≥2
/// boundary crossings: the arc segment containing the click is removed and the
/// circle becomes a single Arc spanning the rest. With fewer than two crossings
/// there is nothing to cut, so an empty result leaves the circle unchanged.
///
/// `ts` are the cut parameters in [0,1) around the circle (angle / TAU), sorted.
fn trim_circle(orig: &CircleEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
if ts.len() < 2 {
return vec![];
}
let tc = t_click.rem_euclid(1.0);
// Find the cyclic gap (ta, tb) between adjacent cuts that holds the click;
// the last gap wraps past 1.0 back to the first cut.
let n = ts.len();
let mut removed: Option<(f64, f64)> = None;
for i in 0..n {
let ta = ts[i];
let tb = if i + 1 < n { ts[i + 1] } else { ts[0] + 1.0 };
if (tc >= ta - 1e-9 && tc <= tb + 1e-9)
|| (tc + 1.0 >= ta - 1e-9 && tc + 1.0 <= tb + 1e-9)
{
removed = Some((ta, tb));
break;
}
}
let (ta, tb) = match removed {
Some(g) => g,
None => return vec![],
};
// Surviving arc runs CCW from the far edge of the removed gap all the way
// around to its near edge.
let mut arc = ArcEnt::new();
arc.common = orig.common.clone();
arc.common.handle = Handle::NULL;
arc.center = orig.center;
arc.radius = orig.radius;
arc.thickness = orig.thickness;
arc.normal = orig.normal;
arc.start_angle = (tb % 1.0) * TAU;
arc.end_angle = (ta % 1.0) * TAU;
vec![EntityType::Arc(arc)]
}
/// 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
/// the surviving piece(s). Bulges on fully-surviving segments are kept; the
/// partial end segments at a cut become straight (issue #65).
fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option<Vec<EntityType>> {
let handle = poly.common.handle;
let n = poly.vertices.len();
if n < 2 {
return None;
}
let closed = poly.is_closed;
let seg_count = if closed { n } else { n - 1 };
let total = seg_count as f64;
let vx = |i: usize| -> (f64, f64) {
let v = &poly.vertices[i % n];
(v.location.x, v.location.y)
};
let point_at = |t: f64| -> (f64, f64) {
let tt = if closed { t.rem_euclid(total) } else { t.clamp(0.0, total) };
let i = (tt.floor() as usize).min(seg_count.saturating_sub(1));
let u = tt - i as f64;
let (ax, ay) = vx(i);
let (bx, by) = vx(i + 1);
(ax + u * (bx - ax), ay + u * (by - ay))
};
// Boundary cuts as global params (segment index + local u).
let mut cuts: Vec<f64> = Vec::new();
for i in 0..seg_count {
let (ax, ay) = vx(i);
let (bx, by) = vx(i + 1);
for u in line_seg_ts(ax, ay, bx, by, handle, geos) {
cuts.push(i as f64 + u.clamp(0.0, 1.0));
}
}
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;
}
// Click param: nearest point on the polyline.
let mut best = (f64::INFINITY, 0.0_f64);
for i in 0..seg_count {
let (ax, ay) = vx(i);
let (bx, by) = vx(i + 1);
let (dx, dy) = (bx - ax, by - ay);
let len2 = dx * dx + dy * dy;
let u = if len2 > 1e-12 {
(((cx - ax) * dx + (cy - ay) * dy) / len2).clamp(0.0, 1.0)
} else {
0.0
};
let (px, py) = (ax + u * dx, ay + u * dy);
let d = (px - cx).powi(2) + (py - cy).powi(2);
if d < best.0 {
best = (d, i as f64 + u);
}
}
let t_click = best.1;
// Emit the surviving sub-polyline from param `s0` to `s1` (s1 > s0), with
// both ends treated as cut points (straight) and interior vertices keeping
// their original bulge.
let emit = |s0: f64, s1: f64, start_cut: bool, end_cut: bool| -> Vec<(f64, f64, f64)> {
let mut o: Vec<(f64, f64, f64)> = Vec::new();
let (sx, sy) = point_at(s0);
let s_idx = (s0.floor() as usize) % seg_count;
let s_bulge = if start_cut { 0.0 } else { poly.vertices[s_idx % n].bulge };
o.push((sx, sy, s_bulge));
let mut k = s0.floor() as i64 + 1;
while (k as f64) < s1 - 1e-9 {
let idx = (k as usize) % n;
let seg = (k as usize) % seg_count;
o.push((vx(idx).0, vx(idx).1, poly.vertices[seg].bulge));
k += 1;
}
if end_cut {
if let Some(l) = o.last_mut() {
l.2 = 0.0; // outgoing toward the cut is partial → straight
}
}
let (ex, ey) = point_at(s1);
o.push((ex, ey, 0.0));
o
};
let mut pieces: Vec<Vec<(f64, f64, f64)>> = Vec::new();
if closed {
if cuts.len() < 2 {
return None;
}
let hi = cuts
.iter()
.cloned()
.find(|&c| c > t_click + 1e-9)
.unwrap_or(cuts[0] + total);
let lo = cuts
.iter()
.cloned()
.rev()
.find(|&c| c < t_click - 1e-9)
.unwrap_or(cuts[cuts.len() - 1] - total);
let mut s1 = lo;
while s1 <= hi {
s1 += total;
}
pieces.push(emit(hi, s1, true, true));
} else {
let lo = cuts.iter().cloned().rev().find(|&c| c < t_click - 1e-9);
let hi = cuts.iter().cloned().find(|&c| c > t_click + 1e-9);
if let Some(lo) = lo {
pieces.push(emit(0.0, lo, false, true));
}
if let Some(hi) = hi {
pieces.push(emit(hi, total, true, false));
}
}
let mut out: Vec<EntityType> = Vec::new();
for verts in pieces {
if verts.len() < 2 {
continue;
}
let mut np = poly.clone();
np.common.handle = Handle::NULL;
np.is_closed = false;
np.vertices = verts
.into_iter()
.map(|(x, y, b)| {
let mut v = LwVertex::from_coords(x, y);
v.bulge = b;
v
})
.collect();
out.push(EntityType::LwPolyline(np));
}
Some(out)
}
// ── Extend helpers ────────────────────────────────────────────────────────
/// Extend the first or last segment of an LwPolyline to the nearest boundary.
/// Click point (DXF XY) determines which end to extend.
fn extend_lwpoly(
poly: &LwPolyline,
click_x: f64,
click_y: f64,
geos: &[Geo],
) -> Option<EntityType> {
let n = poly.vertices.len();
if n < 2 {
return None;
}
let first = &poly.vertices[0];
let second = &poly.vertices[1];
let last = &poly.vertices[n - 1];
let prev = &poly.vertices[n - 2];
let d_first = (first.location.x - click_x).hypot(first.location.y - click_y);
let d_last = (last.location.x - click_x).hypot(last.location.y - click_y);
let extend_end = d_last <= d_first;
// Extract the terminal segment as a virtual line.
let (ax, ay, bx, by) = if extend_end {
(
prev.location.x,
prev.location.y,
last.location.x,
last.location.y,
)
} else {
(
second.location.x,
second.location.y,
first.location.x,
first.location.y,
)
};
let (dx, dy) = (bx - ax, by - ay);
let len2 = dx * dx + dy * dy;
if len2 < 1e-12 {
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 new_x = ax + best_t * dx;
let new_y = ay + best_t * dy;
let mut new_poly = poly.clone();
new_poly.common.handle = Handle::NULL;
if extend_end {
let last_v = new_poly.vertices.last_mut()?;
last_v.location.x = new_x;
last_v.location.y = new_y;
} else {
let first_v = new_poly.vertices.first_mut()?;
first_v.location.x = new_x;
first_v.location.y = new_y;
}
Some(EntityType::LwPolyline(new_poly))
}
/// Extend a Line to the nearest boundary on the extended side.
/// t_click < 0.5 → extend start (look for t < 0); t_click ≥ 0.5 → extend end (t > 1).
fn extend_line(orig: &LineEnt, t_click: f64, geos: &[Geo]) -> Option<EntityType> {
let ax = orig.start.x;
let ay = orig.start.y;
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 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;
let new_y = ay + best_t * dy;
if extend_end {
line.end = Vector3::new(new_x, new_y, orig.end.z);
} else {
line.start = Vector3::new(new_x, new_y, orig.start.z);
}
Some(EntityType::Line(line))
}
/// Trim a Ray entity.
/// Virtual t ∈ [0,1]: t=0 → base_point, t=1 → base + TRIM_EXTENT * dir.
/// Surviving pieces become Lines (finite) or Rays (still semi-infinite).
fn trim_ray(orig: &RayEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
let bx = orig.base_point.x;
let by = orig.base_point.y;
let bz = orig.base_point.z;
let dx = orig.direction.x;
let dy = orig.direction.y;
let dz = orig.direction.z;
let pt = |t: f64| {
[
bx + t * dx * TRIM_EXTENT,
by + t * dy * TRIM_EXTENT,
bz + t * dz * TRIM_EXTENT,
]
};
trim_intervals(ts, t_click)
.into_iter()
.filter_map(|(ta, tb)| {
let pa = pt(ta);
let pb = pt(tb);
if (pb[0] - pa[0]).hypot(pb[1] - pa[1]) < 1e-6 {
return None;
}
if tb > INF_T {
// Still extends to infinity → remains a Ray with new base
let r = RayEnt::new(Vector3::new(pa[0], pa[1], pa[2]), Vector3::new(dx, dy, dz));
let mut r = r;
r.common = orig.common.clone();
r.common.handle = Handle::NULL;
Some(EntityType::Ray(r))
} else {
// Finite segment → Line
let mut l = LineEnt {
common: orig.common.clone(),
..LineEnt::new()
};
l.common.handle = Handle::NULL;
l.start = Vector3::new(pa[0], pa[1], pa[2]);
l.end = Vector3::new(pb[0], pb[1], pb[2]);
Some(EntityType::Line(l))
}
})
.collect()
}
/// Trim an XLine entity.
/// Virtual t ∈ [0,1]: t=0 → base - dir*TRIM_EXTENT, t=0.5 → base, t=1 → base + dir*TRIM_EXTENT.
/// Surviving pieces become Lines (finite), Rays (one infinite end), or the original XLine (both ends).
fn trim_xline(orig: &XLineEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
let bx = orig.base_point.x;
let by = orig.base_point.y;
let bz = orig.base_point.z;
let dx = orig.direction.x;
let dy = orig.direction.y;
let dz = orig.direction.z;
// Point at virtual t: scale factor s = 2t - 1 ∈ [-1, +1]
let pt = |t: f64| {
let s = 2.0 * t - 1.0;
[
bx + s * dx * TRIM_EXTENT,
by + s * dy * TRIM_EXTENT,
bz + s * dz * TRIM_EXTENT,
]
};
trim_intervals(ts, t_click)
.into_iter()
.filter_map(|(ta, tb)| {
let pa = pt(ta);
let pb = pt(tb);
let ext_neg = ta < 1.0 - INF_T; // extends toward -infinity
let ext_pos = tb > INF_T; // extends toward +infinity
match (ext_neg, ext_pos) {
(true, true) => {
// Whole XLine survived (shouldn't happen after a real trim)
let mut x = orig.clone();
x.common.handle = Handle::NULL;
Some(EntityType::XLine(x))
}
(true, false) => {
// Extends toward -infinity: Ray at pb pointing in -dir
let r = RayEnt::new(
Vector3::new(pb[0], pb[1], pb[2]),
Vector3::new(-dx, -dy, -dz),
);
let mut r = r;
r.common = orig.common.clone();
r.common.handle = Handle::NULL;
Some(EntityType::Ray(r))
}
(false, true) => {
// Extends toward +infinity: Ray at pa pointing in +dir
let r =
RayEnt::new(Vector3::new(pa[0], pa[1], pa[2]), Vector3::new(dx, dy, dz));
let mut r = r;
r.common = orig.common.clone();
r.common.handle = Handle::NULL;
Some(EntityType::Ray(r))
}
(false, false) => {
// Finite segment
let mut l = LineEnt {
common: orig.common.clone(),
..LineEnt::new()
};
l.common.handle = Handle::NULL;
l.start = Vector3::new(pa[0], pa[1], pa[2]);
l.end = Vector3::new(pb[0], pb[1], pb[2]);
Some(EntityType::Line(l))
}
}
})
.collect()
}
// ── Point-generation helpers ──────────────────────────────────────────────
const DIM_RED: [f32; 4] = [1.0, 0.3, 0.3, 0.6];
fn line_pts(l: &LineEnt) -> Vec<[f32; 3]> {
vec![
[l.start.x as f32, l.start.y as f32, l.start.z as f32],
[l.end.x as f32, l.end.y as f32, l.end.z as f32],
]
}
fn arc_pts(cx: f64, cy: f64, r: f64, a0: f64, a1: f64, z: f64) -> Vec<[f32; 3]> {
let span = {
let s = norm(a1) - norm(a0);
if s <= 0.0 {
s + TAU
} else {
s
}
};
let steps = (span.abs() * 20.0).ceil().max(4.0) as usize;
(0..=steps)
.map(|i| {
let ang = norm(a0) + span * (i as f64 / steps as f64);
[
(cx + r * ang.cos()) as f32,
(cy + r * ang.sin()) as f32,
z as f32,
]
})
.collect()
}
fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> {
match e {
EntityType::Line(l) => line_pts(l),
EntityType::Arc(a) => arc_pts(
a.center.x,
a.center.y,
a.radius,
a.start_angle,
a.end_angle,
a.center.z,
),
EntityType::Ellipse(e) => {
let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt();
if a < 1e-9 {
return vec![];
}
let b = a * e.minor_axis_ratio;
let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a);
let t0 = e.start_parameter;
let mut t1 = e.end_parameter;
if t1 <= t0 {
t1 += TAU;
}
ellipse_pts(e.center.x, e.center.y, a, b, nx, ny, t0, t1, e.center.z)
}
EntityType::Spline(s) => spline_pts_wire(s),
EntityType::LwPolyline(p) => {
let elev = p.elevation as f32;
let n = p.vertices.len();
let seg_count = if p.is_closed { n } else { n.saturating_sub(1) };
let mut pts = Vec::with_capacity(seg_count * 2);
for i in 0..seg_count {
let v0 = &p.vertices[i];
let v1 = &p.vertices[(i + 1) % n];
pts.push([v0.location.x as f32, v0.location.y as f32, elev]);
pts.push([v1.location.x as f32, v1.location.y as f32, elev]);
}
pts
}
// For preview, show a 20-unit section of semi-infinite results
EntityType::Ray(r) => {
let bx = r.base_point.x;
let by = r.base_point.y;
let bz = r.base_point.z;
let far_x = bx + r.direction.x * 20.0;
let far_y = by + r.direction.y * 20.0;
let far_z = bz + r.direction.z * 20.0;
vec![
[bx as f32, bz as f32, by as f32],
[far_x as f32, far_z as f32, far_y as f32],
]
}
_ => vec![],
}
}
// ══════════════════════════════════════════════════════════════════════════
// TrimCommand
// ══════════════════════════════════════════════════════════════════════════
pub struct TrimCommand {
all_entities: Vec<EntityType>,
geos: Vec<Geo>,
}
impl TrimCommand {
pub fn new(all_entities: Vec<EntityType>) -> Self {
let geos = build_geos(&all_entities);
Self { all_entities, geos }
}
}
impl CadCommand for TrimCommand {
fn name(&self) -> &'static str {
"TRIM"
}
fn prompt(&self) -> String {
"TRIM Click segment to remove:".into()
}
fn options(&self) -> Vec<crate::command::CmdOption> {
vec![crate::command::CmdOption::enter("Done")]
}
fn needs_entity_pick(&self) -> bool {
true
}
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle);
let result: Option<Vec<EntityType>> = match entity {
Some(EntityType::Line(l)) => {
let ax = l.start.x;
let ay = l.start.y;
let bx = l.end.x;
let by = l.end.y;
let ts = line_seg_ts(ax, ay, bx, by, handle, &self.geos);
if ts.is_empty() {
return CmdResult::NeedPoint;
}
let dx = bx - ax;
let dy = by - ay;
let len2 = dx * dx + dy * dy;
let t_click = if len2 > 1e-12 {
((pt.x as f64 - ax) * dx + (pt.y as f64 - ay) * dy) / len2
} else {
0.5
};
Some(trim_line(l, &ts, t_click))
}
Some(EntityType::Arc(a)) => {
let cx = a.center.x;
let cy = a.center.y;
let a0 = a.start_angle;
let a1 = a.end_angle;
let ts = arc_seg_ts(cx, cy, a.radius, a0, a1, handle, &self.geos);
if ts.is_empty() {
return CmdResult::NeedPoint;
}
let click_angle = (pt.y as f64 - cy).atan2(pt.x as f64 - cx);
let t_click = arc_t(click_angle, a0, a1);
Some(trim_arc(a, &ts, t_click))
}
Some(EntityType::Circle(c)) => {
let cx = c.center.x;
let cy = c.center.y;
let ts = arc_seg_ts(cx, cy, c.radius, 0.0, TAU, handle, &self.geos);
if ts.len() < 2 {
return CmdResult::NeedPoint;
}
let click_angle = (pt.y as f64 - cy).atan2(pt.x as f64 - cx);
let t_click = arc_t(click_angle, 0.0, TAU);
let survivors = trim_circle(c, &ts, t_click);
if survivors.is_empty() {
return CmdResult::NeedPoint;
}
Some(survivors)
}
Some(EntityType::Ray(r)) => {
// Virtual segment: base → base + dir * TRIM_EXTENT (t ∈ [0,1])
let bx = r.base_point.x;
let by = r.base_point.y;
let ex = bx + r.direction.x * TRIM_EXTENT;
let ey = by + r.direction.y * TRIM_EXTENT;
let ts = line_seg_ts(bx, by, ex, ey, handle, &self.geos);
if ts.is_empty() {
return CmdResult::NeedPoint;
}
let dx = r.direction.x * TRIM_EXTENT;
let dy = r.direction.y * TRIM_EXTENT;
let len2 = dx * dx + dy * dy;
let t_click = if len2 > 1e-12 {
((pt.x as f64 - bx) * dx + (pt.y as f64 - by) * dy) / len2
} else {
0.5
};
Some(trim_ray(r, &ts, t_click))
}
Some(EntityType::XLine(x)) => {
// Virtual segment: base - dir*TRIM_EXTENT → base + dir*TRIM_EXTENT
let bx = x.base_point.x - x.direction.x * TRIM_EXTENT;
let by = x.base_point.y - x.direction.y * TRIM_EXTENT;
let ex = x.base_point.x + x.direction.x * TRIM_EXTENT;
let ey = x.base_point.y + x.direction.y * TRIM_EXTENT;
let ts = line_seg_ts(bx, by, ex, ey, handle, &self.geos);
if ts.is_empty() {
return CmdResult::NeedPoint;
}
let dx = ex - bx;
let dy = ey - by;
let len2 = dx * dx + dy * dy;
let t_click = if len2 > 1e-12 {
((pt.x as f64 - bx) * dx + (pt.y as f64 - by) * dy) / len2
} else {
0.5
};
Some(trim_xline(x, &ts, t_click))
}
Some(EntityType::Ellipse(e)) => {
let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt();
if a < 1e-9 {
return CmdResult::NeedPoint;
}
let b = a * e.minor_axis_ratio;
let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a);
let t0 = e.start_parameter;
let mut t1 = e.end_parameter;
if t1 <= t0 {
t1 += TAU;
}
let ts = ellipse_seg_ts(
e.center.x, e.center.y, a, b, nx, ny, t0, t1, handle, &self.geos,
);
if ts.is_empty() {
return CmdResult::NeedPoint;
}
// t_click: project mouse onto ellipse local param
let rx = pt.x as f64 - e.center.x;
let ry = pt.y as f64 - e.center.y;
let xl = rx * nx + ry * ny;
let yl = -rx * ny + ry * nx;
let t_ell = yl.atan2(xl);
let t_click = arc_t(t_ell, t0, t1);
Some(trim_ellipse(e, &ts, t_click))
}
Some(EntityType::Spline(s)) => {
let ts = spline_seg_ts(s, handle, &self.geos);
if ts.is_empty() {
return CmdResult::NeedPoint;
}
let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64)
.and_then(|t_actual| {
let bs = spline_to_bspline(s)?;
let (t0, t1) = bs.range_tuple();
Some(t_to_rel(t_actual, t0, t1))
})
.unwrap_or(0.5);
Some(trim_spline(s, &ts, t_click))
}
Some(EntityType::LwPolyline(p)) => {
match trim_lwpolyline(p, pt.x as f64, pt.y as f64, &self.geos) {
Some(v) => Some(v),
None => return CmdResult::NeedPoint,
}
}
_ => None,
};
if let Some(new_entities) = result {
// Snapshot is updated in on_entity_replaced once we know the real handles.
// Pre-stage: remove old entry now so geos exclude it immediately.
if let Some(pos) = self
.all_entities
.iter()
.position(|e| e.common().handle == handle)
{
self.all_entities.remove(pos);
// Add pieces with NULL handles as geometry-only placeholders.
self.all_entities.extend(new_entities.clone());
self.geos = build_geos(&self.all_entities);
}
CmdResult::ReplaceEntity(handle, new_entities)
} else {
self.command_line_hint();
CmdResult::NeedPoint
}
}
fn on_entity_replaced(&mut self, _old: Handle, new_handles: &[acadrust::Handle]) {
// The last new_handles.len() entries in all_entities are the trimmed pieces
// that were appended with NULL handles. Assign their real document handles.
let start = self.all_entities.len().saturating_sub(new_handles.len());
for (e, &h) in self.all_entities[start..]
.iter_mut()
.zip(new_handles.iter())
{
match e {
EntityType::Line(l) => l.common.handle = h,
EntityType::Arc(a) => a.common.handle = h,
EntityType::Ray(r) => r.common.handle = h,
EntityType::XLine(x) => x.common.handle = h,
EntityType::Ellipse(e) => e.common.handle = h,
EntityType::Spline(s) => s.common.handle = h,
// A trimmed (closed or open) polyline is re-emitted as an
// LwPolyline; without its real handle it can't be found on a
// second pick, so the same polyline couldn't be trimmed twice
// in one TRIM command.
EntityType::LwPolyline(p) => p.common.handle = h,
_ => {}
}
}
self.geos = build_geos(&self.all_entities);
}
fn on_hover_entity(&mut self, handle: Handle, pt: DVec3) -> Vec<WireModel> {
if handle.is_null() {
return vec![];
}
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle);
match entity {
Some(EntityType::Line(l)) => {
let ax = l.start.x;
let ay = l.start.y;
let bx = l.end.x;
let by = l.end.y;
let ts = line_seg_ts(ax, ay, bx, by, handle, &self.geos);
if ts.is_empty() {
return vec![];
}
let dx = bx - ax;
let dy = by - ay;
let len2 = dx * dx + dy * dy;
let t_click = if len2 > 1e-12 {
((pt.x as f64 - ax) * dx + (pt.y as f64 - ay) * dy) / len2
} else {
0.5
};
let survivors = trim_line(l, &ts, t_click);
let p1 = [l.start.x as f32, l.start.y as f32, l.start.y as f32];
let p2 = [l.end.x as f32, l.end.y as f32, l.end.y as f32];
let removed = WireModel::solid("trim_rm".into(), vec![p1, p2], DIM_RED, false);
let mut out = vec![removed];
for (i, e) in survivors.iter().enumerate() {
let pts = entity_pts(e);
out.push(WireModel::solid(
format!("trim_keep_{i}"),
pts,
WireModel::CYAN,
false,
));
}
out
}
Some(EntityType::Arc(a)) => {
let cx = a.center.x;
let cy = a.center.y;
let a0 = a.start_angle;
let a1 = a.end_angle;
let ts = arc_seg_ts(cx, cy, a.radius, a0, a1, handle, &self.geos);
if ts.is_empty() {
return vec![];
}
let click_angle = (pt.y as f64 - cy).atan2(pt.x as f64 - cx);
let t_click = arc_t(click_angle, a0, a1);
let survivors = trim_arc(a, &ts, t_click);
let orig_pts = arc_pts(cx, cy, a.radius, a0, a1, a.center.z);
let removed = WireModel::solid("trim_rm".into(), orig_pts, DIM_RED, false);
let mut out = vec![removed];
for (i, e) in survivors.iter().enumerate() {
let pts = entity_pts(e);
out.push(WireModel::solid(
format!("trim_keep_{i}"),
pts,
WireModel::CYAN,
false,
));
}
out
}
Some(EntityType::Circle(c)) => {
let cx = c.center.x;
let cy = c.center.y;
let ts = arc_seg_ts(cx, cy, c.radius, 0.0, TAU, handle, &self.geos);
if ts.len() < 2 {
return vec![];
}
let click_angle = (pt.y as f64 - cy).atan2(pt.x as f64 - cx);
let t_click = arc_t(click_angle, 0.0, TAU);
let survivors = trim_circle(c, &ts, t_click);
if survivors.is_empty() {
return vec![];
}
let orig_pts = arc_pts(cx, cy, c.radius, 0.0, TAU, c.center.z);
let removed = WireModel::solid("trim_rm".into(), orig_pts, DIM_RED, false);
let mut out = vec![removed];
for (i, e) in survivors.iter().enumerate() {
let pts = entity_pts(e);
out.push(WireModel::solid(
format!("trim_keep_{i}"),
pts,
WireModel::CYAN,
false,
));
}
out
}
Some(EntityType::Ray(r)) => {
let bx = r.base_point.x;
let by = r.base_point.y;
let ex = bx + r.direction.x * TRIM_EXTENT;
let ey = by + r.direction.y * TRIM_EXTENT;
let ts = line_seg_ts(bx, by, ex, ey, handle, &self.geos);
if ts.is_empty() {
return vec![];
}
let dx = r.direction.x * TRIM_EXTENT;
let dy = r.direction.y * TRIM_EXTENT;
let len2 = dx * dx + dy * dy;
let t_click = if len2 > 1e-12 {
((pt.x as f64 - bx) * dx + (pt.y as f64 - by) * dy) / len2
} else {
0.5
};
let survivors = trim_ray(r, &ts, t_click);
// Show a finite preview section (20 units) for the original ray
let far = [
(bx + r.direction.x * 20.0) as f32,
(by + r.direction.y * 20.0) as f32,
r.base_point.z as f32,
];
let base = [bx as f32, by as f32, r.base_point.z as f32];
let removed = WireModel::solid("trim_rm".into(), vec![base, far], DIM_RED, false);
let mut out = vec![removed];
for (i, e) in survivors.iter().enumerate() {
let pts = entity_pts(e);
out.push(WireModel::solid(
format!("trim_keep_{i}"),
pts,
WireModel::CYAN,
false,
));
}
out
}
Some(EntityType::XLine(x)) => {
let bx = x.base_point.x;
let by = x.base_point.y;
let ex_start = bx - x.direction.x * TRIM_EXTENT;
let ey_start = by - x.direction.y * TRIM_EXTENT;
let ex_end = bx + x.direction.x * TRIM_EXTENT;
let ey_end = by + x.direction.y * TRIM_EXTENT;
let ts = line_seg_ts(ex_start, ey_start, ex_end, ey_end, handle, &self.geos);
if ts.is_empty() {
return vec![];
}
let dx = ex_end - ex_start;
let dy = ey_end - ey_start;
let len2 = dx * dx + dy * dy;
let t_click = if len2 > 1e-12 {
((pt.x as f64 - ex_start) * dx + (pt.y as f64 - ey_start) * dy) / len2
} else {
0.5
};
let survivors = trim_xline(x, &ts, t_click);
let neg = [
(bx - x.direction.x * 20.0) as f32,
(by - x.direction.y * 20.0) as f32,
x.base_point.z as f32,
];
let pos_pt = [
(bx + x.direction.x * 20.0) as f32,
(by + x.direction.y * 20.0) as f32,
x.base_point.z as f32,
];
let removed = WireModel::solid("trim_rm".into(), vec![neg, pos_pt], DIM_RED, false);
let mut out = vec![removed];
for (i, e) in survivors.iter().enumerate() {
let pts = entity_pts(e);
out.push(WireModel::solid(
format!("trim_keep_{i}"),
pts,
WireModel::CYAN,
false,
));
}
out
}
Some(EntityType::Ellipse(e)) => {
let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt();
if a < 1e-9 {
return vec![];
}
let b = a * e.minor_axis_ratio;
let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a);
let t0 = e.start_parameter;
let mut t1 = e.end_parameter;
if t1 <= t0 {
t1 += TAU;
}
let ts = ellipse_seg_ts(
e.center.x, e.center.y, a, b, nx, ny, t0, t1, handle, &self.geos,
);
if ts.is_empty() {
return vec![];
}
let rx = pt.x as f64 - e.center.x;
let ry = pt.y as f64 - e.center.y;
let xl = rx * nx + ry * ny;
let yl = -rx * ny + ry * nx;
let t_click = arc_t(yl.atan2(xl), t0, t1);
let survivors = trim_ellipse(e, &ts, t_click);
let orig_pts =
ellipse_pts(e.center.x, e.center.y, a, b, nx, ny, t0, t1, e.center.z);
let removed = WireModel::solid("trim_rm".into(), orig_pts, DIM_RED, false);
let mut out = vec![removed];
for (i, ent) in survivors.iter().enumerate() {
let pts = entity_pts(ent);
out.push(WireModel::solid(
format!("trim_keep_{i}"),
pts,
WireModel::CYAN,
false,
));
}
out
}
Some(EntityType::Spline(s)) => {
let ts = spline_seg_ts(s, handle, &self.geos);
if ts.is_empty() {
return vec![];
}
let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64)
.and_then(|t_actual| {
let bs = spline_to_bspline(s)?;
let (t0, t1) = bs.range_tuple();
Some(t_to_rel(t_actual, t0, t1))
})
.unwrap_or(0.5);
let orig_pts = spline_pts_wire(s);
let removed = WireModel::solid("trim_rm".into(), orig_pts, DIM_RED, false);
let survivors = trim_spline(s, &ts, t_click);
let mut out = vec![removed];
for (i, ent) in survivors.iter().enumerate() {
let pts = entity_pts(ent);
out.push(WireModel::solid(
format!("trim_keep_{i}"),
pts,
WireModel::CYAN,
false,
));
}
out
}
Some(EntityType::LwPolyline(p)) => {
let Some(survivors) = trim_lwpolyline(p, pt.x as f64, pt.y as f64, &self.geos)
else {
return vec![];
};
let orig = WireModel::solid("trim_rm".into(), entity_pts(entity.unwrap()), DIM_RED, false);
let mut out = vec![orig];
for (i, ent) in survivors.iter().enumerate() {
out.push(WireModel::solid(
format!("trim_keep_{i}"),
entity_pts(ent),
WireModel::CYAN,
false,
));
}
out
}
_ => vec![],
}
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
impl TrimCommand {
fn command_line_hint(&self) {}
}
// ══════════════════════════════════════════════════════════════════════════
// ExtendCommand
// ══════════════════════════════════════════════════════════════════════════
pub struct ExtendCommand {
all_entities: Vec<EntityType>,
geos: Vec<Geo>,
/// (old_handle, new_entity_with_updated_geometry) — set in on_entity_pick,
/// consumed in on_entity_replaced to patch the snapshot with both new handle + geometry.
pending_replace: Option<(Handle, EntityType)>,
}
impl ExtendCommand {
pub fn new(all_entities: Vec<EntityType>) -> Self {
let geos = build_geos(&all_entities);
Self {
all_entities,
geos,
pending_replace: None,
}
}
}
impl CadCommand for ExtendCommand {
fn name(&self) -> &'static str {
"EXTEND"
}
fn prompt(&self) -> String {
"EXTEND Click near end of object to extend:".into()
}
fn options(&self) -> Vec<crate::command::CmdOption> {
vec![crate::command::CmdOption::enter("Done")]
}
fn needs_entity_pick(&self) -> bool {
true
}
fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle);
let result: Option<EntityType> = match entity {
Some(EntityType::Line(l)) => {
let ax = l.start.x;
let ay = l.start.y;
let bx = l.end.x;
let by = l.end.y;
let dx = bx - ax;
let dy = by - ay;
let len2 = dx * dx + dy * dy;
let t_click = if len2 > 1e-12 {
((pt.x as f64 - ax) * dx + (pt.y as f64 - ay) * dy) / len2
} else {
0.5
};
extend_line(l, t_click, &self.geos)
}
Some(EntityType::Ellipse(e)) => {
let t0 = e.start_parameter;
let mut t1 = e.end_parameter;
if t1 <= t0 {
t1 += TAU;
}
let span = t1 - t0;
let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt();
if a < 1e-9 {
return CmdResult::NeedPoint;
}
let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a);
let rx = pt.x as f64 - e.center.x;
let ry = pt.y as f64 - e.center.y;
let xl = rx * nx + ry * ny;
let yl = -rx * ny + ry * nx;
let t_click = arc_t(yl.atan2(xl), t0, t1);
let _ = span;
extend_ellipse(e, t_click, &self.geos)
}
Some(EntityType::LwPolyline(p)) => {
extend_lwpoly(p, pt.x as f64, pt.y as f64, &self.geos)
}
Some(EntityType::Spline(s)) => {
let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64)
.and_then(|t_actual| {
let bs = spline_to_bspline(s)?;
let (t0, t1) = bs.range_tuple();
Some(t_to_rel(t_actual, t0, t1))
})
.unwrap_or(0.5);
extend_spline(s, t_click, &self.geos)
}
_ => None,
};
if let Some(new_entity) = result {
// Save the extended entity so on_entity_replaced can patch the snapshot
// with both the new geometry and the real document handle.
self.pending_replace = Some((handle, new_entity.clone()));
CmdResult::ReplaceEntity(handle, vec![new_entity])
} else {
CmdResult::NeedPoint
}
}
fn on_entity_replaced(&mut self, old: Handle, new_handles: &[acadrust::Handle]) {
if let (Some(&new_handle), Some((pending_old, mut new_entity))) =
(new_handles.first(), self.pending_replace.take())
{
if pending_old == old {
// Update the snapshot entry: replace geometry + assign real handle.
match &mut new_entity {
EntityType::Line(l) => l.common.handle = new_handle,
EntityType::Ellipse(e) => e.common.handle = new_handle,
EntityType::Spline(s) => s.common.handle = new_handle,
EntityType::LwPolyline(p) => p.common.handle = new_handle,
_ => {}
}
if let Some(pos) = self
.all_entities
.iter()
.position(|e| e.common().handle == old)
{
self.all_entities[pos] = new_entity;
}
self.geos = build_geos(&self.all_entities);
}
}
}
fn on_hover_entity(&mut self, handle: Handle, pt: DVec3) -> Vec<WireModel> {
if handle.is_null() {
return vec![];
}
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle);
match entity {
Some(EntityType::Line(l)) => {
let ax = l.start.x;
let ay = l.start.y;
let bx = l.end.x;
let by = l.end.y;
let dx = bx - ax;
let dy = by - ay;
let len2 = dx * dx + dy * dy;
let t_click = if len2 > 1e-12 {
((pt.x as f64 - ax) * dx + (pt.y as f64 - ay) * dy) / len2
} else {
0.5
};
if let Some(ext) = extend_line(l, t_click, &self.geos) {
return vec![WireModel::solid(
"extend_prev".into(),
entity_pts(&ext),
WireModel::CYAN,
false,
)];
}
}
Some(EntityType::Ellipse(e)) => {
let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt();
if a >= 1e-9 {
let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a);
let t0 = e.start_parameter;
let mut t1 = e.end_parameter;
if t1 <= t0 {
t1 += TAU;
}
let rx = pt.x as f64 - e.center.x;
let ry = pt.y as f64 - e.center.y;
let xl = rx * nx + ry * ny;
let yl = -rx * ny + ry * nx;
let t_click = arc_t(yl.atan2(xl), t0, t1);
if let Some(ext) = extend_ellipse(e, t_click, &self.geos) {
return vec![WireModel::solid(
"extend_prev".into(),
entity_pts(&ext),
WireModel::CYAN,
false,
)];
}
}
}
Some(EntityType::LwPolyline(p)) => {
if let Some(ext) = extend_lwpoly(p, pt.x as f64, pt.y as f64, &self.geos) {
return vec![WireModel::solid(
"extend_prev".into(),
entity_pts(&ext),
WireModel::CYAN,
false,
)];
}
}
Some(EntityType::Spline(s)) => {
let t_click = spline_nearest_t(s, pt.x as f64, pt.y as f64)
.and_then(|t_actual| {
let bs = spline_to_bspline(s)?;
let (t0, t1) = bs.range_tuple();
Some(t_to_rel(t_actual, t0, t1))
})
.unwrap_or(0.5);
if let Some(ext) = extend_spline(s, t_click, &self.geos) {
return vec![WireModel::solid(
"extend_prev".into(),
entity_pts(&ext),
WireModel::CYAN,
false,
)];
}
}
_ => {}
}
vec![]
}
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["EXTEND"] }); // ExtendCommand
inventory::submit!(crate::command::CommandRegistration { names: &["TRIM"] }); // TrimCommand
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn circle(r: f64) -> CircleEnt {
let mut c = CircleEnt::new();
c.center = Vector3::new(0.0, 0.0, 0.0);
c.radius = r;
c
}
/// A circle crossed by a horizontal cutter (cuts at angle 0 and π, i.e.
/// t = 0.0 and 0.5) becomes a single Arc; clicking the top half removes it
/// and leaves the bottom half (π → 0 CCW), clicking the bottom does the
/// reverse.
#[test]
fn trims_circle_into_arc_on_clicked_half() {
let c = circle(10.0);
let ts = [0.0, 0.5];
// Click top (t = 0.25) → removes top, keeps bottom half (start π, end 0).
let top = trim_circle(&c, &ts, 0.25);
assert_eq!(top.len(), 1, "circle should trim to exactly one arc");
match &top[0] {
EntityType::Arc(a) => {
assert!((norm(a.start_angle) - PI).abs() < 1e-9);
assert!(norm(a.end_angle).abs() < 1e-9);
assert_eq!(a.radius, 10.0);
assert!(a.common.handle.is_null());
}
_ => panic!("expected an Arc"),
}
// Click bottom (t = 0.75) → keeps top half (start 0, end π).
let bottom = trim_circle(&c, &ts, 0.75);
match &bottom[0] {
EntityType::Arc(a) => {
assert!(norm(a.start_angle).abs() < 1e-9);
assert!((norm(a.end_angle) - PI).abs() < 1e-9);
}
_ => panic!("expected an Arc"),
}
}
/// Fewer than two crossings can't cut a closed circle, so it is left as-is.
#[test]
fn circle_with_one_or_zero_cuts_is_left_unchanged() {
let c = circle(5.0);
assert!(trim_circle(&c, &[], 0.3).is_empty());
assert!(trim_circle(&c, &[0.4], 0.3).is_empty());
}
}
// ══════════════════════════════════════════════════════════════════════════
// ExtrimCommand — EXTRIM (Express-Tools cookie-cutter trim). #253
//
// Pick one boundary, then a side: every object crossing the boundary is trimmed
// on the picked side, and objects lying wholly on that side are erased. The side
// test is a parity count — a segment from a candidate point to the pick point
// that crosses the boundary an even number of times lands on the pick side.
// ══════════════════════════════════════════════════════════════════════════
/// Extend `Geo::Line` boundary edges so a line boundary cuts across the whole
/// drawing (EXTRIM treats a line boundary as infinite).
fn extend_line_geos(geos: &mut [Geo]) {
for g in geos.iter_mut() {
if let Geo::Line { p1, p2, .. } = g {
let (dx, dy) = (p2[0] - p1[0], p2[1] - p1[1]);
let len = dx.hypot(dy);
if len > 1e-9 {
let (ux, uy) = (dx / len, dy / len);
let mid = [(p1[0] + p2[0]) * 0.5, (p1[1] + p2[1]) * 0.5];
*p1 = [mid[0] - ux * TRIM_EXTENT, mid[1] - uy * TRIM_EXTENT];
*p2 = [mid[0] + ux * TRIM_EXTENT, mid[1] + uy * TRIM_EXTENT];
}
}
}
}
/// Kept parametric intervals — those whose midpoint is NOT on the pick side.
fn extrim_keep(
ts: &[f64],
point_at: &dyn Fn(f64) -> [f64; 2],
on_pick_side: &dyn Fn([f64; 2]) -> bool,
) -> Vec<(f64, f64)> {
let mut bounds = vec![0.0f64];
bounds.extend_from_slice(ts);
bounds.push(1.0);
bounds.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
bounds
.windows(2)
.filter_map(|w| {
if w[1] - w[0] <= 1e-6 {
return None;
}
let mid = point_at((w[0] + w[1]) * 0.5);
if on_pick_side(mid) {
None
} else {
Some((w[0], w[1]))
}
})
.collect()
}
fn extrim_line(orig: &LineEnt, ts: &[f64], side: &dyn Fn([f64; 2]) -> bool) -> Vec<EntityType> {
let p1 = [orig.start.x, orig.start.y];
let p2 = [orig.end.x, orig.end.y];
let z = orig.start.z;
let pa = |t: f64| lerp2(p1, p2, t);
extrim_keep(ts, &pa, side)
.into_iter()
.filter_map(|(ta, tb)| {
let a = lerp2(p1, p2, ta);
let b = lerp2(p1, p2, tb);
if (b[0] - a[0]).hypot(b[1] - a[1]) < 1e-6 {
return None;
}
let mut l = orig.clone();
l.common.handle = Handle::NULL;
l.start = Vector3::new(a[0], a[1], z);
l.end = Vector3::new(b[0], b[1], z);
Some(EntityType::Line(l))
})
.collect()
}
fn extrim_arc(orig: &ArcEnt, ts: &[f64], side: &dyn Fn([f64; 2]) -> bool) -> Vec<EntityType> {
let a0 = orig.start_angle;
let a1 = orig.end_angle;
let span = {
let s = norm(a1) - norm(a0);
if s <= 0.0 {
s + TAU
} else {
s
}
};
let angle_at = |t: f64| norm(a0) + span * t;
let (cx, cy, r) = (orig.center.x, orig.center.y, orig.radius);
let pt = |t: f64| {
let a = angle_at(t);
[cx + r * a.cos(), cy + r * a.sin()]
};
extrim_keep(ts, &pt, side)
.into_iter()
.filter_map(|(ta, tb)| {
if (tb - ta).abs() < 1e-6 {
return None;
}
let mut a = orig.clone();
a.common.handle = Handle::NULL;
a.start_angle = angle_at(ta);
a.end_angle = angle_at(tb);
Some(EntityType::Arc(a))
})
.collect()
}
fn extrim_circle(orig: &CircleEnt, ts: &[f64], side: &dyn Fn([f64; 2]) -> bool) -> Vec<EntityType> {
if ts.len() < 2 {
return vec![];
}
let (cx, cy, r) = (orig.center.x, orig.center.y, orig.radius);
let mut s = ts.to_vec();
s.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = s.len();
let mut out = Vec::new();
for i in 0..n {
let ta = s[i];
let tb = if i + 1 < n { s[i + 1] } else { s[0] + 1.0 };
let mt = ((ta + tb) * 0.5).rem_euclid(1.0) * TAU;
let mid = [cx + r * mt.cos(), cy + r * mt.sin()];
if side(mid) {
continue; // removed side
}
let mut arc = ArcEnt::new();
arc.common = orig.common.clone();
arc.common.handle = Handle::NULL;
arc.center = orig.center;
arc.radius = orig.radius;
arc.thickness = orig.thickness;
arc.normal = orig.normal;
arc.start_angle = ta.rem_euclid(1.0) * TAU;
arc.end_angle = tb.rem_euclid(1.0) * TAU;
out.push(EntityType::Arc(arc));
}
out
}
/// Sample any entity to a dense XY polyline for the sampled trim path.
fn sample_entity_xy(e: &EntityType) -> Vec<[f64; 2]> {
match e {
EntityType::Line(l) => vec![[l.start.x, l.start.y], [l.end.x, l.end.y]],
EntityType::Arc(a) => {
let span = {
let s = norm(a.end_angle) - norm(a.start_angle);
if s <= 0.0 {
s + TAU
} else {
s
}
};
let steps = (span.abs() * 16.0).ceil().max(4.0) as usize;
(0..=steps)
.map(|i| {
let ang = norm(a.start_angle) + span * (i as f64 / steps as f64);
[
a.center.x + a.radius * ang.cos(),
a.center.y + a.radius * ang.sin(),
]
})
.collect()
}
EntityType::LwPolyline(_)
| EntityType::Polyline(_)
| EntityType::Polyline2D(_)
| EntityType::Polyline3D(_) => {
let mut pts: Vec<[f64; 2]> = Vec::new();
for seg in crate::modules::draw::modify::explode::explode_polyline_segments(e) {
let sp = sample_entity_xy(&seg);
if pts.last() == sp.first() {
pts.extend_from_slice(&sp[1..]);
} else {
pts.extend(sp);
}
}
pts
}
EntityType::Ellipse(el) => {
let mx = el.major_axis.x;
let my = el.major_axis.y;
let a = (mx * mx + my * my).sqrt();
if a < 1e-9 {
return vec![];
}
let (nx, ny) = (mx / a, my / a);
let b = a * el.minor_axis_ratio;
let t0 = el.start_parameter;
let mut t1 = el.end_parameter;
if t1 <= t0 {
t1 += TAU;
}
ellipse_pts(el.center.x, el.center.y, a, b, nx, ny, t0, t1, el.center.z)
.into_iter()
.map(|p| [p[0] as f64, p[1] as f64])
.collect()
}
EntityType::Spline(s) => {
let (_, pts) = spline_sample_xy(s, 96);
pts.into_iter().map(|p| [p[0], p[1]]).collect()
}
_ => vec![],
}
}
/// Dense XY sampling of any entity for the removal preview (lines are
/// subdivided and circles closed so the preview cut follows the boundary).
fn preview_sample_xy(e: &EntityType) -> Vec<[f64; 2]> {
match e {
EntityType::Line(l) => {
let p1 = [l.start.x, l.start.y];
let p2 = [l.end.x, l.end.y];
(0..=24).map(|i| lerp2(p1, p2, i as f64 / 24.0)).collect()
}
EntityType::Circle(c) => {
let steps = 64usize;
(0..=steps)
.map(|i| {
let a = TAU * (i as f64 / steps as f64);
[c.center.x + c.radius * a.cos(), c.center.y + c.radius * a.sin()]
})
.collect()
}
_ => sample_entity_xy(e),
}
}
/// Append the sub-runs of `pts` for which `take` holds to `out` as one wire's
/// point list, separated from earlier content by a NaN pen-up.
fn collect_runs(pts: &[[f64; 2]], take: &dyn Fn([f64; 2]) -> bool, out: &mut Vec<[f32; 3]>) {
let mut run: Vec<[f64; 2]> = Vec::new();
let flush = |run: &mut Vec<[f64; 2]>, out: &mut Vec<[f32; 3]>| {
if run.len() >= 2 {
if !out.is_empty() {
out.push([f32::NAN, f32::NAN, f32::NAN]);
}
out.extend(run.iter().map(|p| [p[0] as f32, p[1] as f32, 0.0]));
}
run.clear();
};
for &p in pts {
if take(p) {
run.push(p);
} else {
flush(&mut run, out);
}
}
flush(&mut run, out);
}
/// Build a preview wire from a NaN-break point list.
fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel {
WireModel {
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: name.into(),
points,
points_low: Vec::new(),
color,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
}
}
/// Sampled trim: `None` leaves the entity unchanged, `Some(vec![])` erases it,
/// `Some(runs)` replaces it with the surviving pieces as LwPolylines.
fn extrim_sampled(pts: &[[f64; 2]], side: &dyn Fn([f64; 2]) -> bool) -> Option<Vec<EntityType>> {
if pts.len() < 2 {
return None;
}
let any_removed = pts.iter().any(|p| side(*p));
if !any_removed {
return None; // wholly on the kept side — untouched
}
let any_kept = pts.iter().any(|p| !side(*p));
if !any_kept {
return Some(vec![]); // wholly on the pick side — erased
}
let mut runs: Vec<Vec<[f64; 2]>> = Vec::new();
let mut cur: Vec<[f64; 2]> = Vec::new();
for &p in pts {
if side(p) {
if cur.len() >= 2 {
runs.push(std::mem::take(&mut cur));
} else {
cur.clear();
}
} else {
cur.push(p);
}
}
if cur.len() >= 2 {
runs.push(cur);
}
Some(
runs.into_iter()
.map(|run| {
let mut pl = LwPolyline::new();
pl.common.handle = Handle::NULL;
pl.is_closed = false;
pl.vertices = run
.into_iter()
.map(|p| LwVertex::from_coords(p[0], p[1]))
.collect();
EntityType::LwPolyline(pl)
})
.collect(),
)
}
pub struct ExtrimCommand {
all: Vec<(Handle, EntityType)>,
boundary: Option<Handle>,
geos: Vec<Geo>,
}
impl ExtrimCommand {
pub fn new(all: Vec<(Handle, EntityType)>) -> Self {
Self { all, boundary: None, geos: Vec::new() }
}
}
impl CadCommand for ExtrimCommand {
fn name(&self) -> &'static str {
"EXTRIM"
}
fn prompt(&self) -> String {
if self.boundary.is_none() {
"EXTRIM Select cutting boundary:".into()
} else {
"EXTRIM Click the side to trim away:".into()
}
}
fn needs_entity_pick(&self) -> bool {
self.boundary.is_none()
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
let Some((_, e)) = self.all.iter().find(|(h, _)| *h == handle) else {
return CmdResult::NeedPoint;
};
let mut geos = build_geos(std::slice::from_ref(e));
extend_line_geos(&mut geos);
if geos.is_empty() {
return CmdResult::NeedPoint; // not a usable boundary; keep asking
}
self.boundary = Some(handle);
self.geos = geos;
CmdResult::NeedPoint
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
let Some(bh) = self.boundary else {
return CmdResult::NeedPoint;
};
let q = [pt.x, pt.y];
let geos = self.geos.clone();
let side =
|m: [f64; 2]| line_seg_ts(m[0], m[1], q[0], q[1], Handle::NULL, &geos).len() % 2 == 0;
let mut repl: Vec<(Handle, Vec<EntityType>)> = Vec::new();
for (h, e) in &self.all {
if *h == bh {
continue;
}
match e {
EntityType::Line(l) => {
let ts = line_seg_ts(l.start.x, l.start.y, l.end.x, l.end.y, *h, &geos);
if ts.is_empty() {
let mid = [(l.start.x + l.end.x) * 0.5, (l.start.y + l.end.y) * 0.5];
if side(mid) {
repl.push((*h, vec![]));
}
} else {
repl.push((*h, extrim_line(l, &ts, &side)));
}
}
EntityType::Arc(a) => {
let ts = arc_seg_ts(
a.center.x,
a.center.y,
a.radius,
a.start_angle,
a.end_angle,
*h,
&geos,
);
if ts.is_empty() {
let am = norm(a.start_angle);
let mid =
[a.center.x + a.radius * am.cos(), a.center.y + a.radius * am.sin()];
if side(mid) {
repl.push((*h, vec![]));
}
} else {
repl.push((*h, extrim_arc(a, &ts, &side)));
}
}
EntityType::Circle(c) => {
let ts = arc_seg_ts(c.center.x, c.center.y, c.radius, 0.0, TAU, *h, &geos);
if ts.len() < 2 {
if side([c.center.x, c.center.y]) {
repl.push((*h, vec![]));
}
} else {
repl.push((*h, extrim_circle(c, &ts, &side)));
}
}
EntityType::LwPolyline(_)
| EntityType::Polyline(_)
| EntityType::Polyline2D(_)
| EntityType::Polyline3D(_)
| EntityType::Ellipse(_)
| EntityType::Spline(_) => {
let pts = sample_entity_xy(e);
if let Some(res) = extrim_sampled(&pts, &side) {
repl.push((*h, res));
}
}
_ => {}
}
}
if repl.is_empty() {
return CmdResult::Cancel;
}
CmdResult::ReplaceMany(repl, Vec::new())
}
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let Some(bh) = self.boundary else {
return Vec::new();
};
if self.geos.is_empty() {
return Vec::new();
}
let q = [pt.x, pt.y];
let geos = &self.geos;
let side =
|m: [f64; 2]| line_seg_ts(m[0], m[1], q[0], q[1], Handle::NULL, geos).len() % 2 == 0;
// Removed (pick side) → red, surviving → blue; the boundary → yellow.
let mut removed: Vec<[f32; 3]> = Vec::new();
let mut kept: Vec<[f32; 3]> = Vec::new();
for (h, e) in &self.all {
if *h == bh {
continue;
}
let pts = preview_sample_xy(e);
if pts.len() < 2 {
continue;
}
collect_runs(&pts, &side, &mut removed);
collect_runs(&pts, &|p| !side(p), &mut kept);
}
let mut boundary_pts: Vec<[f32; 3]> = Vec::new();
if let Some((_, be)) = self.all.iter().find(|(h, _)| *h == bh) {
boundary_pts = preview_sample_xy(be)
.into_iter()
.map(|p| [p[0] as f32, p[1] as f32, 0.0])
.collect();
}
const YELLOW: [f32; 4] = [1.0, 0.90, 0.15, 1.0];
const REMOVE_RED: [f32; 4] = [0.95, 0.30, 0.30, 1.0];
let mut out = Vec::new();
if boundary_pts.len() >= 2 {
out.push(preview_wire(boundary_pts, YELLOW, "extrim_boundary"));
}
if kept.len() >= 2 {
out.push(preview_wire(kept, WireModel::SELECTED, "extrim_keep"));
}
if removed.len() >= 2 {
out.push(preview_wire(removed, REMOVE_RED, "extrim_remove"));
}
out
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
inventory::submit!(crate::command::CommandRegistration {
names: &["EXTRIM"]
}); // ExtrimCommand