From 09a6cc3521055447a072c057a450a5d4cfb5ff28 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 7 Apr 2026 10:47:32 +0300 Subject: [PATCH] feat: TRIM and EXTEND support for Ellipse entities - Add Ellipse variant to the Geo boundary enum - Implement algebraic line-ellipse intersection (le()) for fast boundary checks - Add ellipse_seg_ts() using bisection for all boundary types including Arc/Circle/Ellipse boundaries (64-sample sign-change + 32-step bisection) - Add trim_ellipse() producing partial Ellipse entities with updated parameters - Add extend_ellipse() to stretch an ellipse arc to the nearest boundary - Wire Ellipse into TrimCommand and ExtendCommand: on_entity_pick, on_entity_replaced, and on_hover_entity (live red/cyan preview) Co-Authored-By: Claude Sonnet 4.6 --- src/modules/home/modify/trim.rs | 487 ++++++++++++++++++++++++++++++-- 1 file changed, 460 insertions(+), 27 deletions(-) diff --git a/src/modules/home/modify/trim.rs b/src/modules/home/modify/trim.rs index 43b84ab7..33453df5 100644 --- a/src/modules/home/modify/trim.rs +++ b/src/modules/home/modify/trim.rs @@ -10,7 +10,7 @@ use std::f64::consts::TAU; -use acadrust::entities::{Arc as ArcEnt, Line as LineEnt, Ray as RayEnt, XLine as XLineEnt}; +use acadrust::entities::{Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, Ray as RayEnt, XLine as XLineEnt}; use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::Vec3; @@ -144,6 +144,40 @@ fn cc_angles(cx1: f64, cy1: f64, r1: f64, cx2: f64, cy2: f64, r2: f64) -> Vec 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 = 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. @@ -187,6 +221,18 @@ enum Geo { 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) + }, } fn build_geos(entities: &[EntityType]) -> Vec { @@ -228,6 +274,18 @@ fn build_geos(entities: &[EntityType]) -> Vec { 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 }) + } _ => None, } }) @@ -303,6 +361,15 @@ fn line_seg_ts(ax: f64, ay: f64, bx: f64, by: f64, target: Handle, geos: &[Geo]) } } } + 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)); + } + } + } } } ts.sort_by(|a, b| a.partial_cmp(b).unwrap()); @@ -378,6 +445,11 @@ fn arc_seg_ts( .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) + } }; for a in angles { if in_arc(a, a0, a1) { @@ -390,6 +462,278 @@ fn arc_seg_ts( 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 { + // 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 { + 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; + } + } + } + } + } + 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 { + 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 { + 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() +} + // ── Trim helpers ────────────────────────────────────────────────────────── /// Remove the t-interval containing `t_click` from sorted ts. Returns surviving pieces. @@ -559,6 +903,14 @@ fn extend_line(orig: &LineEnt, t_click: f64, geos: &[Geo]) -> Option 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; } + } + } } } @@ -728,6 +1080,16 @@ fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> { a.end_angle.to_radians(), a.center.y, ), + 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) + } // For preview, show a 20-unit section of semi-infinite results EntityType::Ray(r) => { let bx = r.base_point.x; @@ -846,6 +1208,25 @@ impl CadCommand for TrimCommand { } else { 0.5 }; Some(trim_xline(x, &ts, t_click)) } + Some(EntityType::Ellipse(e)) => { + let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); + if a < 1e-9 { return CmdResult::NeedPoint; } + let b = a * e.minor_axis_ratio; + let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a); + let t0 = e.start_parameter; + let mut t1 = e.end_parameter; + if t1 <= t0 { t1 += TAU; } + let ts = ellipse_seg_ts(e.center.x, e.center.y, a, b, nx, ny, t0, t1, handle, &self.geos); + if ts.is_empty() { return CmdResult::NeedPoint; } + // t_click: project mouse onto ellipse local param + let rx = pt.x as f64 - e.center.x; + let ry = pt.y as f64 - e.center.y; + let xl = rx * nx + ry * ny; + let yl = -rx * ny + ry * nx; + let t_ell = yl.atan2(xl); + let t_click = arc_t(t_ell, t0, t1); + Some(trim_ellipse(e, &ts, t_click)) + } _ => None, }; @@ -882,6 +1263,7 @@ impl CadCommand for TrimCommand { 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, _ => {} } } @@ -999,10 +1381,9 @@ impl CadCommand for TrimCommand { ((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); - // Show a finite 40-unit preview section around base 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 = [(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], DIM_RED, false); + 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); @@ -1010,6 +1391,31 @@ impl CadCommand for TrimCommand { } out } + Some(EntityType::Ellipse(e)) => { + let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); + if a < 1e-9 { return vec![]; } + let b = a * e.minor_axis_ratio; + let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a); + let t0 = e.start_parameter; + let mut t1 = e.end_parameter; + if t1 <= t0 { t1 += TAU; } + let ts = ellipse_seg_ts(e.center.x, e.center.y, a, b, nx, ny, t0, t1, handle, &self.geos); + if ts.is_empty() { return vec![]; } + let rx = pt.x as f64 - e.center.x; + let ry = pt.y as f64 - e.center.y; + let xl = rx * nx + ry * ny; + let yl = -rx * ny + ry * nx; + let t_click = arc_t(yl.atan2(xl), t0, t1); + let survivors = trim_ellipse(e, &ts, t_click); + let orig_pts = ellipse_pts(e.center.x, e.center.y, a, b, nx, ny, t0, t1, e.center.z); + let removed = WireModel::solid("trim_rm".into(), orig_pts, DIM_RED, false); + let mut out = vec![removed]; + for (i, ent) in survivors.iter().enumerate() { + let pts = entity_pts(ent); + out.push(WireModel::solid(format!("trim_keep_{i}"), pts, WireModel::CYAN, false)); + } + out + } _ => vec![], } } @@ -1091,6 +1497,22 @@ impl CadCommand for ExtendCommand { }; extend_line(l, t_click, &self.geos) } + Some(EntityType::Ellipse(e)) => { + let t0 = e.start_parameter; + let mut t1 = e.end_parameter; + if t1 <= t0 { t1 += TAU; } + let span = t1 - t0; + let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); + if a < 1e-9 { return CmdResult::NeedPoint; } + let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a); + let rx = pt.x as f64 - e.center.x; + let ry = pt.y as f64 - e.center.y; + let xl = rx * nx + ry * ny; + let yl = -rx * ny + ry * nx; + let t_click = arc_t(yl.atan2(xl), t0, t1); + let _ = span; + extend_ellipse(e, t_click, &self.geos) + } _ => None, }; @@ -1110,8 +1532,10 @@ impl CadCommand for ExtendCommand { { if pending_old == old { // Update the snapshot entry: replace geometry + assign real handle. - if let EntityType::Line(l) = &mut new_entity { - l.common.handle = new_handle; + match &mut new_entity { + EntityType::Line(l) => l.common.handle = new_handle, + EntityType::Ellipse(e) => e.common.handle = new_handle, + _ => {} } if let Some(pos) = self .all_entities @@ -1134,28 +1558,37 @@ impl CadCommand for ExtendCommand { .all_entities .iter() .find(|e| e.common().handle == handle); - if let Some(EntityType::Line(l)) = entity { - 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(extended) = extend_line(l, t_click, &self.geos) { - let pts = entity_pts(&extended); - return vec![WireModel::solid( - "extend_prev".into(), - pts, - WireModel::CYAN, - false, - )]; + match entity { + Some(EntityType::Line(l)) => { + let ax = l.start.x; let ay = l.start.y; + let bx = l.end.x; let by = l.end.y; + let dx = bx - ax; let dy = by - ay; + let len2 = dx * dx + dy * dy; + let t_click = if len2 > 1e-12 { + ((pt.x as f64 - ax) * dx + (pt.y as f64 - ay) * dy) / len2 + } else { 0.5 }; + if let Some(ext) = extend_line(l, t_click, &self.geos) { + return vec![WireModel::solid("extend_prev".into(), entity_pts(&ext), WireModel::CYAN, false)]; + } } + Some(EntityType::Ellipse(e)) => { + let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); + if a >= 1e-9 { + let (nx, ny) = (e.major_axis.x / a, e.major_axis.y / a); + let t0 = e.start_parameter; + let mut t1 = e.end_parameter; + if t1 <= t0 { t1 += TAU; } + let rx = pt.x as f64 - e.center.x; + let ry = pt.y as f64 - e.center.y; + let xl = rx * nx + ry * ny; + let yl = -rx * ny + ry * nx; + let t_click = arc_t(yl.atan2(xl), t0, t1); + if let Some(ext) = extend_ellipse(e, t_click, &self.geos) { + return vec![WireModel::solid("extend_prev".into(), entity_pts(&ext), WireModel::CYAN, false)]; + } + } + } + _ => {} } vec![] }