forked from 1b-agent/opencadstudio-upstream
fix(stretch): free crossing-window corners from Ortho, draw filled marquee (#291)
STRETCH picks its crossing window as two corner points. After the first corner set last_point, Ortho/Polar constrained the opposite corner to an axis through it, collapsing the window to a line and making selection impossible. Add CadCommand::window_corner_pick(); STRETCH returns true for its two window steps, and the viewport skips the ortho/polar (and OTRACK) lock for that point in both the preview and commit paths — the corners are now free. Also render the window like a normal box selection instead of a bare green outline: add SelectionState::preview_box (a hit-test-free marquee), draw it with the shared draw_marquee helper (green crossing fill + stroke), and drive it from the command's new window_first_corner(). Cleared on command end. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
824a508555
commit
e015fbe328
6 files changed
+105
-50
No files matched your search
@@ -909,6 +909,15 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.is_selection_gathering())
|
.map(|c| c.is_selection_gathering())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
// A selection-window corner (STRETCH crossing window) is a
|
||||||
|
// free point — Ortho/Polar must not pin it to an axis through
|
||||||
|
// the first corner, or the rectangle collapses to a line
|
||||||
|
// (#291).
|
||||||
|
let is_window_corner = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.window_corner_pick())
|
||||||
|
.unwrap_or(false);
|
||||||
let needs_tan = self.tabs[i]
|
let needs_tan = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -956,7 +965,10 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
Instant::now(),
|
Instant::now(),
|
||||||
);
|
);
|
||||||
if self.tabs[i].snap_result.is_none() {
|
if self.tabs[i].snap_result.is_none() {
|
||||||
let step = if self.polar_mode {
|
// A window corner is a free point, so neither the
|
||||||
|
// Polar increment nor the Ortho lock may pull the
|
||||||
|
// OTRACK alignment onto an axis (#291).
|
||||||
|
let step = if self.polar_mode && !is_window_corner {
|
||||||
Some(self.polar_increment_deg)
|
Some(self.polar_increment_deg)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -969,7 +981,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
bounds,
|
bounds,
|
||||||
step,
|
step,
|
||||||
self.last_point,
|
self.last_point,
|
||||||
self.ortho_mode,
|
self.ortho_mode && !is_window_corner,
|
||||||
ucs,
|
ucs,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -1018,7 +1030,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
let osnap_locked = self.tabs[i]
|
let osnap_locked = self.tabs[i]
|
||||||
.snap_result
|
.snap_result
|
||||||
.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid);
|
.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid);
|
||||||
if !osnap_locked {
|
if !osnap_locked && !is_window_corner {
|
||||||
if let Some(base) = self.last_point {
|
if let Some(base) = self.last_point {
|
||||||
let ucs_xf = self.tabs[i].ucs_xform();
|
let ucs_xf = self.tabs[i].ucs_xform();
|
||||||
if self.ortho_mode {
|
if self.ortho_mode {
|
||||||
@@ -1075,6 +1087,20 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
self.tabs[i].last_point_screen = lps;
|
self.tabs[i].last_point_screen = lps;
|
||||||
self.tabs[i].dyn_ref_screen = drs;
|
self.tabs[i].dyn_ref_screen = drs;
|
||||||
|
|
||||||
|
// Point-picked selection window (STRETCH): draw a filled
|
||||||
|
// crossing marquee from the first corner to the cursor so it
|
||||||
|
// reads like a normal box selection, not a bare outline (#291).
|
||||||
|
let window_first = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.window_first_corner());
|
||||||
|
self.tabs[i].scene.selection.borrow_mut().preview_box =
|
||||||
|
if is_window_corner {
|
||||||
|
window_first.map(|c1| (proj(c1.as_vec3()), p_full, true))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Entity-pick previews (TRIM/EXTEND/FILLET…) compare the
|
// Entity-pick previews (TRIM/EXTEND/FILLET…) compare the
|
||||||
// cursor against WCS document entities and return WCS wires.
|
// cursor against WCS document entities and return WCS wires.
|
||||||
// `effective` is offset-relative, so build a WCS copy for
|
// `effective` is offset-relative, so build a WCS copy for
|
||||||
@@ -1812,6 +1838,13 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.is_selection_gathering())
|
.map(|c| c.is_selection_gathering())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
// A committed window corner must stay free of the Ortho/Polar
|
||||||
|
// lock too, so the picked rectangle isn't flattened (#291).
|
||||||
|
let is_window_corner = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.window_corner_pick())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
if is_down && is_click && self.tabs[i].active_cmd.is_some() && !is_gathering {
|
if is_down && is_click && self.tabs[i].active_cmd.is_some() && !is_gathering {
|
||||||
let (vw, vh) = (tile_vw, tile_vh);
|
let (vw, vh) = (tile_vw, tile_vh);
|
||||||
@@ -1876,14 +1909,14 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
// OTRACK alignment wins over ortho/polar; otherwise apply
|
// OTRACK alignment wins over ortho/polar; otherwise apply
|
||||||
// ortho/polar relative to the last point.
|
// ortho/polar relative to the last point.
|
||||||
let otrack = if snap_hit.is_none() {
|
let otrack = if snap_hit.is_none() {
|
||||||
let step = if self.polar_mode {
|
let step = if self.polar_mode && !is_window_corner {
|
||||||
Some(self.polar_increment_deg)
|
Some(self.polar_increment_deg)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let ucs = self.tabs[i].scene.viewcube_ucs_mat();
|
let ucs = self.tabs[i].scene.viewcube_ucs_mat();
|
||||||
self.snapper
|
self.snapper
|
||||||
.otrack_snap(raw.as_vec3(), view_rot, eye, bounds, step, self.last_point, self.ortho_mode, ucs)
|
.otrack_snap(raw.as_vec3(), view_rot, eye, bounds, step, self.last_point, self.ortho_mode && !is_window_corner, ucs)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -1892,8 +1925,9 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
|||||||
if self.tabs[i].active_ucs.is_none() {
|
if self.tabs[i].active_ucs.is_none() {
|
||||||
pt.z = 0.0;
|
pt.z = 0.0;
|
||||||
}
|
}
|
||||||
} else if !snap_hit
|
} else if !is_window_corner
|
||||||
.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid)
|
&& !snap_hit
|
||||||
|
.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid)
|
||||||
{
|
{
|
||||||
// Object snap wins over ortho/polar — a snapped point
|
// Object snap wins over ortho/polar — a snapped point
|
||||||
// commits as-is. Grid snap still combines. (#132)
|
// commits as-is. Grid snap still combines. (#132)
|
||||||
|
|||||||
@@ -702,6 +702,25 @@ pub trait CadCommand: Send {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the current step picks a corner of a selection
|
||||||
|
/// *window* by point (e.g. STRETCH's crossing window). Such a pick must be a
|
||||||
|
/// free point: applying the Ortho/Polar lock would pin the opposite corner to
|
||||||
|
/// an axis through the first corner, collapsing the rectangle to a line and
|
||||||
|
/// making the window unusable. The host skips the ortho/polar constraint for
|
||||||
|
/// these steps. Default `false`. (#291)
|
||||||
|
fn window_corner_pick(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The already-picked first corner of the selection window (world space)
|
||||||
|
/// while `window_corner_pick()` is true and the opposite corner is being
|
||||||
|
/// dragged. The host projects it and draws a filled selection marquee to the
|
||||||
|
/// cursor, so a point-picked window (STRETCH) reads like a normal box
|
||||||
|
/// selection. `None` before the first corner is set. (#291)
|
||||||
|
fn window_first_corner(&self) -> Option<glam::DVec3> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` when the command wants object picks via Tangent snap.
|
/// Returns `true` when the command wants object picks via Tangent snap.
|
||||||
fn needs_tangent_pick(&self) -> bool {
|
fn needs_tangent_pick(&self) -> bool {
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -130,25 +130,27 @@ impl CadCommand for StretchCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn window_corner_pick(&self) -> bool {
|
||||||
|
// The two crossing-window corners are free points; Ortho/Polar must not
|
||||||
|
// pin the opposite corner to an axis or the window becomes a line (#291).
|
||||||
|
matches!(self.step, Step::WindowCorner1 | Step::WindowCorner2(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_first_corner(&self) -> Option<DVec3> {
|
||||||
|
// Expose the first corner so the host draws a filled crossing marquee to
|
||||||
|
// the cursor, matching a normal box selection instead of a bare outline.
|
||||||
|
match &self.step {
|
||||||
|
Step::WindowCorner2(c1) => Some(c1.as_dvec3()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> { let pt = pt.as_vec3();
|
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> { let pt = pt.as_vec3();
|
||||||
match &self.step {
|
match &self.step {
|
||||||
Step::WindowCorner2(c1) => {
|
// The crossing-window rectangle is drawn as a filled selection
|
||||||
// Show crossing-window rectangle preview (dashed green)
|
// marquee by the host (via window_first_corner) so it matches a
|
||||||
let c1 = *c1;
|
// normal box selection — nothing to draw here. (#291)
|
||||||
let pts = vec![
|
Step::WindowCorner2(_) => 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],
|
|
||||||
];
|
|
||||||
vec![WireModel::solid(
|
|
||||||
"stretch_window".into(),
|
|
||||||
pts,
|
|
||||||
[0.3, 1.0, 0.3, 0.7],
|
|
||||||
false,
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
Step::Target {
|
Step::Target {
|
||||||
win_min,
|
win_min,
|
||||||
win_max,
|
win_max,
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ pub struct SelectionState {
|
|||||||
pub box_last: Option<(Point, Point)>,
|
pub box_last: Option<(Point, Point)>,
|
||||||
pub box_crossing: bool,
|
pub box_crossing: bool,
|
||||||
pub box_last_crossing: bool,
|
pub box_last_crossing: bool,
|
||||||
|
/// A preview-only selection marquee `(anchor, current, crossing)` in canvas
|
||||||
|
/// pixels, drawn identically to a real box-selection (green crossing fill /
|
||||||
|
/// blue window fill) but with NO hit-test behaviour. Commands that pick a
|
||||||
|
/// window by point (STRETCH's crossing window) set this so the region reads
|
||||||
|
/// like a normal selection instead of a bare outline. (#291)
|
||||||
|
pub preview_box: Option<(Point, Point, bool)>,
|
||||||
pub poly_active: bool,
|
pub poly_active: bool,
|
||||||
pub poly_points: Vec<Point>,
|
pub poly_points: Vec<Point>,
|
||||||
pub poly_crossing: bool,
|
pub poly_crossing: bool,
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ impl Scene {
|
|||||||
self.preview_wires = vec![];
|
self.preview_wires = vec![];
|
||||||
self.preview_text = vec![];
|
self.preview_text = vec![];
|
||||||
self.interim_wire = None;
|
self.interim_wire = None;
|
||||||
|
// Drop any point-picked window marquee (STRETCH) so it doesn't linger
|
||||||
|
// after the command ends before the next mouse move. (#291)
|
||||||
|
self.selection.borrow_mut().preview_box = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wire_models_for(&self, handles: &[acadrust::Handle]) -> Vec<WireModel> {
|
pub fn wire_models_for(&self, handles: &[acadrust::Handle]) -> Vec<WireModel> {
|
||||||
|
|||||||
+17
-26
@@ -430,36 +430,19 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) {
|
// Draw a selection marquee (green crossing / blue window) as a filled,
|
||||||
let (fill, stroke) = if self.selection.box_crossing {
|
// stroked rectangle between two canvas points. Shared by the live
|
||||||
|
// box-selection and the preview-only window marquee (#291).
|
||||||
|
fn draw_marquee(frame: &mut canvas::Frame, a: Point, b: Point, crossing: bool) {
|
||||||
|
let (fill, stroke) = if crossing {
|
||||||
(
|
(
|
||||||
Color {
|
Color { r: 0.20, g: 0.72, b: 0.44, a: 0.12 },
|
||||||
r: 0.20,
|
Color { r: 0.20, g: 0.72, b: 0.44, a: 0.9 },
|
||||||
g: 0.72,
|
|
||||||
b: 0.44,
|
|
||||||
a: 0.12,
|
|
||||||
},
|
|
||||||
Color {
|
|
||||||
r: 0.20,
|
|
||||||
g: 0.72,
|
|
||||||
b: 0.44,
|
|
||||||
a: 0.9,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
Color {
|
Color { r: 0.20, g: 0.44, b: 0.72, a: 0.12 },
|
||||||
r: 0.20,
|
Color { r: 0.20, g: 0.44, b: 0.72, a: 0.9 },
|
||||||
g: 0.44,
|
|
||||||
b: 0.72,
|
|
||||||
a: 0.12,
|
|
||||||
},
|
|
||||||
Color {
|
|
||||||
r: 0.20,
|
|
||||||
g: 0.44,
|
|
||||||
b: 0.72,
|
|
||||||
a: 0.9,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let x0 = a.x.min(b.x);
|
let x0 = a.x.min(b.x);
|
||||||
@@ -478,6 +461,14 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let (Some(a), Some(b)) = (self.selection.box_anchor, self.selection.box_current) {
|
||||||
|
draw_marquee(&mut frame, a, b, self.selection.box_crossing);
|
||||||
|
}
|
||||||
|
// Preview marquee for point-picked windows (STRETCH) — same look, no pick.
|
||||||
|
if let Some((a, b, crossing)) = self.selection.preview_box {
|
||||||
|
draw_marquee(&mut frame, a, b, crossing);
|
||||||
|
}
|
||||||
|
|
||||||
if self.selection.poly_active && self.selection.poly_points.len() > 1 {
|
if self.selection.poly_active && self.selection.poly_points.len() > 1 {
|
||||||
let (fill, stroke) = if self.selection.poly_crossing {
|
let (fill, stroke) = if self.selection.poly_crossing {
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in new issue
Block a user