From f6efe7a06a6c2c21b3b93301703476973695b21c Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 2 Apr 2026 23:45:06 +0300 Subject: [PATCH] =?UTF-8?q?Feat:=20LENGTHEN=20command=20(LEN)=20=E2=80=94?= =?UTF-8?q?=20extend=20or=20trim=20Line/Arc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Options via text input after entity pick: DE — extend by delta (negative trims) TO — set absolute total length / arc length P — scale by percentage — plain number treated as DE delta Closest end to pick point is modified. Uses CmdResult::LengthenEntity. Co-Authored-By: Claude Sonnet 4.6 --- src/app/cmd_result.rs | 24 ++++ src/app/commands.rs | 9 +- src/command/mod.rs | 6 + src/modules/home/modify/lengthen.rs | 198 ++++++++++++++++++++++++++++ src/modules/home/modify/mod.rs | 1 + 5 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 src/modules/home/modify/lengthen.rs diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 7f5bd66f3..53cd41a00 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -371,6 +371,30 @@ impl H7CAD { self.restore_pre_cmd_tangent(); self.command_line.push_output(&msg); } + CmdResult::LengthenEntity { handle, pick_pt, mode } => { + use crate::modules::home::modify::lengthen::lengthen_entity; + let result = self.tabs[i].scene.document + .get_entity(handle) + .and_then(|e| lengthen_entity(e, pick_pt, &mode)); + match result { + Some(new_entity) => { + let label = self.history_label_from_active_cmd(i, "LENGTHEN"); + self.push_undo_snapshot(i, label); + self.tabs[i].scene.erase_entities(&[handle]); + self.tabs[i].scene.add_entity(new_entity); + self.tabs[i].dirty = true; + self.command_line.push_output("LENGTHEN: applied."); + self.refresh_properties(); + } + None => { + self.command_line.push_error("LENGTHEN: entity type not supported."); + } + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + } CmdResult::DivideEntity { handle, n } => { use crate::modules::home::inquiry::divide::divide_entity; let pts = self.tabs[i].scene.document diff --git a/src/app/commands.rs b/src/app/commands.rs index d5c7f0ed9..2e2a2c3ce 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -985,6 +985,13 @@ impl H7CAD { self.tabs[i].active_cmd = Some(Box::new(cmd)); } + "LENGTHEN"|"LEN" => { + use crate::modules::home::modify::lengthen::LengthenCommand; + let cmd = LengthenCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + "DIVIDE"|"DIV" => { use crate::modules::home::inquiry::divide::DivideCommand; let cmd = DivideCommand::new(); @@ -1024,7 +1031,7 @@ impl H7CAD { "HELP"|"?" => { self.command_line.push_output( "Draw: LINE CIRCLE ARC PLINE RECT POLY POINT ELLIPSE SPLINE RAY XLINE HATCH | \ - Modify: MOVE COPY ROTATE SCALE MIRROR ERASE OFFSET EXTEND FILLET CHAMFER STRETCH EXPLODE TRIM BREAK JOIN | \ + Modify: MOVE COPY ROTATE SCALE MIRROR ERASE OFFSET EXTEND FILLET CHAMFER STRETCH EXPLODE TRIM BREAK JOIN LENGTHEN | \ Array: ARRAY ARRAYRECT ARRAYPOLAR ARRAYPATH | \ Text: TEXT MTEXT LEADER MLEADER | \ Dimension: DIMLINEAR DIMANGULAR DIMRADIUS | \ diff --git a/src/command/mod.rs b/src/command/mod.rs index 0b60ef9fc..d8dfd4d4f 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -103,6 +103,12 @@ pub enum CmdResult { DivideEntity { handle: Handle, n: usize }, /// Place Point entities at `segment_length` intervals along the entity. MeasureEntity { handle: Handle, segment_length: f64 }, + /// Extend/trim a Line or Arc by the given mode; end command. + LengthenEntity { + handle: Handle, + pick_pt: Vec3, + mode: crate::modules::home::modify::lengthen::LenMode, + }, } // ── Trait ───────────────────────────────────────────────────────────────── diff --git a/src/modules/home/modify/lengthen.rs b/src/modules/home/modify/lengthen.rs new file mode 100644 index 000000000..e8e5a1315 --- /dev/null +++ b/src/modules/home/modify/lengthen.rs @@ -0,0 +1,198 @@ +// 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 acadrust::entities::{Arc as ArcEnt, Line as LineEnt}; +use acadrust::types::Vector3; +use acadrust::{EntityType, Handle}; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; + +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 => "LENGTHEN Select object:".into(), + LenState::PickOption { .. } => + "LENGTHEN Enter option [DE / TO / P ]:".into(), + } + } + + fn needs_entity_pick(&self) -> bool { + matches!(self.state, LenState::PickEntity) + } + + fn on_entity_pick(&mut self, handle: Handle, pt: Vec3) -> CmdResult { + 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 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 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: Vec3) -> 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 or Arc. +/// `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), + _ => None, + } +} + +fn lengthen_line(line: &LineEnt, pick_pt: Vec3, mode: &LenMode) -> Option { + let s = Vec3::new(line.start.x as f32, line.start.z as f32, 0.0); + let e = Vec3::new(line.end.x as f32, line.end.z as f32, 0.0); + let p = Vec3::new(pick_pt.x, pick_pt.z, 0.0); + + let current_len = (e - s).length() as f64; + if current_len < 1e-10 { return None; } + + let new_len = apply_mode(current_len, mode)?; + if new_len < 1e-10 { return None; } + + let dir = (e - s) / current_len as f32; + + // Which end is closer to pick? + let dist_to_start = (p - s).length(); + let dist_to_end = (p - e).length(); + + let mut result = line.clone(); + result.common.handle = Handle::NULL; + + if dist_to_end <= dist_to_start { + // Extend/trim the end + let new_end = s + dir * new_len as f32; + result.end = xz_to_v3(new_end, line.end.z); + } else { + // Extend/trim the start (move start backward along dir) + let new_start = e - dir * new_len as f32; + result.start = xz_to_v3(new_start, line.start.z); + } + Some(EntityType::Line(result)) +} + +fn lengthen_arc(arc: &ArcEnt, pick_pt: Vec3, mode: &LenMode) -> Option { + let cx = arc.center.x as f32; + let cy = arc.center.z as f32; // Y-up: DXF Y → world Z + + // Current arc span + let span = arc_span_deg(arc.start_angle, arc.end_angle); + let current_arc_len = arc.radius * span.to_radians(); + + let new_arc_len = apply_mode(current_arc_len, mode)?; + if new_arc_len < 1e-10 { return None; } + let new_span_deg = (new_arc_len / arc.radius).to_degrees(); + + // Which end (start or end angle) is closer to pick? + let start_rad = arc.start_angle.to_radians(); + let end_rad = arc.end_angle.to_radians(); + + let start_pt = Vec3::new( + cx + arc.radius as f32 * start_rad.cos() as f32, + pick_pt.y, + cy + arc.radius as f32 * start_rad.sin() as f32, + ); + let end_pt = Vec3::new( + cx + arc.radius as f32 * end_rad.cos() as f32, + pick_pt.y, + cy + arc.radius as f32 * end_rad.sin() as f32, + ); + let dist_start = (pick_pt - start_pt).length(); + let dist_end = (pick_pt - end_pt).length(); + + let delta_span = new_span_deg - span; + + 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_deg; + } else { + // Extend start angle (move start backwards) + result.start_angle = arc.end_angle - new_span_deg; + } + let _ = delta_span; + Some(EntityType::Arc(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_deg(start: f64, end: f64) -> f64 { + let span = ((end - start) + 360.0) % 360.0; + if span < 1e-6 { 360.0 } else { span } +} + +fn xz_to_v3(v: Vec3, z: f64) -> Vector3 { + // v is (world_x, world_z, 0) → DXF (x, world_z, z) + Vector3::new(v.x as f64, v.y as f64, z) +} diff --git a/src/modules/home/modify/mod.rs b/src/modules/home/modify/mod.rs index 61abe818c..a4beda290 100644 --- a/src/modules/home/modify/mod.rs +++ b/src/modules/home/modify/mod.rs @@ -4,6 +4,7 @@ pub mod copy; pub mod delete; pub mod explode; pub mod join; +pub mod lengthen; mod extend; pub mod fillet; pub mod mirror;