// LENGTHEN command — extend or trim a Line or Arc by a specified delta or total. // // Options (entered as text after the entity pick): // DE — extend by delta (positive extends, negative trims) // TO — set total length (Line) or arc length (Arc) // P — change by percentage (100 = no change, 150 = +50%) // // The entity is modified at whichever end is closest to the pick point. use crate::modules::draw::modify::spline_ops::{spline_cut, spline_to_nurbs}; use acadrust::entities::{ Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Spline as SplineEnt, }; use cadkernel::geom2d::{ Curve, Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc, }; use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::{DVec3, Vec3}; use crate::t; use crate::command::{CadCommand, CmdResult}; const TAU: f64 = std::f64::consts::TAU; pub struct LengthenCommand { state: LenState, } enum LenState { PickEntity, PickOption { handle: Handle, pick_pt: Vec3 }, } impl LengthenCommand { pub fn new() -> Self { Self { state: LenState::PickEntity, } } } impl CadCommand for LengthenCommand { fn name(&self) -> &'static str { "LENGTHEN" } fn prompt(&self) -> String { match &self.state { LenState::PickEntity => t!("LENGTHEN Select object:").into_owned(), LenState::PickOption { .. } => { t!("LENGTHEN Enter option [DE / TO / P ]:").into_owned() } } } fn needs_entity_pick(&self) -> bool { matches!(self.state, LenState::PickEntity) } fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { let pt = pt.as_vec3(); if handle.is_null() { return CmdResult::NeedPoint; } self.state = LenState::PickOption { handle, pick_pt: pt, }; CmdResult::NeedPoint } fn wants_text_input(&self) -> bool { matches!(self.state, LenState::PickOption { .. }) } fn dyn_field(&self) -> crate::command::DynField { if matches!(self.state, LenState::PickOption { .. }) { crate::command::DynField::Scalar } else { crate::command::DynField::Point } } fn on_text_input(&mut self, text: &str) -> Option { let (handle, pick_pt) = match &self.state { LenState::PickOption { handle, pick_pt } => (*handle, *pick_pt), _ => return None, }; let pick_pt = pick_pt.as_dvec3(); let text = text.trim().to_uppercase(); if let Some(rest) = text.strip_prefix("DE ").or_else(|| text.strip_prefix("DE")) { let delta: f64 = rest.trim().replace(',', ".").parse().ok()?; Some(CmdResult::LengthenEntity { handle, pick_pt, mode: LenMode::Delta(delta), }) } else if let Some(rest) = text.strip_prefix("TO ").or_else(|| text.strip_prefix("TO")) { let total: f64 = rest .trim() .replace(',', ".") .parse() .ok() .filter(|&v: &f64| v > 0.0)?; Some(CmdResult::LengthenEntity { handle, pick_pt, mode: LenMode::Total(total), }) } else if let Some(rest) = text.strip_prefix("P ").or_else(|| text.strip_prefix("P")) { let pct: f64 = rest .trim() .replace(',', ".") .parse() .ok() .filter(|&v: &f64| v > 0.0)?; Some(CmdResult::LengthenEntity { handle, pick_pt, mode: LenMode::Percent(pct), }) } else { // Try plain number as delta let delta: f64 = text.replace(',', ".").parse().ok()?; Some(CmdResult::LengthenEntity { handle, pick_pt, mode: LenMode::Delta(delta), }) } } fn on_point(&mut self, _pt: DVec3) -> CmdResult { CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } } // ── Mode enum (also used in CmdResult) ──────────────────────────────────── #[derive(Clone)] pub enum LenMode { Delta(f64), Total(f64), Percent(f64), } // ── Geometry ─────────────────────────────────────────────────────────────── /// Apply LENGTHEN to a Line, Arc, Ellipse, or Spline. /// `pick_pt` determines which end to extend/trim (closest end is modified). pub fn lengthen_entity(entity: &EntityType, pick_pt: Vec3, mode: &LenMode) -> Option { match entity { EntityType::Line(l) => lengthen_line(l, pick_pt, mode), EntityType::Arc(a) => lengthen_arc(a, pick_pt, mode), EntityType::Ellipse(e) => lengthen_ellipse(e, pick_pt, mode), EntityType::Spline(s) => lengthen_spline(s, pick_pt, mode), EntityType::LwPolyline(p) => { let p = crate::entities::curve::lwpolyline_world_xy(p)?; lengthen_lwpoly(&p, pick_pt, mode) } _ => None, } } fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option { // Keep the line's own coordinates on the f64 grid; only pick_pt (screen-only, // nearest-end selection) widens for the distance compare. let sx = line.start.x; let sy = line.start.y; let ex = line.end.x; let ey = line.end.y; let dx = ex - sx; let dy = ey - sy; let current_len = (dx * dx + dy * dy).sqrt(); if current_len < 1e-10 { return None; } let new_len = apply_mode(current_len, mode)?; if new_len < 1e-10 { return None; } let ux = dx / current_len; let uy = dy / current_len; // Which end is closer to pick? let px = pick_pt.x as f64; let py = pick_pt.y as f64; let dist_to_start = (px - sx).hypot(py - sy); let dist_to_end = (px - ex).hypot(py - ey); let mut result = line.clone(); result.common.handle = Handle::NULL; if dist_to_end <= dist_to_start { // Extend/trim the end let new_x = sx + ux * new_len; let new_y = sy + uy * new_len; result.end = Vector3::new(new_x, new_y, line.end.z); } else { // Extend/trim the start (move start backward along dir) let new_x = ex - ux * new_len; let new_y = ey - uy * new_len; result.start = Vector3::new(new_x, new_y, line.start.z); } Some(EntityType::Line(result)) } fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option { // Current arc span let span = arc_span_rad(arc.start_angle, arc.end_angle); let current_arc_len = arc.radius * span; let new_arc_len = apply_mode(current_arc_len, mode)?; if new_arc_len < 1e-10 || new_arc_len >= std::f64::consts::TAU * arc.radius - 1.0e-9 { return None; } let new_span = new_arc_len / arc.radius; // Which end (start or end angle) is closer to pick? let curve = crate::entities::curve::arc_curve(arc); let start_pt = Vec3::from_array(curve.point_at(0.0).map(|value| value as f32)); let end_pt = Vec3::from_array(curve.point_at(1.0).map(|value| value as f32)); let dist_start = (pick_pt - start_pt).length(); let dist_end = (pick_pt - end_pt).length(); let mut result = arc.clone(); result.common.handle = Handle::NULL; if dist_end <= dist_start { // Extend end angle result.end_angle = arc.start_angle + new_span; } else { // Extend start angle (move start backwards) result.start_angle = arc.end_angle - new_span; } Some(EntityType::Arc(result)) } fn lengthen_ellipse(ell: &EllipseEnt, pick_pt: Vec3, mode: &LenMode) -> Option { let a = (ell.major_axis.x.powi(2) + ell.major_axis.y.powi(2)).sqrt(); if a < 1e-9 { return None; } let b = a * ell.minor_axis_ratio; let nx = ell.major_axis.x / a; let ny = ell.major_axis.y / a; let t0 = ell.start_parameter; let mut t1 = ell.end_parameter; if t1 <= t0 { t1 += TAU; } // Measured by the kernel rather than by a hundred and twenty-eight // chords: the chord sum reads short, so LENGTHEN's idea of "current" was // already below the true length before a delta was applied to it. let shape = KernelEllipse { centre: [ell.center.x, ell.center.y], major_radius: a, minor_radius: b, major_axis: [nx, ny], }; let arc = |from: f64, to: f64| { Curve::Ellipse(KernelEllipseArc { ellipse: shape, start_parameter: from, end_parameter: to, }) }; let current_len = arc(t0, t1).length(); if current_len < 1e-10 { return None; } let new_len = apply_mode(current_len, mode)?; if new_len < 1e-10 { return None; } // Which end is closer to the pick, in the DXF XY plane. let point_at = |t: f64| { ( ell.center.x + a * t.cos() * nx - b * t.sin() * ny, ell.center.y + a * t.cos() * ny + b * t.sin() * nx, ) }; let (p_x, p_y) = (pick_pt.x as f64, pick_pt.y as f64); let (sx, sy) = point_at(t0); let (ex, ey) = point_at(t1); let extend_end = (p_x - ex).hypot(p_y - ey) <= (p_x - sx).hypot(p_y - sy); let mut result = ell.clone(); result.common.handle = Handle::NULL; if extend_end { // Walk `new_len` forward from the fixed start. A whole turn is the // most there is to walk, and the kernel clamps to it. let whole = arc(t0, t0 + TAU); result.end_parameter = t0 + whole.parameter_at_distance(new_len) * TAU; } else { // The same measured backwards from the fixed end: the last `new_len` // of a whole turn ending at t1. let whole = arc(t1 - TAU, t1); let from_start = whole.length() - new_len; result.start_parameter = t1 - TAU + whole.parameter_at_distance(from_start) * TAU; } Some(EntityType::Ellipse(result)) } fn apply_mode(current: f64, mode: &LenMode) -> Option { match mode { LenMode::Delta(d) => Some(current + d), LenMode::Total(t) => Some(*t), LenMode::Percent(p) => Some(current * p / 100.0), } } fn arc_span_rad(start: f64, end: f64) -> f64 { let span = (end - start).rem_euclid(TAU); if span < 1e-6 { TAU } else { span } } fn lengthen_lwpoly(poly: &LwPolyline, pick_pt: Vec3, mode: &LenMode) -> Option { let n = poly.vertices.len(); if n < 2 { return None; } // Determine which end is closer to the pick point (DXF XY: pick_pt.x, pick_pt.z). let px = pick_pt.x as f64; let py = pick_pt.y as f64; let first = &poly.vertices[0]; let last = &poly.vertices[n - 1]; let d_first = (first.location.x - px).hypot(first.location.y - py); let d_last = (last.location.x - px).hypot(last.location.y - py); let at_end = d_last <= d_first; // Terminal segment direction and current length. let (sx, sy, ex, ey) = if at_end { ( poly.vertices[n - 2].location.x, poly.vertices[n - 2].location.y, last.location.x, last.location.y, ) } else { ( poly.vertices[1].location.x, poly.vertices[1].location.y, first.location.x, first.location.y, ) }; let dx = ex - sx; let dy = ey - sy; let current_len = (dx * dx + dy * dy).sqrt(); if current_len < 1e-10 { return None; } let new_len = apply_mode(current_len, mode)?; if new_len < 1e-10 { return None; } let ux = dx / current_len; let uy = dy / current_len; let new_x = sx + ux * new_len; let new_y = sy + uy * new_len; let mut new_poly = poly.clone(); new_poly.common.handle = Handle::NULL; if at_end { let v = new_poly.vertices.last_mut()?; v.location.x = new_x; v.location.y = new_y; } else { let v = new_poly.vertices.first_mut()?; v.location.x = new_x; v.location.y = new_y; } Some(EntityType::LwPolyline(new_poly)) } fn lengthen_spline(spl: &SplineEnt, pick_pt: Vec3, mode: &LenMode) -> Option { let nurbs = spline_to_nurbs(spl)?; let (t0, t1) = nurbs.domain(); if (t1 - t0).abs() < 1e-12 { return None; } let curve = Curve::Nurbs(nurbs.clone()); let arc_len = curve.length(); if arc_len < 1e-10 { return None; } let new_len = apply_mode(arc_len, mode)?; if new_len < 1e-10 || new_len >= arc_len { // A spline is shortened by splitting it, so there is nothing to keep // if the new length is the whole of it or more. Extending would mean // continuing the curve past its own control polygon, which is a // different operation from cutting one. return None; } let p_start = nurbs.point_at_knot(t0); let p_end = nurbs.point_at_knot(t1); let (px, py) = (pick_pt.x as f64, pick_pt.y as f64); let extend_end = (p_end[0] - px).hypot(p_end[1] - py) <= (p_start[0] - px).hypot(p_start[1] - py); // Where to cut, by distance along the curve rather than by a bisection // over repeated chord sums. Keeping the head means cutting `new_len` from // the start; keeping the tail means cutting what is left over. let along = if extend_end { new_len } else { arc_len - new_len }; let at = curve.parameter_at_distance(along); let cut = (t0 + at * (t1 - t0)).clamp(t0 + 1e-10, t1 - 1e-10); let (left, right) = spline_cut(spl, cut)?; Some(EntityType::Spline(if extend_end { left } else { right })) } // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["LENGTHEN"] }); // LengthenCommand