From d10b50778ccf5a7ad479acac29cc235341ac6653 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:23:43 +0300 Subject: [PATCH] Match LENGTHEN command behavior --- src/modules/draw/modify/lengthen.rs | 362 +++++++++++++++------------- 1 file changed, 190 insertions(+), 172 deletions(-) diff --git a/src/modules/draw/modify/lengthen.rs b/src/modules/draw/modify/lengthen.rs index 6c241cf64..a7f238481 100644 --- a/src/modules/draw/modify/lengthen.rs +++ b/src/modules/draw/modify/lengthen.rs @@ -1,6 +1,6 @@ // LENGTHEN command — extend or trim a Line or Arc by a specified delta or total. // -// Options (entered as text after the entity pick): +// Choose an option, enter its value, then pick objects repeatedly: // 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%) @@ -9,7 +9,7 @@ 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, + Ellipse as EllipseEnt, LwPolyline, Spline as SplineEnt, }; use cadkernel::geom2d::{ Curve, Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc, @@ -25,120 +25,191 @@ const TAU: f64 = std::f64::consts::TAU; pub struct LengthenCommand { state: LenState, + picked: Option, + measurement: Option, + edits: usize, } +#[derive(Clone, Copy)] +enum ValueMode { Delta, Total, Percent, DeltaAngle, TotalAngle, Dynamic } + +struct LengthenDefaults { mode: ValueMode, delta: f64, total: f64, percent: f64, delta_angle: f64, total_angle: f64 } +static LENGTHEN_DEFAULTS: std::sync::Mutex = std::sync::Mutex::new( + LengthenDefaults { mode: ValueMode::Total, delta: 0.0, total: 1.0, percent: 100.0, delta_angle: 0.0, total_angle: 180.0 / std::f64::consts::PI } +); + enum LenState { - PickEntity, - PickOption { handle: Handle, pick_pt: Vec3 }, + ChooseMode, + Value(ValueMode), + Apply(LenMode), + DynamicPick, + DynamicPoint { handle: Handle, entity: EntityType, pick: DVec3 }, } impl LengthenCommand { pub fn new() -> Self { - Self { - state: LenState::PickEntity, - } + Self { state: LenState::ChooseMode, picked: None, measurement: None, edits: 0 } } } impl CadCommand for LengthenCommand { - fn name(&self) -> &'static str { - "LENGTHEN" - } + 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() + LenState::ChooseMode => { + let prompt = t!("LENGTHEN Select an object to measure or [Delta/Percent/Total/Dynamic]:"); + self.measurement.map_or_else(|| prompt.to_string(), |length| format!("Length: {length:.4} {prompt}")) } + LenState::Value(ValueMode::Delta) => format!("LENGTHEN Enter delta length <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().delta), + LenState::Value(ValueMode::Total) => format!("LENGTHEN Enter total length <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().total), + LenState::Value(ValueMode::Percent) => format!("LENGTHEN Enter percentage length <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().percent), + LenState::Value(ValueMode::DeltaAngle) => format!("LENGTHEN Enter delta angle <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().delta_angle), + LenState::Value(ValueMode::TotalAngle) => format!("LENGTHEN Enter total angle <{}>:", LENGTHEN_DEFAULTS.lock().unwrap().total_angle), + LenState::DynamicPoint { .. } => "LENGTHEN Specify new end point:".into(), + LenState::Apply(_) | LenState::DynamicPick | LenState::Value(ValueMode::Dynamic) => t!("LENGTHEN Select an object to change or [Undo]:").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; + fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.state { + LenState::ChooseMode => vec![CmdOption::new("Delta", "DE"), CmdOption::new("Percent", "P"), CmdOption::new("Total", "TO"), CmdOption::new("Dynamic", "DY")], + LenState::Value(ValueMode::Delta | ValueMode::Total) => vec![CmdOption::new("Angle", "A")], + LenState::Apply(_) | LenState::DynamicPick => vec![CmdOption::new("Undo", "U")], + _ => Vec::new(), } - self.state = LenState::PickOption { - handle, - pick_pt: pt, - }; - CmdResult::NeedPoint } - fn wants_text_input(&self) -> bool { - matches!(self.state, LenState::PickOption { .. }) + fn needs_entity_pick(&self) -> bool { matches!(self.state, LenState::ChooseMode | LenState::Apply(_) | LenState::DynamicPick) } + fn inject_before_entity_pick(&self) -> bool { true } + fn inject_picked_entity(&mut self, entity: EntityType) { self.picked = Some(entity); } + + fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { + if handle.is_null() { return CmdResult::NeedPoint; } + let Some(entity) = self.picked.take() else { return CmdResult::NeedPoint; }; + match &self.state { + LenState::ChooseMode => { + self.measurement = crate::entities::curve::entity_curve(&entity) + .map(|curve| curve.curve.length()).filter(|length| length.is_finite()); + CmdResult::NeedPoint + } + LenState::Apply(mode) => match lengthen_entity_precise(&entity, pt, mode) { + Some(replacement) => CmdResult::ReplaceManyContinue(vec![(handle, vec![replacement])]), + None => CmdResult::NeedPoint, + }, + LenState::DynamicPick if matches!(entity, EntityType::Line(_) | EntityType::Arc(_)) => { + self.state = LenState::DynamicPoint { handle, entity, pick: pt }; + CmdResult::NeedPoint + } + _ => CmdResult::NeedPoint, + } } + fn on_entity_replaced(&mut self, _old: Handle, _new_handles: &[Handle]) { self.edits += 1; } + fn wants_text_input(&self) -> bool { true } + fn dyn_commit_as_text(&self) -> bool { matches!(self.state, LenState::Value(_)) } + fn dyn_auto_sign_angle(&self) -> bool { false } fn dyn_field(&self) -> crate::command::DynField { - if matches!(self.state, LenState::PickOption { .. }) { - crate::command::DynField::Scalar - } else { - crate::command::DynField::Point - } + if matches!(self.state, LenState::Value(ValueMode::DeltaAngle | ValueMode::TotalAngle)) { crate::command::DynField::Angle } + else if matches!(self.state, LenState::Value(_)) { 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), - }) + if matches!(self.state, LenState::DynamicPoint { .. }) { return None; } + let upper = text.trim().to_uppercase(); + if upper.is_empty() { return Some(self.on_enter()); } + if matches!(self.state, LenState::Apply(_) | LenState::DynamicPick) { + return Some(if matches!(upper.as_str(), "U" | "UNDO") && self.edits > 0 { + self.edits -= 1; + CmdResult::UndoDocument + } else { CmdResult::NeedPoint }); } + if matches!(self.state, LenState::ChooseMode) { + let mut parts = upper.split_whitespace(); + let mode = match parts.next().unwrap_or("") { + "D" | "DE" | "DELTA" => ValueMode::Delta, + "T" | "TO" | "TOTAL" => ValueMode::Total, + "P" | "PERCENT" => ValueMode::Percent, + "DY" | "DYNAMIC" => ValueMode::Dynamic, + _ => return Some(CmdResult::NeedPoint), + }; + LENGTHEN_DEFAULTS.lock().unwrap().mode = mode; + self.state = if matches!(mode, ValueMode::Dynamic) { LenState::DynamicPick } else { LenState::Value(mode) }; + if let Some(value) = parts.next() { return self.on_text_input(value); } + return Some(CmdResult::NeedPoint); + } + let LenState::Value(mode) = self.state else { return Some(CmdResult::NeedPoint); }; + if matches!(upper.as_str(), "A" | "ANGLE") { + self.state = match mode { + ValueMode::Delta => LenState::Value(ValueMode::DeltaAngle), + ValueMode::Total => LenState::Value(ValueMode::TotalAngle), + _ => return Some(CmdResult::NeedPoint), + }; + return Some(CmdResult::NeedPoint); + } + let Some(value) = upper.replace(',', ".").parse::().ok().filter(|v| v.is_finite()) else { + return Some(CmdResult::NeedPoint); + }; + if !matches!(mode, ValueMode::Delta | ValueMode::DeltaAngle) && value <= 0.0 { return Some(CmdResult::NeedPoint); } + if matches!(mode, ValueMode::TotalAngle) && value >= 360.0 { return Some(CmdResult::NeedPoint); } + { + let mut defaults = LENGTHEN_DEFAULTS.lock().unwrap(); + match mode { ValueMode::Delta => defaults.delta = value, ValueMode::Total => defaults.total = value, ValueMode::Percent => defaults.percent = value, + ValueMode::DeltaAngle => defaults.delta_angle = value, ValueMode::TotalAngle => defaults.total_angle = value, ValueMode::Dynamic => {} } + } + self.state = LenState::Apply(match mode { + ValueMode::Delta => LenMode::Delta(value), + ValueMode::Total => LenMode::Total(value), + ValueMode::Percent => LenMode::Percent(value), + ValueMode::DeltaAngle => LenMode::DeltaAngle(value.to_radians()), + ValueMode::TotalAngle => LenMode::TotalAngle(value.to_radians()), + ValueMode::Dynamic => return Some(CmdResult::NeedPoint), + }); + Some(CmdResult::NeedPoint) } - fn on_point(&mut self, _pt: DVec3) -> CmdResult { + fn on_point(&mut self, pt: DVec3) -> CmdResult { + if let LenState::DynamicPoint { handle, entity, pick } = &self.state { + if let Some(replacement) = lengthen_entity_precise(entity, *pick, &LenMode::Dynamic(pt)) { + let handle = *handle; + self.state = LenState::DynamicPick; + return CmdResult::ReplaceManyContinue(vec![(handle, vec![replacement])]); + } + } CmdResult::NeedPoint } + fn on_mouse_move(&mut self, pt: DVec3) -> Option { + let LenState::DynamicPoint { entity, pick, .. } = &self.state else { return None; }; + let replacement = lengthen_entity_precise(entity, *pick, &LenMode::Dynamic(pt))?; + let curve = crate::entities::curve::entity_curve(&replacement)?; + let points = crate::entities::curve::curve_points(&curve).into_iter() + .map(|p| [p[0] as f32, p[1] as f32, p[2] as f32]).collect(); + Some(crate::scene::model::wire_model::WireModel::solid( + "lengthen_dynamic_preview".into(), points, + crate::scene::model::wire_model::WireModel::CYAN, false, + )) + } fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel + match self.state { + LenState::ChooseMode => { + let mode = LENGTHEN_DEFAULTS.lock().unwrap().mode; + self.state = if matches!(mode, ValueMode::Dynamic) { LenState::DynamicPick } else { LenState::Value(mode) }; + CmdResult::NeedPoint + } + LenState::Value(mode) => { + let value = { + let defaults = LENGTHEN_DEFAULTS.lock().unwrap(); + match mode { ValueMode::Delta => defaults.delta, ValueMode::Total => defaults.total, ValueMode::Percent => defaults.percent, + ValueMode::DeltaAngle => defaults.delta_angle, ValueMode::TotalAngle => defaults.total_angle, ValueMode::Dynamic => 0.0 } + }; + self.on_text_input(&value.to_string()).unwrap_or(CmdResult::NeedPoint) + } + _ => CmdResult::Cancel, + } } } - // ── Mode enum (also used in CmdResult) ──────────────────────────────────── #[derive(Clone)] @@ -146,6 +217,9 @@ pub enum LenMode { Delta(f64), Total(f64), Percent(f64), + DeltaAngle(f64), + TotalAngle(f64), + Dynamic(DVec3), } // ── Geometry ─────────────────────────────────────────────────────────────── @@ -153,96 +227,48 @@ pub enum LenMode { /// 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 { + lengthen_entity_precise(entity, pick_pt.as_dvec3(), mode) +} + +fn lengthen_entity_precise(entity: &EntityType, pick: DVec3, mode: &LenMode) -> Option { + use cadkernel::space::lengthen::{LengthChange, lengthen_line, lengthen_arc}; + let change = match mode { + LenMode::Delta(value) => LengthChange::Delta(*value), + LenMode::Total(value) => LengthChange::Total(*value), + LenMode::Percent(value) => LengthChange::Percent(*value), + LenMode::DeltaAngle(value) => LengthChange::DeltaAngle(*value), + LenMode::TotalAngle(value) => LengthChange::TotalAngle(*value), + LenMode::Dynamic(point) => LengthChange::Dynamic(point.to_array()), + }; 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::Line(line) => { + let [start, end] = lengthen_line( + [line.start.x, line.start.y, line.start.z], + [line.end.x, line.end.y, line.end.z], pick.to_array(), change)?; + let mut result = line.clone(); + result.common.handle = Handle::NULL; + result.start = Vector3::new(start[0], start[1], start[2]); + result.end = Vector3::new(end[0], end[1], end[2]); + Some(EntityType::Line(result)) + } + EntityType::Arc(arc) => { + let (start, end) = lengthen_arc(&crate::entities::curve::arc_curve(arc), pick.to_array(), change)?; + let mut result = arc.clone(); + result.common.handle = Handle::NULL; + result.start_angle = start; + result.end_angle = end; + Some(EntityType::Arc(result)) + } + EntityType::Ellipse(e) => lengthen_ellipse(e, pick.as_vec3(), mode), + EntityType::Spline(s) => lengthen_spline(s, pick.as_vec3(), mode), EntityType::LwPolyline(p) => { let p = crate::entities::curve::lwpolyline_world_xy(p)?; - lengthen_lwpoly(&p, pick_pt, mode) + lengthen_lwpoly(&p, pick.as_vec3(), 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 { @@ -318,15 +344,7 @@ fn apply_mode(current: f64, mode: &LenMode) -> Option { 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 + _ => None, } }