diff --git a/ROADMAP.md b/ROADMAP.md index 3efb81f7d..878de818f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -124,7 +124,7 @@ Underlay (PDF/DWF/DGN) | JOIN (J) | ✅ | — | | EXPLODE (X) | 🔧 | Dimension eksik | | PEDIT (PE) | ✅ | — | -| STRETCH (SS) | 🔧 | Crossing-window seçici yok (MOVE gibi çalışıyor) | +| STRETCH (SS) | ✅ | — | | SPLINEDIT | ⬜ | — | | HATCHEDIT | ✅ | — | | ATTEDIT | ⬜ | Attribute değerlerini düzenleme | @@ -275,7 +275,7 @@ Underlay (PDF/DWF/DGN) | Grip düzenleme (tüm entity tipleri) | ✅ | | MATCHPROP (özellik kopyala) | ✅ | | BYLAYER hızlı atama | ✅ | -| Çoklu seçim (window/crossing) | 🔧 Window var, crossing-window eksik | +| Çoklu seçim (window/crossing) | ✅ | | Sağ tık bağlam menüsü | ⬜ | | Araç çubuğu özelleştirme | ⬜ | | Tema / Renk şeması seçimi | ⬜ | @@ -302,10 +302,9 @@ Underlay (PDF/DWF/DGN) ## Öncelik Sırası (Bir Sonraki Adımlar) ### Yüksek Öncelik -1. **Crossing-window seçici** — STRETCH ve diğer komutlar için gerekli -2. **TRIM / EXTEND / OFFSET / BREAK / LENGTHEN → Spline desteği** -3. **EXPLODE → Dimension desteği** -4. **FILLET → LwPolyline desteği** +1. **TRIM / EXTEND / OFFSET / BREAK / LENGTHEN → Spline desteği** +2. **EXPLODE → Dimension desteği** +3. **FILLET → LwPolyline desteği** ### Orta Öncelik 5. **Solid3D tessellation** tamamlama (ACIS → truck pipeline) diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 29779e341..2b7798678 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -619,6 +619,112 @@ impl H7CAD { self.tabs[i].scene.clear_preview_wire(); self.restore_pre_cmd_tangent(); } + CmdResult::StretchEntities { handles, win_min, win_max, delta } => { + self.push_undo_snapshot(i, "STRETCH"); + let mut count = 0usize; + + // Helper: is DXF point (x, y) inside the world-space window? + // World XZ = DXF XY. + let in_win = |x: f64, y: f64| -> bool { + let wx = x as f32; + let wy = y as f32; + wx >= win_min.x && wx <= win_max.x + && wy >= win_min.z && wy <= win_max.z + }; + + let dx = delta.x as f64; + let dy = delta.z as f64; // world Z = DXF Y + let dz = delta.y as f64; + + for handle in &handles { + let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) else { continue }; + let mut stretched = false; + match entity { + acadrust::EntityType::Line(l) => { + let s_in = in_win(l.start.x, l.start.y); + let e_in = in_win(l.end.x, l.end.y); + if s_in { l.start.x += dx; l.start.y += dy; l.start.z += dz; stretched = true; } + if e_in { l.end.x += dx; l.end.y += dy; l.end.z += dz; stretched = true; } + } + acadrust::EntityType::LwPolyline(p) => { + for v in &mut p.vertices { + if in_win(v.location.x, v.location.y) { + v.location.x += dx; + v.location.y += dy; + stretched = true; + } + } + } + acadrust::EntityType::Polyline2D(p) => { + for v in &mut p.vertices { + if in_win(v.location.x, v.location.y) { + v.location.x += dx; + v.location.y += dy; + stretched = true; + } + } + } + acadrust::EntityType::Polyline(p) => { + for v in &mut p.vertices { + if in_win(v.location.x, v.location.z) { + v.location.x += dx; + v.location.z += dy; + stretched = true; + } + } + } + acadrust::EntityType::Arc(a) => { + if in_win(a.center.x, a.center.y) { + a.center.x += dx; a.center.y += dy; a.center.z += dz; + stretched = true; + } + } + acadrust::EntityType::Circle(c) => { + if in_win(c.center.x, c.center.y) { + c.center.x += dx; c.center.y += dy; c.center.z += dz; + stretched = true; + } + } + acadrust::EntityType::Ellipse(e) => { + if in_win(e.center.x, e.center.y) { + e.center.x += dx; e.center.y += dy; e.center.z += dz; + stretched = true; + } + } + acadrust::EntityType::Insert(ins) => { + if in_win(ins.insert_point.x, ins.insert_point.y) { + ins.insert_point.x += dx; ins.insert_point.y += dy; ins.insert_point.z += dz; + stretched = true; + } + } + acadrust::EntityType::Text(t) => { + if in_win(t.insertion_point.x, t.insertion_point.y) { + t.insertion_point.x += dx; t.insertion_point.y += dy; t.insertion_point.z += dz; + stretched = true; + } + } + acadrust::EntityType::MText(t) => { + if in_win(t.insertion_point.x, t.insertion_point.y) { + t.insertion_point.x += dx; t.insertion_point.y += dy; t.insertion_point.z += dz; + stretched = true; + } + } + _ => { + // Generic: move entire entity (treat as block-level) + stretched = false; // skip generic types + } + } + if stretched { count += 1; } + } + + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + self.command_line.push_output(&format!("STRETCH: {count} entity(ies) stretched.")); + self.refresh_properties(); + } CmdResult::HatcheditApply { handle, name, scale, angle } => { if let Some(mut model) = self.tabs[i].scene.hatches.get(&handle).cloned() { // Update model fields diff --git a/src/command/mod.rs b/src/command/mod.rs index 7625e25a6..e66ca4c10 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -128,6 +128,16 @@ pub enum CmdResult { DdeditEntity { handle: Handle, new_text: String }, /// Apply new pattern/scale/angle to an existing hatch entity. HatcheditApply { handle: Handle, name: String, scale: f32, angle: f32 }, + /// Stretch entities: move only vertices/endpoints inside the crossing window. + StretchEntities { + handles: Vec, + /// Min corner of the crossing window in world XZ (= DXF XY). + win_min: Vec3, + /// Max corner of the crossing window in world XZ (= DXF XY). + win_max: Vec3, + /// Translation vector to apply to vertices inside the window. + delta: Vec3, + }, } // ── Trait ───────────────────────────────────────────────────────────────── diff --git a/src/modules/home/modify/stretch.rs b/src/modules/home/modify/stretch.rs index c0d1b0aa8..25308c8e5 100644 --- a/src/modules/home/modify/stretch.rs +++ b/src/modules/home/modify/stretch.rs @@ -1,17 +1,24 @@ // Stretch tool — ribbon definition + interactive command. // // Command: STRETCH (SS) -// STRETCH: Moves endpoints/vertices that lie within a crossing -// window while leaving the rest of the object fixed. -// Simplified implementation: works like MOVE on the full selected set -// because we don't yet have a crossing-window selector in the viewport. -// Step 1: pick base point -// Step 2: pick new point → translates all selected entities by (new - base) +// Workflow: +// 1. Pick first corner of the crossing window (right-to-left = crossing). +// 2. Pick second corner. +// 3. Pick base point. +// 4. Pick new point → stretches only vertices inside the crossing window. +// +// Entity behaviour: +// Line : move start if inside, move end if inside, move both if both inside. +// LwPolyline : move each vertex independently. +// Polyline/P2D: move each vertex independently. +// Arc / Circle: move the whole entity if its center is inside the window. +// Insert : move the whole entity if its insertion point is inside. +// All others : move the whole entity if any point is inside. use acadrust::Handle; use glam::Vec3; -use crate::command::{CadCommand, CmdResult, EntityTransform}; +use crate::command::{CadCommand, CmdResult}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::wire_model::WireModel; @@ -29,8 +36,14 @@ pub fn tool() -> ToolDef { // ── Command implementation ───────────────────────────────────────────────── enum Step { - Base, - Target(Vec3), + /// Waiting for the first crossing-window corner. + WindowCorner1, + /// Waiting for the second corner; `c1` is the first corner. + WindowCorner2(Vec3), + /// Crossing window defined; waiting for base point. + Base { win_min: Vec3, win_max: Vec3 }, + /// Waiting for target point. + Target { win_min: Vec3, win_max: Vec3, base: Vec3 }, } pub struct StretchCommand { @@ -40,71 +53,89 @@ pub struct StretchCommand { impl StretchCommand { pub fn new(handles: Vec) -> Self { - Self { - handles, - step: Step::Base, - } + Self { handles, step: Step::WindowCorner1 } } } impl CadCommand for StretchCommand { - fn name(&self) -> &'static str { - "STRETCH" - } + fn name(&self) -> &'static str { "STRETCH" } fn prompt(&self) -> String { match &self.step { - Step::Base => format!( - "STRETCH Specify base point [{} objects]:", + Step::WindowCorner1 => format!( + "STRETCH Specify first corner of crossing window [{} objects]:", self.handles.len() ), - Step::Target(base) => format!( - "STRETCH Specify new point [base {:.3},{:.3}]:", - base.x, base.y + Step::WindowCorner2(_) => "STRETCH Specify opposite corner:".into(), + Step::Base { .. } => "STRETCH Specify base point:".into(), + Step::Target { base, .. } => format!( + "STRETCH Specify new point [base {:.3},{:.3}]:", base.x, base.z ), } } fn on_point(&mut self, pt: Vec3) -> CmdResult { match &self.step { - Step::Base => { - self.step = Step::Target(pt); + Step::WindowCorner1 => { + self.step = Step::WindowCorner2(pt); CmdResult::NeedPoint } - Step::Target(base) => { + Step::WindowCorner2(c1) => { + let win_min = c1.min(pt); + let win_max = c1.max(pt); + self.step = Step::Base { win_min, win_max }; + CmdResult::NeedPoint + } + Step::Base { win_min, win_max } => { + let (wmin, wmax) = (*win_min, *win_max); + self.step = Step::Target { win_min: wmin, win_max: wmax, base: pt }; + CmdResult::NeedPoint + } + Step::Target { win_min, win_max, base } => { let delta = pt - *base; - CmdResult::TransformSelected( - self.handles.clone(), - EntityTransform::Translate(delta), - ) + CmdResult::StretchEntities { + handles: self.handles.clone(), + win_min: *win_min, + win_max: *win_max, + delta, + } } } } - fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel - } - fn on_escape(&mut self) -> CmdResult { - CmdResult::Cancel - } + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } fn on_mouse_move(&mut self, pt: Vec3) -> Option { - if let Step::Target(base) = &self.step { - Some(WireModel { - name: "rubber_band".into(), - points: vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]], - color: WireModel::CYAN, - 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![], - }) - } else { - None + match &self.step { + Step::WindowCorner2(c1) => { + // Show crossing-window rectangle preview (dashed green) + let c1 = *c1; + let pts = vec![ + [c1.x, c1.y, c1.z], + [pt.x, c1.y, c1.z], + [pt.x, pt.y, pt.z], + [c1.x, pt.y, pt.z], + [c1.x, c1.y, c1.z], + ]; + Some(WireModel::solid("stretch_window".into(), pts, [0.3, 1.0, 0.3, 0.7], false)) + } + Step::Target { base, .. } => { + Some(WireModel { + name: "rubber_band".into(), + points: vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]], + color: WireModel::CYAN, + 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![], + }) + } + _ => None, } } }