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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-15 23:43:07 +03:00
commit e2be8197b4
3 changed files with 50 additions and 23 deletions

View file

@ -1567,7 +1567,8 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(cmd)); self.tabs[i].active_cmd = Some(Box::new(cmd));
} else { } else {
use crate::modules::home::modify::stretch::StretchCommand; 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.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd)); self.tabs[i].active_cmd = Some(Box::new(new_cmd));
} }

View file

@ -52,13 +52,15 @@ enum Step {
pub struct StretchCommand { pub struct StretchCommand {
handles: Vec<Handle>, handles: Vec<Handle>,
wire_models: Vec<WireModel>,
step: Step, step: Step,
} }
impl StretchCommand { impl StretchCommand {
pub fn new(handles: Vec<Handle>) -> Self { pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
Self { Self {
handles, handles,
wire_models,
step: Step::WindowCorner1, step: Step::WindowCorner1,
} }
} }
@ -128,7 +130,7 @@ impl CadCommand for StretchCommand {
CmdResult::Cancel CmdResult::Cancel
} }
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> { fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
match &self.step { match &self.step {
Step::WindowCorner2(c1) => { Step::WindowCorner2(c1) => {
// Show crossing-window rectangle preview (dashed green) // Show crossing-window rectangle preview (dashed green)
@ -140,31 +142,35 @@ impl CadCommand for StretchCommand {
[c1.x, pt.y, pt.z], [c1.x, pt.y, pt.z],
[c1.x, c1.y, c1.z], [c1.x, c1.y, c1.z],
]; ];
Some(WireModel::solid( vec![WireModel::solid(
"stretch_window".into(), "stretch_window".into(),
pts, pts,
[0.3, 1.0, 0.3, 0.7], [0.3, 1.0, 0.3, 0.7],
false, false,
)) )]
} }
Step::Target { base, .. } => Some(WireModel { Step::Target {
name: "rubber_band".into(), win_min,
points: vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]], win_max,
color: WireModel::CYAN, base,
selected: false, } => {
pattern_length: 0.0, let delta = pt - *base;
pattern: [0.0; 8], // Live ghost: vertices inside the crossing window follow the
line_weight_px: 1.0, // cursor, the rest stay anchored.
snap_pts: vec![], let mut out: Vec<WireModel> = self
tangent_geoms: vec![], .wire_models
aci: 0, .iter()
key_vertices: vec![], .map(|w| w.stretched(*win_min, *win_max, delta))
aabb: WireModel::UNBOUNDED_AABB, .collect();
plinegen: true, out.push(WireModel::solid(
vp_scissor: None, "rubber_band".into(),
fill_tris: vec![], vec![[base.x, base.y, base.z], [pt.x, pt.y, pt.z]],
}), WireModel::CYAN,
_ => None, false,
));
out
}
_ => vec![],
} }
} }
} }

View file

@ -155,6 +155,26 @@ impl WireModel {
out 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`. /// Return a clone mirrored across the line through `p1`→`p2`.
pub fn mirrored(&self, p1: glam::Vec3, p2: glam::Vec3) -> Self { pub fn mirrored(&self, p1: glam::Vec3, p2: glam::Vec3) -> Self {
let ax = p2.x - p1.x; let ax = p2.x - p1.x;