// 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;

use super::entity_index::ModifyEntityIndex;

// ── 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])>,
    },
}

impl Geo {
    fn handle(&self) -> Handle {
        match self {
            Self::Line { handle, .. }
            | Self::Arc { handle, .. }
            | Self::Circle { handle, .. }
            | Self::Ray { handle, .. }
            | Self::InfLine { handle, .. }
            | Self::Ellipse { handle, .. }
            | Self::Spline { handle, .. } => *handle,
        }
    }
}

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| {
                        // `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
}

/// 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()
}

/// Extend a clicked Arc end along its circle to the nearest boundary
/// crossing (#409). `t_click` ∈ [0,1] within the current span picks the end:
/// ≥ 0.5 extends the end angle CCW, < 0.5 extends the start angle CW.
fn extend_arc(orig: &ArcEnt, t_click: f64, geos: &[Geo]) -> Option<EntityType> {
    let cx = orig.center.x;
    let cy = orig.center.y;
    let r = orig.radius;
    if r < 1e-9 {
        return None;
    }
    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
        }
    };
    if span >= TAU - 1e-9 {
        return None; // already a full circle
    }
    // Boundary crossings around the FULL circle, then re-expressed relative
    // to the arc's own span: t ∈ [0,1] lies on the arc, t > 1 walks CCW past
    // the end and wraps around toward the start.
    let ts: Vec<f64> = arc_seg_ts(cx, cy, r, a0, a0 + TAU, orig.common.handle, geos)
        .into_iter()
        .map(|t| t * TAU / span)
        .collect();
    let extend_end = t_click >= 0.5;
    let best = if extend_end {
        // First crossing CCW past the end.
        ts.iter()
            .copied()
            .filter(|&t| t > 1.0 + 1e-6)
            .min_by(|x, y| x.partial_cmp(y).unwrap())
    } else {
        // First crossing CW before the start — in wrapped span-relative
        // coordinates that is the LARGEST t outside the arc.
        ts.iter()
            .copied()
            .filter(|&t| t > 1.0 + 1e-6)
            .max_by(|x, y| x.partial_cmp(y).unwrap())
    }?;
    let ang = norm(a0) + span * best;
    let mut a = orig.clone();
    a.common.handle = Handle::NULL;
    if extend_end {
        a.end_angle = ang;
    } else {
        a.start_angle = ang;
    }
    Some(EntityType::Arc(a))
}

/// 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
// ══════════════════════════════════════════════════════════════════════════


// ── TRIM / EXTEND option machinery (#336) ─────────────────────────────────

/// Sub-mode of the TRIM / EXTEND commands (#336). `Pick` is the quick mode;
/// the others are entered through the option keywords.
enum TrimMode {
    Pick,
    /// Collecting cutting/boundary edges; Enter returns to Pick.
    SelectEdges,
    /// Collecting fence points; Enter runs the fence pass.
    Fence(Vec<[f64; 2]>),
    /// Crossing: waiting for the first rectangle corner.
    CrossFirst,
    /// Crossing: first corner picked, waiting for the second.
    CrossSecond([f64; 2]),
    /// Erase mode: each pick deletes the object; Enter returns to Pick.
    Erase,
}

/// Quick-mode trim at a click/crossing point: the surviving pieces, or `None`
/// when the pick doesn't intersect any boundary (or the type is unsupported).
fn pick_trim_at(
    all: &[EntityType],
    geos: &[Geo],
    handle: Handle,
    px: f64,
    py: f64,
) -> Option<Vec<EntityType>> {
    let entity = all.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, geos);
                if ts.is_empty() {
                    return None;
                }
                let dx = bx - ax;
                let dy = by - ay;
                let len2 = dx * dx + dy * dy;
                let t_click = if len2 > 1e-12 {
                    ((px - ax) * dx + (py - 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, geos);
                if ts.is_empty() {
                    return None;
                }
                let click_angle = (py - cy).atan2(px - 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, geos);
                if ts.len() < 2 {
                    return None;
                }
                let click_angle = (py - cy).atan2(px - cx);
                let t_click = arc_t(click_angle, 0.0, TAU);
                let survivors = trim_circle(c, &ts, t_click);
                if survivors.is_empty() {
                    return None;
                }
                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, geos);
                if ts.is_empty() {
                    return None;
                }
                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 {
                    ((px - bx) * dx + (py - 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, geos);
                if ts.is_empty() {
                    return None;
                }
                let dx = ex - bx;
                let dy = ey - by;
                let len2 = dx * dx + dy * dy;
                let t_click = if len2 > 1e-12 {
                    ((px - bx) * dx + (py - 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 None;
                }
                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, geos,
                );
                if ts.is_empty() {
                    return None;
                }
                // t_click: project mouse onto ellipse local param
                let rx = px - e.center.x;
                let ry = py - 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, geos);
                if ts.is_empty() {
                    return None;
                }
                let t_click = spline_nearest_t(s, px, py)
                    .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)) => trim_lwpolyline(p, px, py, geos),
            _ => None,
        };
    result
}

/// Quick-mode extend at a click/crossing point: the lengthened entity, or
/// `None` when no boundary lies beyond that end (or the type is unsupported).
fn pick_extend_at(
    all: &[EntityType],
    geos: &[Geo],
    handle: Handle,
    px: f64,
    py: f64,
) -> Option<EntityType> {
    let entity = all.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 {
                    ((px - ax) * dx + (py - ay) * dy) / len2
                } else {
                    0.5
                };
                extend_line(l, t_click, geos)
            }
            Some(EntityType::Arc(a)) => {
                let ang = (py - a.center.y).atan2(px - a.center.x);
                let t_click = arc_t(ang, a.start_angle, a.end_angle);
                extend_arc(a, t_click, 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 None;
                }
                let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a);
                let rx = px - e.center.x;
                let ry = py - 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, geos)
            }
            Some(EntityType::LwPolyline(p)) => {
                extend_lwpoly(p, px, py, geos)
            }
            Some(EntityType::Spline(s)) => {
                let t_click = spline_nearest_t(s, px, py)
                    .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, geos)
            }
            _ => None,
        };
    result
}

/// Edge option (#336): treat boundary edges as implied — straight edges run
/// on past their endpoints and arcs close to full circles, so objects trim /
/// extend to the boundary's extrapolation, not only its drawn extent.
fn imply_edge_geos(geos: &mut [Geo]) {
    for g in geos.iter_mut() {
        match g {
            Geo::Line { p1, p2, .. } => {
                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);
                    *p1 = [p1[0] - ux * TRIM_EXTENT, p1[1] - uy * TRIM_EXTENT];
                    *p2 = [p2[0] + ux * TRIM_EXTENT, p2[1] + uy * TRIM_EXTENT];
                }
            }
            Geo::Arc { a0, a1, .. } => {
                *a0 = 0.0;
                *a1 = TAU;
            }
            _ => {}
        }
    }
}

/// Dense XY sampling for the fence pass — covers the types the quick pick
/// supports (adds Circle / Ray / XLine over `sample_entity_xy`).
fn fence_sample_xy(e: &EntityType) -> Vec<[f64; 2]> {
    match e {
        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()
        }
        EntityType::Ray(r) => vec![
            [r.base_point.x, r.base_point.y],
            [
                r.base_point.x + r.direction.x * TRIM_EXTENT,
                r.base_point.y + r.direction.y * TRIM_EXTENT,
            ],
        ],
        EntityType::XLine(x) => vec![
            [
                x.base_point.x - x.direction.x * TRIM_EXTENT,
                x.base_point.y - x.direction.y * TRIM_EXTENT,
            ],
            [
                x.base_point.x + x.direction.x * TRIM_EXTENT,
                x.base_point.y + x.direction.y * TRIM_EXTENT,
            ],
        ],
        _ => sample_entity_xy(e),
    }
}

/// Every crossing point of `e` with the fence polyline, in traversal order.
/// Each drives the same per-type pick as a direct click, so the actual cuts
/// happen at the exact boundary intersections.
fn fence_cross_points(e: &EntityType, fence_geos: &[Geo]) -> Vec<[f64; 2]> {
    let pts = fence_sample_xy(e);
    let mut out = Vec::new();
    for w in pts.windows(2) {
        // NOTE: the exclusion handle must differ from the fence geos' own —
        // passing NULL against NULL-handle geos excluded the whole fence.
        let mut ts = line_seg_ts(w[0][0], w[0][1], w[1][0], w[1][1], Handle::new(FENCE_PROBE), fence_geos);
        ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
        for t in ts {
            out.push(lerp2(w[0], w[1], t));
        }
    }
    out
}

/// Trim/extend one entity at EVERY fence crossing: the pick is re-applied to
/// the surviving pieces until no crossing bites (a fence across both
/// overhanging ends of one line clears both). Returns `None` when nothing
/// changed. Pieces carry NULL handles for the ReplaceMany placeholder flow.
fn fence_pieces(
    e: &EntityType,
    geos: &[Geo],
    fence_geos: &[Geo],
    window: Option<[[f64; 2]; 2]>,
    extend: bool,
) -> Option<Vec<EntityType>> {
    // Sentinel handle: keeps the self-exclusion in *_seg_ts from matching any
    // real boundary geo while the piece is being re-picked.
    const TMP: u64 = u64::MAX - 7;
    let mut pieces = vec![e.clone()];
    let mut changed = false;
    for _round in 0..8 {
        let mut next: Vec<EntityType> = Vec::new();
        let mut any = false;
        for piece in &pieces {
            let mut tmp = piece.clone();
            tmp.as_entity_mut().set_handle(Handle::new(TMP));
            let tmp_all = [tmp];
            let mut consumed = false;
            let mut cps = fence_cross_points(&tmp_all[0], fence_geos);
            // Crossing works like a crossing SELECTION: an object wholly
            // inside the window is picked too — synthesize the click at a
            // sampled point inside it.
            if cps.is_empty() {
                if let Some([min, max]) = window {
                    if let Some(p) = fence_sample_xy(&tmp_all[0]).into_iter().find(|p| {
                        p[0] >= min[0] && p[0] <= max[0] && p[1] >= min[1] && p[1] <= max[1]
                    }) {
                        cps.push(p);
                    }
                }
            }
            for cp in cps {
                let res = if extend {
                    pick_extend_at(&tmp_all, geos, Handle::new(TMP), cp[0], cp[1])
                        .map(|x| vec![x])
                } else {
                    pick_trim_at(&tmp_all, geos, Handle::new(TMP), cp[0], cp[1])
                };
                if let Some(mut sub) = res {
                    for sp in &mut sub {
                        sp.as_entity_mut().set_handle(Handle::NULL);
                    }
                    next.extend(sub);
                    any = true;
                    changed = true;
                    consumed = true;
                    break;
                }
            }
            if !consumed {
                next.push(piece.clone());
            }
        }
        pieces = next;
        if !any || pieces.len() > 64 {
            break;
        }
    }
    if changed { Some(pieces) } else { None }
}

/// Fence / Crossing pass (#336): every object crossing the fence polyline is
/// trimmed (or extended) at each of its crossing points; the cuts land on the
/// boundary-edge intersections, the fence only selects.
/// Boundary/edge-selection highlight colour (the EXTRIM boundary yellow).
const OPT_YELLOW: [f32; 4] = [1.0, 0.90, 0.15, 1.0];

/// Sentinel handles for the fence pass: the fence's own geos and the probe
/// handle used against them must never collide with a real entity handle (or
/// with each other — `line_seg_ts` drops geos matching the probe handle).
const FENCE_GEO: u64 = u64::MAX - 8;
const FENCE_PROBE: u64 = u64::MAX - 9;

fn build_fence_geos(fence: &[[f64; 2]]) -> Vec<Geo> {
    fence
        .windows(2)
        .map(|w| Geo::Line {
            handle: Handle::new(FENCE_GEO),
            p1: w[0],
            p2: w[1],
        })
        .collect()
}

/// Live result preview of a fence / crossing pass: each affected original in
/// red, its surviving pieces in cyan — the same convention as the pick hover.
fn fence_result_preview(
    all: &[EntityType],
    geos: &[Geo],
    fence: &[[f64; 2]],
    window: Option<[[f64; 2]; 2]>,
    extend: bool,
    implied_edges: bool,
) -> Vec<WireModel> {
    let fence_geos = build_fence_geos(fence);
    let mut out = Vec::new();
    for e in all {
        if e.common().handle.is_null() {
            continue;
        }
        if let Some(pieces) = fence_pieces(e, geos, &fence_geos, window, extend) {
            out.push(WireModel::solid(
                "trim_rm".into(),
                entity_pts(e),
                DIM_RED,
                false,
            ));
            for (i, pe) in pieces.iter().enumerate() {
                out.push(WireModel::solid(
                    format!("trim_keep_{i}"),
                    entity_pts(pe),
                    WireModel::CYAN,
                    false,
                ));
                if extend {
                    if let Some(t) = extend_tail_preview(e, pe, "extend_tail") {
                        out.push(t);
                    }
                }
            }
            if implied_edges {
                let cuts = piece_cut_points(e, &pieces);
                out.extend(implied_cut_guides(all, e.common().handle, &cuts));
            }
        }
    }
    out
}

fn fence_pass(
    all: &[EntityType],
    geos: &[Geo],
    fence: &[[f64; 2]],
    window: Option<[[f64; 2]; 2]>,
    extend: bool,
) -> Vec<(Handle, Vec<EntityType>)> {
    let fence_geos: Vec<Geo> = build_fence_geos(fence);
    let mut out = Vec::new();
    for e in all {
        let h = e.common().handle;
        if h.is_null() {
            continue;
        }
        if let Some(pieces) = fence_pieces(e, geos, &fence_geos, window, extend) {
            out.push((h, pieces));
        }
    }
    out
}

/// The ADDED tail of an extend result, dashed — the slice of the extended
/// curve beyond the original endpoint, so the preview shows the extension
/// itself rather than only a recolored whole (#336). Generic over entity
/// type via the sampled points.
fn extend_tail_preview(orig: &EntityType, ext: &EntityType, name: &str) -> Option<WireModel> {
    let op = entity_pts(orig);
    let ep = entity_pts(ext);
    if op.len() < 2 || ep.len() < 2 {
        return None;
    }
    let d2 = |a: &[f32; 3], b: &[f32; 3]| {
        let dx = a[0] - b[0];
        let dy = a[1] - b[1];
        dx * dx + dy * dy
    };
    let nearest_idx = |pts: &[[f32; 3]], q: &[f32; 3]| {
        let mut best = 0usize;
        let mut bd = f32::MAX;
        for (i, p) in pts.iter().enumerate() {
            let d = d2(p, q);
            if d < bd {
                bd = d;
                best = i;
            }
        }
        best
    };
    let of = op.first().unwrap();
    let ol = op.last().unwrap();
    let ef = ep.first().unwrap();
    let el = ep.last().unwrap();
    let tail: Vec<[f32; 3]> = if d2(ol, el) > 1e-10 {
        let i = nearest_idx(&ep, ol);
        ep[i..].to_vec()
    } else if d2(of, ef) > 1e-10 {
        let i = nearest_idx(&ep, of);
        ep[..=i].to_vec()
    } else {
        return None;
    };
    if tail.len() < 2 {
        return None;
    }
    let mut w = WireModel::solid(name.into(), tail, [0.3, 1.0, 1.0, 1.0], false);
    w.pattern_length = 0.8;
    w.pattern = [0.5, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
    w.line_weight_px = 1.6;
    Some(w)
}

/// Distance from `q` to the drawn (sampled) body of `e`.
fn dist_to_drawn(e: &EntityType, q: [f64; 2]) -> f64 {
    let pts = fence_sample_xy(e);
    let mut best = f64::MAX;
    for w in pts.windows(2) {
        let (a, b) = (w[0], w[1]);
        let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
        let len2 = dx * dx + dy * dy;
        let t = if len2 > 1e-12 {
            (((q[0] - a[0]) * dx + (q[1] - a[1]) * dy) / len2).clamp(0.0, 1.0)
        } else {
            0.0
        };
        let px = a[0] + dx * t;
        let py = a[1] + dy * t;
        best = best.min((q[0] - px).hypot(q[1] - py));
    }
    best
}

/// Endpoints of the surviving pieces that are NOT endpoints of the original —
/// i.e. the actual cut points of a trim result.
fn piece_cut_points(orig: &EntityType, pieces: &[EntityType]) -> Vec<[f64; 2]> {
    let op = entity_pts(orig);
    if op.len() < 2 {
        return vec![];
    }
    let f = op.first().unwrap();
    let l = op.last().unwrap();
    let closed = (f[0] - l[0]).abs() < 1e-6 && (f[1] - l[1]).abs() < 1e-6;
    let ends: Vec<[f32; 3]> = if closed { vec![] } else { vec![*f, *l] };
    let mut out = Vec::new();
    for pe in pieces {
        let pp = entity_pts(pe);
        if pp.len() < 2 {
            continue;
        }
        for q in [pp.first().unwrap(), pp.last().unwrap()] {
            let is_orig_end = ends.iter().any(|e| {
                let dx = e[0] - q[0];
                let dy = e[1] - q[1];
                dx * dx + dy * dy < 1e-6
            });
            if !is_orig_end {
                out.push([q[0] as f64, q[1] as f64]);
            }
        }
    }
    out
}

/// Edge: Extend guides (#336): for every cut point that does NOT lie on a
/// boundary's drawn body, draw a dashed guide along the boundary's implied
/// extension — from its drawn end to the cut — so the user sees WHICH edge
/// causes the cut there. Lines get a straight guide, arcs follow the circle.
fn implied_cut_guides(
    all: &[EntityType],
    target: Handle,
    cuts: &[[f64; 2]],
) -> Vec<WireModel> {
    let mut out = Vec::new();
    'cuts: for (ci, cp) in cuts.iter().enumerate() {
        let tol = 1e-6 * (1.0 + cp[0].abs() + cp[1].abs());
        // Attributed to a drawn body → nothing implied to explain.
        for e in all {
            let h = e.common().handle;
            if h.is_null() || h == target {
                continue;
            }
            if dist_to_drawn(e, *cp) < tol {
                continue 'cuts;
            }
        }
        // Find the boundary whose extrapolation passes through the cut.
        for e in all {
            let h = e.common().handle;
            if h.is_null() || h == target {
                continue;
            }
            match e {
                EntityType::Line(l) => {
                    let (ax, ay) = (l.start.x, l.start.y);
                    let (dx, dy) = (l.end.x - ax, l.end.y - ay);
                    let len = dx.hypot(dy);
                    if len < 1e-9 {
                        continue;
                    }
                    // Perpendicular distance to the infinite line.
                    let d = ((cp[0] - ax) * dy - (cp[1] - ay) * dx).abs() / len;
                    if d < tol {
                        let d_start = (cp[0] - ax).hypot(cp[1] - ay);
                        let d_end = (cp[0] - l.end.x).hypot(cp[1] - l.end.y);
                        let from = if d_start < d_end {
                            [ax as f32, ay as f32, 0.0]
                        } else {
                            [l.end.x as f32, l.end.y as f32, 0.0]
                        };
                        let mut w = WireModel::solid(
                            format!("edge_guide_{ci}"),
                            vec![from, [cp[0] as f32, cp[1] as f32, 0.0]],
                            OPT_YELLOW,
                            false,
                        );
                        w.pattern_length = 0.8;
                        w.pattern = [0.5, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
                        out.push(w);
                        continue 'cuts;
                    }
                }
                EntityType::Arc(a) => {
                    let r = (cp[0] - a.center.x).hypot(cp[1] - a.center.y);
                    if (r - a.radius).abs() < tol {
                        // Follow the circle from the nearer drawn end to the
                        // cut, going the short way outside the drawn span.
                        let theta = (cp[1] - a.center.y).atan2(cp[0] - a.center.x);
                        let a0 = norm(a.start_angle);
                        let a1 = norm(a.end_angle);
                        let ccw_from_end = (theta - a1).rem_euclid(TAU);
                        let cw_from_start = (a0 - theta).rem_euclid(TAU);
                        let (base, sweep) = if ccw_from_end <= cw_from_start {
                            (a1, ccw_from_end)
                        } else {
                            (a0, -cw_from_start)
                        };
                        let steps = ((sweep.abs() * 16.0).ceil() as usize).max(2);
                        let pts: Vec<[f32; 3]> = (0..=steps)
                            .map(|i| {
                                let ang = base + sweep * (i as f64 / steps as f64);
                                [
                                    (a.center.x + a.radius * ang.cos()) as f32,
                                    (a.center.y + a.radius * ang.sin()) as f32,
                                    0.0,
                                ]
                            })
                            .collect();
                        let mut w = WireModel::solid(
                            format!("edge_guide_{ci}"),
                            pts,
                            OPT_YELLOW,
                            false,
                        );
                        w.pattern_length = 0.8;
                        w.pattern = [0.5, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
                        out.push(w);
                        continue 'cuts;
                    }
                }
                _ => {}
            }
        }
    }
    out
}

/// Extend hover preview: the whole result in cyan, the added tail dashed and
/// — with Edge: Extend on — a guide along the implied boundary that the new
/// endpoint lands on.
fn extend_hover_wires(
    orig: &EntityType,
    ext: &EntityType,
    all: &[EntityType],
    target: Handle,
    implied_edges: bool,
) -> Vec<WireModel> {
    let mut out = vec![WireModel::solid(
        "extend_prev".into(),
        entity_pts(ext),
        WireModel::CYAN,
        false,
    )];
    if let Some(t) = extend_tail_preview(orig, ext, "extend_tail") {
        out.push(t);
    }
    if implied_edges {
        // The moved endpoint is the cut against the (implied) boundary.
        let cuts = piece_cut_points(orig, std::slice::from_ref(ext));
        out.extend(implied_cut_guides(all, target, &cuts));
    }
    out
}

/// Fence preview polyline (committed points + rubber point), dashed.
fn fence_preview_wire(pts: &[[f64; 2]], cursor: [f64; 2], name: &str) -> WireModel {
    let mut wire: Vec<[f32; 3]> = pts.iter().map(|p| [p[0] as f32, p[1] as f32, 0.0]).collect();
    wire.push([cursor[0] as f32, cursor[1] as f32, 0.0]);
    let mut w = WireModel::solid(name.into(), wire, WireModel::CYAN, false);
    w.pattern_length = 0.8;
    w.pattern = [0.5, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
    w
}

/// Crossing preview rectangle — the selection crossing-box look (dashed
/// green outline) so the option reads like a crossing selection (#336).
fn crossing_preview_wire(p1: [f64; 2], cursor: [f64; 2], name: &str) -> WireModel {
    let pts = vec![
        [p1[0] as f32, p1[1] as f32, 0.0],
        [p1[0] as f32, cursor[1] as f32, 0.0],
        [cursor[0] as f32, cursor[1] as f32, 0.0],
        [cursor[0] as f32, p1[1] as f32, 0.0],
        [p1[0] as f32, p1[1] as f32, 0.0],
    ];
    let mut w = WireModel::solid(name.into(), pts, [0.35, 0.85, 0.35, 0.9], false);
    w.pattern_length = 0.8;
    w.pattern = [0.5, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
    w
}

pub struct TrimCommand {
    all_entities: Vec<EntityType>,
    entity_index: ModifyEntityIndex,
    geos: Vec<Geo>,
    mode: TrimMode,
    /// Cutting-edge selection; empty = every object cuts (quick mode).
    edge_set: Vec<Handle>,
    /// Edge option: boundaries extrapolate past their drawn extent.
    implied_edges: bool,
    /// Live Shift state — Shift+click extends instead of trims (#336).
    shift: bool,
}

impl TrimCommand {
    pub fn new(all_entities: Vec<EntityType>) -> Self {
        let entity_index = ModifyEntityIndex::build(&all_entities);
        let geos = build_geos(&all_entities);
        Self {
            all_entities,
            entity_index,
            geos,
            mode: TrimMode::Pick,
            edge_set: Vec::new(),
            implied_edges: false,
            shift: false,
        }
    }

    /// Boundary geometry from the edge selection (or everything when none),
    /// with the Edge option's implied extrapolation applied on top.
    fn rebuild_geos(&mut self) {
        self.entity_index = ModifyEntityIndex::build(&self.all_entities);
        self.geos = if self.edge_set.is_empty() {
            build_geos(&self.all_entities)
        } else {
            let picked: Vec<EntityType> = self
                .all_entities
                .iter()
                .filter(|e| self.edge_set.contains(&e.common().handle))
                .cloned()
                .collect();
            build_geos(&picked)
        };
        if self.implied_edges {
            imply_edge_geos(&mut self.geos);
        }
    }

    /// Exact analytic boundaries whose boxes overlap the picked entity. The
    /// host already narrowed the cursor pick; this second broad phase keeps
    /// TRIM preview/intersection work local on dense drawings.
    fn nearby_geos(&self, handle: Handle) -> Vec<Geo> {
        if self.implied_edges {
            return self.geos.clone();
        }
        let Some(handles) = self.entity_index.nearby_handles(&self.all_entities, handle) else {
            return self.geos.clone();
        };
        self.geos
            .iter()
            .filter(|geo| handles.contains(&geo.handle()))
            .cloned()
            .collect()
    }

    fn fence_run(
        &mut self,
        fence: &[[f64; 2]],
        window: Option<[[f64; 2]; 2]>,
    ) -> CmdResult {
        let repl = fence_pass(&self.all_entities, &self.geos, fence, window, self.shift);
        if repl.is_empty() {
            return CmdResult::NeedPoint;
        }
        CmdResult::ReplaceMany(repl, Vec::new())
    }
}

impl CadCommand for TrimCommand {
    fn name(&self) -> &'static str {
        "TRIM"
    }

    fn prompt(&self) -> String {
        let edge = if self.implied_edges { " [Edge: Extend]" } else { "" };
        match &self.mode {
            TrimMode::Pick => {
                format!("TRIM{edge}  Click segment to remove (Shift+click extends):")
            }
            TrimMode::SelectEdges => format!(
                "TRIM  Select cutting edges [{} picked, Enter = done]:",
                self.edge_set.len()
            ),
            TrimMode::Fence(pts) => format!(
                "TRIM{edge}  Fence: pick points [{} placed, Enter = trim crossed]:",
                pts.len()
            ),
            TrimMode::CrossFirst => format!("TRIM{edge}  Crossing: first corner:"),
            TrimMode::CrossSecond(_) => format!("TRIM{edge}  Crossing: opposite corner:"),
            TrimMode::Erase => "TRIM  Erase: click objects to delete [Enter = done]:".into(),
        }
    }

    fn options(&self) -> Vec<crate::command::CmdOption> {
        use crate::command::CmdOption;
        match self.mode {
            TrimMode::Pick => vec![
                CmdOption::new("Cutting edges", "T"),
                CmdOption::new("Fence", "F"),
                CmdOption::new("Crossing", "C"),
                CmdOption::new(
                    if self.implied_edges { "Edge: Extend" } else { "Edge: No extend" },
                    "E",
                ),
                CmdOption::new("Erase", "R"),
                CmdOption::enter("Done"),
            ],
            _ => vec![CmdOption::enter("Done")],
        }
    }

    fn wants_text_input(&self) -> bool {
        true
    }

    fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
        // Consumed inputs return Some(NeedPoint) — None would be offered to
        // the command a second time by the driver.
        match text.trim().to_uppercase().as_str() {
            "T" | "CUTTING" | "B" | "BOUNDARY" => {
                self.edge_set.clear();
                self.mode = TrimMode::SelectEdges;
                Some(CmdResult::NeedPoint)
            }
            "F" | "FENCE" => {
                self.mode = TrimMode::Fence(Vec::new());
                Some(CmdResult::NeedPoint)
            }
            "C" | "CROSSING" => {
                self.mode = TrimMode::CrossFirst;
                Some(CmdResult::NeedPoint)
            }
            "E" | "EDGE" => {
                self.implied_edges = !self.implied_edges;
                self.rebuild_geos();
                Some(CmdResult::NeedPoint)
            }
            "R" | "ERASE" => {
                self.mode = TrimMode::Erase;
                Some(CmdResult::NeedPoint)
            }
            _ => None,
        }
    }

    fn set_shift(&mut self, shift: bool) {
        self.shift = shift;
    }

    fn needs_entity_pick(&self) -> bool {
        matches!(
            self.mode,
            TrimMode::Pick | TrimMode::SelectEdges | TrimMode::Erase
        )
    }

    fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
        if handle.is_null() {
            return CmdResult::NeedPoint;
        }
        match self.mode {
            TrimMode::SelectEdges => {
                // Toggle membership; the boundary rebuilds on Enter.
                if let Some(pos) = self.edge_set.iter().position(|h| *h == handle) {
                    self.edge_set.remove(pos);
                } else {
                    self.edge_set.push(handle);
                }
                CmdResult::NeedPoint
            }
            TrimMode::Erase => {
                // Erase option: delete without needing an intersection.
                if let Some(pos) = self
                    .all_entities
                    .iter()
                    .position(|e| e.common().handle == handle)
                {
                    self.all_entities.remove(pos);
                }
                self.edge_set.retain(|h| *h != handle);
                self.rebuild_geos();
                CmdResult::ReplaceEntity(handle, vec![])
            }
            _ => {
                let (px, py) = (pt.x, pt.y);
                let new_entities = if self.shift {
                    // Shift+click swaps to Extend for this pick (#336).
                    pick_extend_at(&self.all_entities, &self.geos, handle, px, py)
                        .map(|e| vec![e])
                } else {
                    let geos = self.nearby_geos(handle);
                    pick_trim_at(&self.all_entities, &geos, handle, px, py)
                };
                if let Some(new_entities) = new_entities {
                    // Snapshot is updated in on_entity_replaced once we know
                    // the real handles. Pre-stage: remove the 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);
                        // Pieces join with NULL handles as geometry-only placeholders.
                        self.all_entities.extend(new_entities.clone());
                        self.edge_set.retain(|h| *h != handle);
                        self.rebuild_geos();
                    }
                    CmdResult::ReplaceEntity(handle, new_entities)
                } else {
                    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.rebuild_geos();
    }

    fn on_hover_entity(&mut self, handle: Handle, pt: DVec3) -> Vec<WireModel> {
        match self.mode {
            TrimMode::SelectEdges => {
                // Picked cutting edges stay highlighted; the hovered candidate
                // joins them at half strength.
                let mut out: Vec<WireModel> = self
                    .edge_set
                    .iter()
                    .filter_map(|h| {
                        self.all_entities.iter().find(|e| e.common().handle == *h)
                    })
                    .map(|e| {
                        WireModel::solid("edge_sel".into(), entity_pts(e), OPT_YELLOW, false)
                    })
                    .collect();
                if !handle.is_null() && !self.edge_set.contains(&handle) {
                    if let Some(e) = self
                        .all_entities
                        .iter()
                        .find(|e| e.common().handle == handle)
                    {
                        let mut c = OPT_YELLOW;
                        c[3] = 0.45;
                        out.push(WireModel::solid(
                            "edge_cand".into(),
                            entity_pts(e),
                            c,
                            false,
                        ));
                    }
                }
                return out;
            }
            TrimMode::Erase => {
                // Erase mode: the hovered object previews fully red.
                if let Some(e) = self
                    .all_entities
                    .iter()
                    .find(|e| e.common().handle == handle)
                {
                    return vec![WireModel::solid(
                        "erase_prev".into(),
                        entity_pts(e),
                        DIM_RED,
                        false,
                    )];
                }
                return vec![];
            }
            TrimMode::Pick => {}
            _ => return vec![],
        }
        if self.shift {
            // Shift held: preview the extend result instead.
            if let Some(ext) =
                pick_extend_at(&self.all_entities, &self.geos, handle, pt.x, pt.y)
            {
                if let Some(orig) = self
                    .all_entities
                    .iter()
                    .find(|e| e.common().handle == handle)
                {
                    return extend_hover_wires(
                        orig,
                        &ext,
                        &self.all_entities,
                        handle,
                        self.implied_edges,
                    );
                }
                return vec![WireModel::solid(
                    "extend_prev".into(),
                    entity_pts(&ext),
                    WireModel::CYAN,
                    false,
                )];
            }
            return vec![];
        }
        if handle.is_null() {
            return vec![];
        }

        let nearby_geos = self.nearby_geos(handle);
        let geos = nearby_geos.as_slice();
        let entity = self
            .entity_index.get(&self.all_entities, handle);

        let mut hover_wires = 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, 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, 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, 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, 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, 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, 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, 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, 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![],
        };
        // Edge: Extend — explain cuts landing on an IMPLIED boundary with a
        // dashed guide from that boundary's drawn end to the cut point (#336).
        if self.implied_edges && !hover_wires.is_empty() {
            if let (Some(orig), Some(pieces)) = (
                entity,
                pick_trim_at(&self.all_entities, geos, handle, pt.x, pt.y),
            ) {
                let cuts = piece_cut_points(orig, &pieces);
                hover_wires.extend(implied_cut_guides(&self.all_entities, handle, &cuts));
            }
        }
        hover_wires
    }

    fn on_point(&mut self, pt: DVec3) -> CmdResult {
        match &mut self.mode {
            TrimMode::Fence(pts) => {
                pts.push([pt.x, pt.y]);
                CmdResult::NeedPoint
            }
            TrimMode::CrossFirst => {
                self.mode = TrimMode::CrossSecond([pt.x, pt.y]);
                CmdResult::NeedPoint
            }
            TrimMode::CrossSecond(p1) => {
                // The rectangle's edges act as a 4-segment fence, and objects
                // wholly inside the window are picked too (crossing-selection
                // semantics).
                let p1 = *p1;
                let p2 = [pt.x, pt.y];
                self.mode = TrimMode::Pick;
                let rect = [p1, [p1[0], p2[1]], p2, [p2[0], p1[1]], p1];
                let window = [
                    [p1[0].min(p2[0]), p1[1].min(p2[1])],
                    [p1[0].max(p2[0]), p1[1].max(p2[1])],
                ];
                self.fence_run(&rect, Some(window))
            }
            _ => CmdResult::NeedPoint,
        }
    }

    fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
        match &self.mode {
            TrimMode::Fence(pts) if !pts.is_empty() => {
                let mut out = vec![fence_preview_wire(pts, [pt.x, pt.y], "trim_fence")];
                let mut fpts = pts.clone();
                fpts.push([pt.x, pt.y]);
                out.extend(fence_result_preview(
                    &self.all_entities,
                    &self.geos,
                    &fpts,
                    None,
                    self.shift,
                    self.implied_edges,
                ));
                out
            }
            TrimMode::CrossSecond(p1) => {
                let p1 = *p1;
                let p2 = [pt.x, pt.y];
                let mut out = vec![crossing_preview_wire(p1, p2, "trim_cross")];
                let rect = [p1, [p1[0], p2[1]], p2, [p2[0], p1[1]], p1];
                let window = [
                    [p1[0].min(p2[0]), p1[1].min(p2[1])],
                    [p1[0].max(p2[0]), p1[1].max(p2[1])],
                ];
                out.extend(fence_result_preview(
                    &self.all_entities,
                    &self.geos,
                    &rect,
                    Some(window),
                    self.shift,
                    self.implied_edges,
                ));
                out
            }
            _ => Vec::new(),
        }
    }

    fn on_enter(&mut self) -> CmdResult {
        match std::mem::replace(&mut self.mode, TrimMode::Pick) {
            TrimMode::SelectEdges => {
                self.rebuild_geos();
                CmdResult::NeedPoint
            }
            TrimMode::Fence(pts) if pts.len() >= 2 => self.fence_run(&pts, None),
            TrimMode::Fence(_)
            | TrimMode::CrossFirst
            | TrimMode::CrossSecond(_)
            | TrimMode::Erase => CmdResult::NeedPoint,
            TrimMode::Pick => CmdResult::Cancel,
        }
    }
    fn on_escape(&mut self) -> CmdResult {
        // Esc leaves a sub-mode first; a second Esc ends the command.
        if !matches!(self.mode, TrimMode::Pick) {
            self.mode = TrimMode::Pick;
            return CmdResult::NeedPoint;
        }
        CmdResult::Cancel
    }
}

// ══════════════════════════════════════════════════════════════════════════
// ExtendCommand
// ══════════════════════════════════════════════════════════════════════════

pub struct ExtendCommand {
    all_entities: Vec<EntityType>,
    entity_index: ModifyEntityIndex,
    geos: Vec<Geo>,
    mode: TrimMode,
    /// Boundary-edge selection; empty = every object is a boundary.
    edge_set: Vec<Handle>,
    /// Edge option: boundaries extrapolate past their drawn extent.
    implied_edges: bool,
    /// Live Shift state — Shift+click trims instead of extends (#336).
    shift: bool,
}

impl ExtendCommand {
    pub fn new(all_entities: Vec<EntityType>) -> Self {
        let entity_index = ModifyEntityIndex::build(&all_entities);
        let geos = build_geos(&all_entities);
        Self {
            all_entities,
            entity_index,
            geos,
            mode: TrimMode::Pick,
            edge_set: Vec::new(),
            implied_edges: false,
            shift: false,
        }
    }

    fn rebuild_geos(&mut self) {
        self.entity_index = ModifyEntityIndex::build(&self.all_entities);
        self.geos = if self.edge_set.is_empty() {
            build_geos(&self.all_entities)
        } else {
            let picked: Vec<EntityType> = self
                .all_entities
                .iter()
                .filter(|e| self.edge_set.contains(&e.common().handle))
                .cloned()
                .collect();
            build_geos(&picked)
        };
        if self.implied_edges {
            imply_edge_geos(&mut self.geos);
        }
    }

    fn fence_run(
        &mut self,
        fence: &[[f64; 2]],
        window: Option<[[f64; 2]; 2]>,
    ) -> CmdResult {
        // EXTEND's fence extends; Shift held at Enter swaps it to trim.
        let repl = fence_pass(&self.all_entities, &self.geos, fence, window, !self.shift);
        if repl.is_empty() {
            return CmdResult::NeedPoint;
        }
        CmdResult::ReplaceMany(repl, Vec::new())
    }
}

impl CadCommand for ExtendCommand {
    fn name(&self) -> &'static str {
        "EXTEND"
    }

    fn prompt(&self) -> String {
        let edge = if self.implied_edges { " [Edge: Extend]" } else { "" };
        match &self.mode {
            TrimMode::Pick => format!(
                "EXTEND{edge}  Click near end of object to extend (Shift+click trims):"
            ),
            TrimMode::SelectEdges => format!(
                "EXTEND  Select boundary edges [{} picked, Enter = done]:",
                self.edge_set.len()
            ),
            TrimMode::Fence(pts) => format!(
                "EXTEND{edge}  Fence: pick points [{} placed, Enter = extend crossed]:",
                pts.len()
            ),
            TrimMode::CrossFirst => format!("EXTEND{edge}  Crossing: first corner:"),
            TrimMode::CrossSecond(_) => format!("EXTEND{edge}  Crossing: opposite corner:"),
            TrimMode::Erase => "EXTEND  [Enter = done]:".into(),
        }
    }

    fn options(&self) -> Vec<crate::command::CmdOption> {
        use crate::command::CmdOption;
        match self.mode {
            TrimMode::Pick => vec![
                CmdOption::new("Boundary edges", "B"),
                CmdOption::new("Fence", "F"),
                CmdOption::new("Crossing", "C"),
                CmdOption::new(
                    if self.implied_edges { "Edge: Extend" } else { "Edge: No extend" },
                    "E",
                ),
                CmdOption::enter("Done"),
            ],
            _ => vec![CmdOption::enter("Done")],
        }
    }

    fn wants_text_input(&self) -> bool {
        true
    }

    fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
        match text.trim().to_uppercase().as_str() {
            "B" | "BOUNDARY" | "T" | "CUTTING" => {
                self.edge_set.clear();
                self.mode = TrimMode::SelectEdges;
                Some(CmdResult::NeedPoint)
            }
            "F" | "FENCE" => {
                self.mode = TrimMode::Fence(Vec::new());
                Some(CmdResult::NeedPoint)
            }
            "C" | "CROSSING" => {
                self.mode = TrimMode::CrossFirst;
                Some(CmdResult::NeedPoint)
            }
            "E" | "EDGE" => {
                self.implied_edges = !self.implied_edges;
                self.rebuild_geos();
                Some(CmdResult::NeedPoint)
            }
            _ => None,
        }
    }

    fn set_shift(&mut self, shift: bool) {
        self.shift = shift;
    }

    fn needs_entity_pick(&self) -> bool {
        matches!(self.mode, TrimMode::Pick | TrimMode::SelectEdges)
    }

    fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult {
        if handle.is_null() {
            return CmdResult::NeedPoint;
        }
        match self.mode {
            TrimMode::SelectEdges => {
                if let Some(pos) = self.edge_set.iter().position(|h| *h == handle) {
                    self.edge_set.remove(pos);
                } else {
                    self.edge_set.push(handle);
                }
                CmdResult::NeedPoint
            }
            _ => {
                let (px, py) = (pt.x, pt.y);
                let new_entities = if self.shift {
                    // Shift+click swaps to Trim for this pick (#336).
                    pick_trim_at(&self.all_entities, &self.geos, handle, px, py)
                } else {
                    pick_extend_at(&self.all_entities, &self.geos, handle, px, py)
                        .map(|e| vec![e])
                };
                if let Some(new_entities) = new_entities {
                    // Same snapshot bookkeeping as TRIM: drop the old entry,
                    // append the pieces as NULL-handle placeholders, and let
                    // on_entity_replaced assign the real handles.
                    if let Some(pos) = self
                        .all_entities
                        .iter()
                        .position(|e| e.common().handle == handle)
                    {
                        self.all_entities.remove(pos);
                        self.all_entities.extend(new_entities.clone());
                        self.edge_set.retain(|h| *h != handle);
                        self.rebuild_geos();
                    }
                    CmdResult::ReplaceEntity(handle, new_entities)
                } else {
                    CmdResult::NeedPoint
                }
            }
        }
    }

    fn on_entity_replaced(&mut self, _old: Handle, new_handles: &[acadrust::Handle]) {
        // The last new_handles.len() entries are the pieces appended with NULL
        // handles in on_entity_pick — 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,
                EntityType::LwPolyline(p) => p.common.handle = h,
                _ => {}
            }
        }
        self.rebuild_geos();
    }

    fn on_hover_entity(&mut self, handle: Handle, pt: DVec3) -> Vec<WireModel> {
        match self.mode {
            TrimMode::SelectEdges => {
                let mut out: Vec<WireModel> = self
                    .edge_set
                    .iter()
                    .filter_map(|h| self.entity_index.get(&self.all_entities, *h))
                    .map(|e| {
                        WireModel::solid("edge_sel".into(), entity_pts(e), OPT_YELLOW, false)
                    })
                    .collect();
                if !handle.is_null() && !self.edge_set.contains(&handle) {
                    if let Some(e) = self
                        .entity_index.get(&self.all_entities, handle)
                    {
                        let mut c = OPT_YELLOW;
                        c[3] = 0.45;
                        out.push(WireModel::solid(
                            "edge_cand".into(),
                            entity_pts(e),
                            c,
                            false,
                        ));
                    }
                }
                return out;
            }
            TrimMode::Pick => {}
            _ => return vec![],
        }
        if self.shift {
            // Shift held: preview the trim result instead.
            if let Some(pieces) =
                pick_trim_at(&self.all_entities, &self.geos, handle, pt.x, pt.y)
            {
                let mut out = Vec::new();
                for (i, e) in pieces.iter().enumerate() {
                    out.push(WireModel::solid(
                        format!("trim_keep_{i}"),
                        entity_pts(e),
                        WireModel::CYAN,
                        false,
                    ));
                }
                return out;
            }
            return vec![];
        }
        if handle.is_null() {
            return vec![];
        }

        let entity = self
            .entity_index.get(&self.all_entities, 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 extend_hover_wires(
                        &EntityType::Line(l.clone()),
                        &ext,
                        &self.all_entities,
                        handle,
                        self.implied_edges,
                    );
                }
            }
            Some(EntityType::Arc(a)) => {
                let ang = (pt.y as f64 - a.center.y).atan2(pt.x as f64 - a.center.x);
                let t_click = arc_t(ang, a.start_angle, a.end_angle);
                if let Some(ext) = extend_arc(a, t_click, &self.geos) {
                    return extend_hover_wires(
                        &EntityType::Arc(a.clone()),
                        &ext,
                        &self.all_entities,
                        handle,
                        self.implied_edges,
                    );
                }
            }
            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 extend_hover_wires(
                        &EntityType::Ellipse(e.clone()),
                        &ext,
                        &self.all_entities,
                        handle,
                        self.implied_edges,
                    );
                    }
                }
            }
            Some(EntityType::LwPolyline(p)) => {
                if let Some(ext) = extend_lwpoly(p, pt.x as f64, pt.y as f64, &self.geos) {
                    return extend_hover_wires(
                        &EntityType::LwPolyline(p.clone()),
                        &ext,
                        &self.all_entities,
                        handle,
                        self.implied_edges,
                    );
                }
            }
            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 extend_hover_wires(
                        &EntityType::Spline(s.clone()),
                        &ext,
                        &self.all_entities,
                        handle,
                        self.implied_edges,
                    );
                }
            }
            _ => {}
        }
        vec![]
    }

    fn on_point(&mut self, pt: DVec3) -> CmdResult {
        match &mut self.mode {
            TrimMode::Fence(pts) => {
                pts.push([pt.x, pt.y]);
                CmdResult::NeedPoint
            }
            TrimMode::CrossFirst => {
                self.mode = TrimMode::CrossSecond([pt.x, pt.y]);
                CmdResult::NeedPoint
            }
            TrimMode::CrossSecond(p1) => {
                let p1 = *p1;
                let p2 = [pt.x, pt.y];
                self.mode = TrimMode::Pick;
                let rect = [p1, [p1[0], p2[1]], p2, [p2[0], p1[1]], p1];
                let window = [
                    [p1[0].min(p2[0]), p1[1].min(p2[1])],
                    [p1[0].max(p2[0]), p1[1].max(p2[1])],
                ];
                self.fence_run(&rect, Some(window))
            }
            _ => CmdResult::NeedPoint,
        }
    }

    fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
        match &self.mode {
            TrimMode::Fence(pts) if !pts.is_empty() => {
                let mut out = vec![fence_preview_wire(pts, [pt.x, pt.y], "extend_fence")];
                let mut fpts = pts.clone();
                fpts.push([pt.x, pt.y]);
                out.extend(fence_result_preview(
                    &self.all_entities,
                    &self.geos,
                    &fpts,
                    None,
                    !self.shift,
                    self.implied_edges,
                ));
                out
            }
            TrimMode::CrossSecond(p1) => {
                let p1 = *p1;
                let p2 = [pt.x, pt.y];
                let mut out = vec![crossing_preview_wire(p1, p2, "extend_cross")];
                let rect = [p1, [p1[0], p2[1]], p2, [p2[0], p1[1]], p1];
                let window = [
                    [p1[0].min(p2[0]), p1[1].min(p2[1])],
                    [p1[0].max(p2[0]), p1[1].max(p2[1])],
                ];
                out.extend(fence_result_preview(
                    &self.all_entities,
                    &self.geos,
                    &rect,
                    Some(window),
                    !self.shift,
                    self.implied_edges,
                ));
                out
            }
            _ => Vec::new(),
        }
    }

    fn on_enter(&mut self) -> CmdResult {
        match std::mem::replace(&mut self.mode, TrimMode::Pick) {
            TrimMode::SelectEdges => {
                self.rebuild_geos();
                CmdResult::NeedPoint
            }
            TrimMode::Fence(pts) if pts.len() >= 2 => self.fence_run(&pts, None),
            TrimMode::Fence(_)
            | TrimMode::CrossFirst
            | TrimMode::CrossSecond(_)
            | TrimMode::Erase => CmdResult::NeedPoint,
            TrimMode::Pick => CmdResult::Cancel,
        }
    }
    fn on_escape(&mut self) -> CmdResult {
        if !matches!(self.mode, TrimMode::Pick) {
            self.mode = TrimMode::Pick;
            return CmdResult::NeedPoint;
        }
        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 xline_at(x1: f64, y1: f64, x2: f64, y2: f64, h: u64) -> EntityType {
        let mut l = LineEnt::new();
        l.start = Vector3::new(x1, y1, 0.0);
        l.end = Vector3::new(x2, y2, 0.0);
        l.common.handle = Handle::new(h);
        EntityType::Line(l)
    }

    /// Edge: Extend — the target must lengthen to the BOUNDARY'S extrapolation
    /// when the boundary doesn't physically cross the target's path (#336).
    #[test]
    fn edge_extend_reaches_implied_boundary() {
        let target = xline_at(0.0, 0.0, 2.0, 0.0, 1);
        let boundary = xline_at(5.0, 2.0, 5.0, 8.0, 2);
        let all = vec![target.clone(), boundary];
        // Edge off: the boundary's drawn extent never meets y=0 — no extend.
        let geos = build_geos(&all);
        assert!(pick_extend_at(&all, &geos, Handle::new(1), 1.9, 0.0).is_none());
        // Edge on: the implied boundary crosses y=0 at x=5 — extend to (5,0).
        let mut implied = build_geos(&all);
        imply_edge_geos(&mut implied);
        match pick_extend_at(&all, &implied, Handle::new(1), 1.9, 0.0) {
            Some(EntityType::Line(l)) => {
                assert!((l.end.x - 5.0).abs() < 1e-6 && l.end.y.abs() < 1e-6,
                    "extends to the implied boundary, got ({}, {})", l.end.x, l.end.y);
            }
            other => panic!("expected an extended Line, got {other:?}"),
        }
    }

    /// #336 repro: two lines forming an X; a fence across one overhanging arm
    /// must trim that arm back to the intersection.
    #[test]
    fn fence_trims_x_arm() {
        let all = vec![
            xline_at(0.0, 0.0, 10.0, 10.0, 1),
            xline_at(0.0, 10.0, 10.0, 0.0, 2),
        ];
        let geos = build_geos(&all);
        let fence = [[8.0, 10.0], [10.0, 8.0]];
        let repl = fence_pass(&all, &geos, &fence, None, false);
        assert_eq!(repl.len(), 1, "exactly the crossed line is trimmed: {repl:?}");
        assert_eq!(repl[0].0, Handle::new(1));
        assert_eq!(repl[0].1.len(), 1);
        match &repl[0].1[0] {
            EntityType::Line(l) => {
                assert!((l.end.x - 5.0).abs() < 1e-6 && (l.end.y - 5.0).abs() < 1e-6,
                    "arm cut back to the intersection, got end ({}, {})", l.end.x, l.end.y);
            }
            other => panic!("expected a Line, got {other:?}"),
        }
    }

    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()
        }
        // Polylines sample only their vertices — subdivide the straight
        // segments so the preview cut follows the boundary instead of
        // jumping at the nearest vertex (#340).
        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: Vec<[f64; 2]> = match &seg {
                    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()
                    }
                    _ => sample_entity_xy(&seg),
                };
                if pts.last() == sp.first() {
                    pts.extend_from_slice(&sp[1..]);
                } else {
                    pts.extend(sp);
                }
            }
            pts
        }
        _ => 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 {
        taper_widths: Vec::new(),
        world_width: 0.0,
        depth_override: None,
        fill_is_3d: false,
        pick_tris: Vec::new(),
        pick_tris_low: Vec::new(),
        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,
        fill_tris: vec![],
        fill_tris_low: Vec::new(),
    }
}

/// Insert the exact boundary crossings between consecutive samples so a
/// sampled cut lands ON the boundary instead of at the nearest sample. A
/// polyline samples only its vertices, so without this a straight segment
/// crossing the boundary was cut at the wrong place — or not at all when
/// both endpoints were on the kept side (#340).
fn insert_boundary_crossings(pts: &[[f64; 2]], geos: &[Geo]) -> Vec<[f64; 2]> {
    let Some(last) = pts.last() else {
        return Vec::new();
    };
    let mut out: Vec<[f64; 2]> = Vec::with_capacity(pts.len());
    for w in pts.windows(2) {
        let (a, b) = (w[0], w[1]);
        out.push(a);
        let mut ts = line_seg_ts(a[0], a[1], b[0], b[1], Handle::NULL, geos);
        ts.sort_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
        for t in ts {
            if t > 1e-6 && t < 1.0 - 1e-6 {
                out.push(lerp2(a, b, t));
            }
        }
    }
    out.push(*last);
    out
}

/// Sampled trim: `None` leaves the entity unchanged, `Some(vec![])` erases it,
/// `Some(runs)` replaces it with the surviving pieces as LwPolylines.
/// Classification is per SEGMENT midpoint (samples + exact crossings), so a
/// vertex sitting exactly on the boundary can't flip the parity test.
fn extrim_sampled(
    pts: &[[f64; 2]],
    geos: &[Geo],
    side: &dyn Fn([f64; 2]) -> bool,
) -> Option<Vec<EntityType>> {
    if pts.len() < 2 {
        return None;
    }
    let dense = insert_boundary_crossings(pts, geos);
    let seg_kept: Vec<bool> = dense
        .windows(2)
        .map(|w| !side([(w[0][0] + w[1][0]) * 0.5, (w[0][1] + w[1][1]) * 0.5]))
        .collect();
    if seg_kept.iter().all(|&k| k) {
        return None; // wholly on the kept side — untouched
    }
    if seg_kept.iter().all(|&k| !k) {
        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 (i, &kept) in seg_kept.iter().enumerate() {
        if kept {
            if cur.is_empty() {
                cur.push(dense[i]);
            }
            cur.push(dense[i + 1]);
        } else if cur.len() >= 2 {
            runs.push(std::mem::take(&mut cur));
        } else {
            cur.clear();
        }
    }
    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));
        // Only a picked LINE boundary is treated as infinite. A polyline
        // explodes into per-segment Line geos — extending those turns a
        // closed boundary into a grid of infinite lines and the parity
        // side-test breaks (#340).
        if matches!(e, EntityType::Line(_)) {
            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, &geos, &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
