From e2be8197b4fc9cabd2b4741eb1b1174abad64bd9 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 15 Jun 2026 23:43:07 +0300 Subject: [PATCH] feat(stretch): live ghost preview while dragging the new point STRETCH now shows a live ghost where vertices inside the crossing window follow the cursor and the rest stay anchored, instead of only a rubber-band line. Add WireModel::stretched and pass the selection's wires into the command. Co-Authored-By: Claude Opus 4.8 --- src/app/commands.rs | 3 +- src/modules/home/modify/stretch.rs | 50 +++++++++++++++++------------- src/scene/wire_model.rs | 20 ++++++++++++ 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/app/commands.rs b/src/app/commands.rs index a091055a5..311e449c2 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -1567,7 +1567,8 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::stretch::StretchCommand; - let new_cmd = StretchCommand::new(handles); + let wires = self.tabs[i].scene.wire_models_for(&handles); + let new_cmd = StretchCommand::new(handles, wires); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } diff --git a/src/modules/home/modify/stretch.rs b/src/modules/home/modify/stretch.rs index c52a38a9c..32fb994d7 100644 --- a/src/modules/home/modify/stretch.rs +++ b/src/modules/home/modify/stretch.rs @@ -52,13 +52,15 @@ enum Step { pub struct StretchCommand { handles: Vec, + wire_models: Vec, step: Step, } impl StretchCommand { - pub fn new(handles: Vec) -> Self { + pub fn new(handles: Vec, wire_models: Vec) -> Self { Self { handles, + wire_models, step: Step::WindowCorner1, } } @@ -128,7 +130,7 @@ impl CadCommand for StretchCommand { CmdResult::Cancel } - fn on_mouse_move(&mut self, pt: Vec3) -> Option { + fn on_preview_wires(&mut self, pt: Vec3) -> Vec { match &self.step { Step::WindowCorner2(c1) => { // Show crossing-window rectangle preview (dashed green) @@ -140,31 +142,35 @@ impl CadCommand for StretchCommand { [c1.x, pt.y, pt.z], [c1.x, c1.y, c1.z], ]; - Some(WireModel::solid( + vec![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![], - aabb: WireModel::UNBOUNDED_AABB, - plinegen: true, - vp_scissor: None, - fill_tris: vec![], - }), - _ => None, + Step::Target { + win_min, + win_max, + base, + } => { + let delta = pt - *base; + // Live ghost: vertices inside the crossing window follow the + // cursor, the rest stay anchored. + let mut out: Vec = self + .wire_models + .iter() + .map(|w| w.stretched(*win_min, *win_max, delta)) + .collect(); + out.push(WireModel::solid( + "rubber_band".into(), + vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]], + WireModel::CYAN, + false, + )); + out + } + _ => vec![], } } } diff --git a/src/scene/wire_model.rs b/src/scene/wire_model.rs index a0abd491a..6ab655be4 100644 --- a/src/scene/wire_model.rs +++ b/src/scene/wire_model.rs @@ -155,6 +155,26 @@ impl WireModel { out } + /// Return a clone for a stretch preview: every point whose XY lies inside + /// the crossing window `[win_min, win_max]` is translated by `delta`; points + /// outside stay put. Exact for line/polyline vertices (the primary stretch + /// targets); curve tessellation points may deform where a window edge cuts + /// through them, matching the per-vertex nature of the operation. + pub fn stretched(&self, win_min: glam::Vec3, win_max: glam::Vec3, delta: glam::Vec3) -> Self { + let mut out = self.clone(); + out.name = format!("preview_{}", self.name); + out.color = Self::CYAN; + out.selected = false; + for p in &mut out.points { + if p[0] >= win_min.x && p[0] <= win_max.x && p[1] >= win_min.y && p[1] <= win_max.y { + p[0] += delta.x; + p[1] += delta.y; + p[2] += delta.z; + } + } + out + } + /// Return a clone mirrored across the line through `p1`→`p2`. pub fn mirrored(&self, p1: glam::Vec3, p2: glam::Vec3) -> Self { let ax = p2.x - p1.x;