refactor(selection): split 31-field SelectionState into five sub-structs

SelectionState now holds view, gesture, input, menu and orbit sub-structs; all 450 access sites updated mechanically with no behavior change. Deletes three write-only dead fields (box_last, box_last_crossing, poly_last_crossing). Hand-rolled clears in commands and dim_viewport route through the new clear methods; bench fixture updated. Full suite: 1976 passed / 0 failed / 25 ignored.
This commit is contained in:
Karim Jerbi committed 2026-10-06 20:22:04 +01:00
1 parent 8c2cfa2c8f
commit 7aafd9c710
27 files changed
+512 -469

No files matched your search

+19 -6
View File
@@ -50,7 +50,9 @@ use OpenCADStudio::scene::parametric_constraints::{
};
use OpenCADStudio::scene::pick::hit_test::{box_hit, click_hit};
use OpenCADStudio::scene::pick::interaction_index::InteractionIndex;
use OpenCADStudio::scene::pick::selection_state::SelectionState;
use OpenCADStudio::scene::pick::selection_state::{
SelectionGesture, SelectionState, SelectionView,
};
use OpenCADStudio::scene::pipeline::wire_arena::partition_wires;
use OpenCADStudio::scene::view::camera::Camera;
use OpenCADStudio::scene::{ChangeKind, Scene};
@@ -713,12 +715,17 @@ fn bench_selection_cloning(runner: &mut BenchmarkRunner) {
}
let mut state = SelectionState {
vp_size: (1920.0, 1080.0),
poly_points: vec![Point::new(10.0, 10.0); 64],
view: SelectionView {
vp_size: (1920.0, 1080.0),
},
gesture: SelectionGesture {
box_anchor: Some(Point::new(0.0, 0.0)),
box_current: Some(Point::new(100.0, 100.0)),
poly_points: vec![Point::new(10.0, 10.0); 64],
..Default::default()
},
..Default::default()
};
state.box_anchor = Some(Point::new(0.0, 0.0));
state.box_current = Some(Point::new(100.0, 100.0));
let n = if runner.quick_mode { 1_000 } else { 5_000 };
let runs = 5;
@@ -793,6 +800,7 @@ fn bench_ui_ribbon_view_construction(runner: &mut BenchmarkRunner) {
visible: true,
frozen: false,
locked: false,
vp_frozen: None,
},
LayerInfo {
name: "DRAWING".to_string(),
@@ -800,6 +808,7 @@ fn bench_ui_ribbon_view_construction(runner: &mut BenchmarkRunner) {
visible: true,
frozen: false,
locked: false,
vp_frozen: None,
},
LayerInfo {
name: "DIMENSIONS".to_string(),
@@ -807,6 +816,7 @@ fn bench_ui_ribbon_view_construction(runner: &mut BenchmarkRunner) {
visible: true,
frozen: false,
locked: false,
vp_frozen: None,
},
LayerInfo {
name: "ANNOTATIONS".to_string(),
@@ -814,6 +824,7 @@ fn bench_ui_ribbon_view_construction(runner: &mut BenchmarkRunner) {
visible: true,
frozen: false,
locked: false,
vp_frozen: None,
},
],
"0",
@@ -1371,7 +1382,7 @@ fn bench_ui_constraint_glyphs(runner: &mut BenchmarkRunner) {
);
scene.note_parametric_constraint_applied(ParametricScope::ModelSpace, id, 3);
}
scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
scene.selection.borrow_mut().view.vp_size = (1920.0, 1080.0);
let vp = (1920.0_f32, 1080.0_f32);
// Prime the cache so every timed call below is a hit (same key, same Arc).
@@ -1526,6 +1537,8 @@ fn build_selection_overlay_element(
snap_angle_deg: 0.0,
point_mode: false,
pick_pending: false,
hide_arms: false,
snap_color: None,
},
SelectionVisualOptions::default(),
empty_glyphs,
+8 -8
View File
@@ -1757,7 +1757,7 @@ mod tests {
);
assert_eq!(text["added"], 1, "{text}");
app.tabs[i].scene.selection.borrow_mut().vp_size = (1600.0, 800.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 800.0);
app.tabs[i].scene.fit_all();
let (_, max) = app.tabs[i]
.scene
@@ -2775,7 +2775,7 @@ mod tests {
app.pick_add = add;
app.pick_drag_rect = rect;
let _ = app.run_command_line("LINE 0,0 10,10");
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let _ = app.run_command_line("ZOOM EXTENTS");
// Both directions. Crossing = a right → left diagonal sweep (the
// freeform path may be degenerate — crossing counts hits).
@@ -2819,16 +2819,16 @@ mod tests {
}
{
let sel = app.tabs[i].scene.selection.borrow();
assert!(sel.left_dragging, "drag must start (add={add} rect={rect})");
assert!(sel.input.left_dragging, "drag must start (add={add} rect={rect})");
if rect {
// Rectangle mode drives the box machinery, not the lasso.
assert!(
sel.box_anchor.is_some() && sel.box_current.is_some() && !sel.poly_active,
sel.gesture.box_anchor.is_some() && sel.gesture.box_current.is_some() && !sel.gesture.poly_active,
"rect marquee must arm the box (add={add})"
);
} else {
assert!(sel.poly_active, "lasso must start (add={add})");
assert!(sel.poly_points.len() >= 3, "lasso points (add={add})");
assert!(sel.gesture.poly_active, "lasso must start (add={add})");
assert!(sel.gesture.poly_points.len() >= 3, "lasso points (add={add})");
}
}
let _ = app.update(Message::ViewportLeftRelease);
@@ -3157,7 +3157,7 @@ mod tests {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
{
app.tabs[0].scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
app.tabs[0].scene.selection.borrow_mut().view.vp_size = (1920.0, 1080.0);
app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0);
}
@@ -3226,7 +3226,7 @@ mod tests {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
{
app.tabs[0].scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
app.tabs[0].scene.selection.borrow_mut().view.vp_size = (1920.0, 1080.0);
app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0);
}
+1 -1
View File
@@ -1148,7 +1148,7 @@ impl OpenCADStudio {
// The command picked the path; the hit test lives here, where
// the camera and the drawing's geometry are. Everything after
// matches what a lasso does, Remove included.
let canvas = self.tabs[i].scene.selection.borrow().vp_size;
let canvas = self.tabs[i].scene.selection.borrow().view.vp_size;
let bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
+3 -3
View File
@@ -364,7 +364,7 @@ impl OpenCADStudio {
.is_some_and(|command| command.wants_point_pick_context())
{
let scene = &self.tabs[i].scene;
let size = scene.selection.borrow().vp_size;
let size = scene.selection.borrow().view.vp_size;
let edit = scene.viewport_edit_frame(size);
let tile = edit
.as_ref()
@@ -919,8 +919,8 @@ impl OpenCADStudio {
let crossing = matches!(kw.as_str(), "C" | "CROSSING");
{
let mut selection = self.tabs[i].scene.selection.borrow_mut();
selection.box_crossing = crossing;
selection.box_crossing_locked = true;
selection.gesture.box_crossing = crossing;
selection.gesture.box_crossing_locked = true;
}
let hint = if crossing {
crate::t!("Crossing: specify first corner.")
+45 -56
View File
@@ -80,7 +80,7 @@ impl OpenCADStudio {
.scene
.selection
.borrow_mut()
.right_click_entered = false;
.input.right_click_entered = false;
// A fresh command starts at the polar/cartesian default — clear
// any `,`-driven reshape and locked dynamic-input values from a
// previous command. Otherwise a bare Enter on the first point prompt
@@ -184,18 +184,7 @@ impl OpenCADStudio {
{
let mut selection = self.tabs[i].scene.selection.borrow_mut();
// Cancel the active selection gesture before the new command starts.
selection.left_down = false;
selection.left_press_pos = None;
selection.left_press_time = None;
selection.left_dragging = false;
selection.box_anchor = None;
selection.box_anchor_world = None;
selection.box_current = None;
selection.box_crossing = false;
selection.box_crossing_locked = false;
selection.poly_active = false;
selection.poly_points.clear();
selection.poly_crossing = false;
selection.clear_left_selection_gesture();
}
// Cancel any running command before starting a new one.
if self.tabs[i].active_cmd.is_some() {
@@ -961,31 +950,31 @@ mod marquee_cancel_tests {
fn arm_marquee(app: &mut OpenCADStudio) {
let i = app.active_tab;
let mut sel = app.tabs[i].scene.selection.borrow_mut();
sel.left_down = true;
sel.left_press_pos = Some(iced::Point::new(10.0, 10.0));
sel.left_press_time = Some(Instant::now());
sel.left_dragging = true;
sel.box_anchor = Some(iced::Point::new(10.0, 10.0));
sel.box_anchor_world = Some(glam::DVec3::new(1.0, 2.0, 0.0));
sel.box_current = Some(iced::Point::new(40.0, 40.0));
sel.box_crossing = true;
sel.box_crossing_locked = true;
sel.input.left_down = true;
sel.input.left_press_pos = Some(iced::Point::new(10.0, 10.0));
sel.input.left_press_time = Some(Instant::now());
sel.input.left_dragging = true;
sel.gesture.box_anchor = Some(iced::Point::new(10.0, 10.0));
sel.gesture.box_anchor_world = Some(glam::DVec3::new(1.0, 2.0, 0.0));
sel.gesture.box_current = Some(iced::Point::new(40.0, 40.0));
sel.gesture.box_crossing = true;
sel.gesture.box_crossing_locked = true;
}
/// Arm a held lasso drag.
fn arm_lasso(app: &mut OpenCADStudio) {
let i = app.active_tab;
let mut sel = app.tabs[i].scene.selection.borrow_mut();
sel.left_down = true;
sel.left_press_pos = Some(iced::Point::new(10.0, 10.0));
sel.left_press_time = Some(Instant::now());
sel.left_dragging = true;
sel.poly_active = true;
sel.poly_points = vec![
sel.input.left_down = true;
sel.input.left_press_pos = Some(iced::Point::new(10.0, 10.0));
sel.input.left_press_time = Some(Instant::now());
sel.input.left_dragging = true;
sel.gesture.poly_active = true;
sel.gesture.poly_points = vec![
iced::Point::new(10.0, 10.0),
iced::Point::new(20.0, 30.0),
];
sel.poly_crossing = true;
sel.gesture.poly_crossing = true;
}
#[test]
@@ -997,15 +986,15 @@ mod marquee_cancel_tests {
let _ = app.dispatch_command("LINE");
let sel = app.tabs[i].scene.selection.borrow();
assert!(!sel.left_down);
assert!(sel.left_press_pos.is_none());
assert!(sel.left_press_time.is_none());
assert!(!sel.left_dragging);
assert!(sel.box_anchor.is_none());
assert!(sel.box_anchor_world.is_none());
assert!(sel.box_current.is_none());
assert!(!sel.box_crossing);
assert!(!sel.box_crossing_locked);
assert!(!sel.input.left_down);
assert!(sel.input.left_press_pos.is_none());
assert!(sel.input.left_press_time.is_none());
assert!(!sel.input.left_dragging);
assert!(sel.gesture.box_anchor.is_none());
assert!(sel.gesture.box_anchor_world.is_none());
assert!(sel.gesture.box_current.is_none());
assert!(!sel.gesture.box_crossing);
assert!(!sel.gesture.box_crossing_locked);
}
#[test]
@@ -1017,13 +1006,13 @@ mod marquee_cancel_tests {
let _ = app.dispatch_command("LINE");
let sel = app.tabs[i].scene.selection.borrow();
assert!(!sel.left_down);
assert!(sel.left_press_pos.is_none());
assert!(sel.left_press_time.is_none());
assert!(!sel.left_dragging);
assert!(!sel.poly_active);
assert!(sel.poly_points.is_empty());
assert!(!sel.poly_crossing);
assert!(!sel.input.left_down);
assert!(sel.input.left_press_pos.is_none());
assert!(sel.input.left_press_time.is_none());
assert!(!sel.input.left_dragging);
assert!(!sel.gesture.poly_active);
assert!(sel.gesture.poly_points.is_empty());
assert!(!sel.gesture.poly_crossing);
}
#[test]
@@ -1039,8 +1028,8 @@ mod marquee_cancel_tests {
Some("LINE")
);
let sel = app.tabs[i].scene.selection.borrow();
assert!(sel.box_anchor.is_none());
assert!(sel.box_anchor_world.is_none());
assert!(sel.gesture.box_anchor.is_none());
assert!(sel.gesture.box_anchor_world.is_none());
}
#[test]
@@ -1052,15 +1041,15 @@ mod marquee_cancel_tests {
let _ = app.dispatch_command("ORTHO");
let sel = app.tabs[i].scene.selection.borrow();
assert!(sel.left_down);
assert!(sel.left_press_pos.is_some());
assert!(sel.left_press_time.is_some());
assert!(sel.left_dragging);
assert!(sel.box_anchor.is_some());
assert!(sel.box_anchor_world.is_some());
assert!(sel.box_current.is_some());
assert!(sel.box_crossing);
assert!(sel.box_crossing_locked);
assert!(sel.input.left_down);
assert!(sel.input.left_press_pos.is_some());
assert!(sel.input.left_press_time.is_some());
assert!(sel.input.left_dragging);
assert!(sel.gesture.box_anchor.is_some());
assert!(sel.gesture.box_anchor_world.is_some());
assert!(sel.gesture.box_current.is_some());
assert!(sel.gesture.box_crossing);
assert!(sel.gesture.box_crossing_locked);
}
#[test]
+1 -1
View File
@@ -1496,7 +1496,7 @@ impl OpenCADStudio {
.filter(|v| v.is_finite())
.ok_or_else(|| failure("invalid_point", "Missing finite y"))?
as f32;
let size = self.tabs[self.active_tab].scene.selection.borrow().vp_size;
let size = self.tabs[self.active_tab].scene.selection.borrow().view.vp_size;
if x < 0. || y < 0. || x > size.0 || y > size.1 {
return Err(failure(
"outside_viewport",
+4 -4
View File
@@ -439,7 +439,7 @@ impl OpenCADStudio {
"layout":tab.scene.current_layout,
"ucs":tab.active_ucs.as_ref().map(|u|json!({"name":u.name,"origin":[u.origin.x,u.origin.y,u.origin.z],"x_axis":[u.x_axis.x,u.x_axis.y,u.x_axis.z],"y_axis":[u.y_axis.x,u.y_axis.y,u.y_axis.z],"elevation":u.elevation})),
"cursor":{"world":tab.last_cursor_world.to_array(),"screen":[tab.last_cursor_screen.x,tab.last_cursor_screen.y]},
"viewport_size":({let s=tab.scene.selection.borrow();[s.vp_size.0,s.vp_size.1]}),
"viewport_size":({let s=tab.scene.selection.borrow();[s.view.vp_size.0,s.view.vp_size.1]}),
"camera":({let c=tab.scene.camera.borrow();json!({"target":c.target.to_array(),"rotation":[c.rotation.x,c.rotation.y,c.rotation.z,c.rotation.w],"distance":c.distance,"fov_y":c.fov_y,"projection":format!("{:?}",c.projection),"yaw":c.yaw,"pitch":c.pitch})}),
"mtext_editor":self.mtext_editor.as_ref().map(|e|json!({"text":e.content.text(),"height":e.height,"style":e.style})),
"text_editor":self.text_inline.is_some(),"event_cursor":self.control.serial,
@@ -1358,7 +1358,7 @@ impl OpenCADStudio {
.ok_or("Renderer returned malformed image data")?;
let mut actual_scope = "window";
let (vp_w, vp_h) = self.tabs[self.active_tab].scene.selection.borrow().vp_size;
let (vp_w, vp_h) = self.tabs[self.active_tab].scene.selection.borrow().view.vp_size;
let mut vp_logical_width = if vp_w > 0.0 { vp_w } else { image.width() as f32 / s.scale_factor };
let mut vp_logical_height = if vp_h > 0.0 { vp_h } else { image.height() as f32 / s.scale_factor };
@@ -2138,7 +2138,7 @@ mod tests {
let i = app.active_tab;
{
let tab = &mut app.tabs[i];
tab.scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
tab.scene.selection.borrow_mut().view.vp_size = (1920.0, 1080.0);
tab.scene.sync_tiles_from_panes(1920.0, 1080.0);
tab.scene.fit_all(); // bring the line into the pane so snaps can hit
}
@@ -2200,7 +2200,7 @@ mod tests {
let i = app.active_tab;
{
let tab = &mut app.tabs[i];
tab.scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
tab.scene.selection.borrow_mut().view.vp_size = (1920.0, 1080.0);
tab.scene.sync_tiles_from_panes(1920.0, 1080.0);
tab.scene.fit_all();
}
+1 -1
View File
@@ -604,7 +604,7 @@ mod tests {
app.automation_op(r#"{"op":"run","cmd":"LINE -10,-10 10,10"}"#);
// Set test canvas viewport size
app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[0].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
app.tabs[0].scene.fit_all();
let grounding = compute_grounding(&app, 800.0, 600.0, 800, 600);
+1 -4
View File
@@ -20,10 +20,7 @@ pub(crate) enum DimensionMeasureSpace {
impl OpenCADStudio {
pub(crate) fn finish_command_click(&mut self, i: usize) {
let mut selection = self.tabs[i].scene.selection.borrow_mut();
selection.left_down = false;
selection.left_press_pos = None;
selection.left_press_time = None;
selection.left_dragging = false;
selection.input.clear_left_buttons();
}
pub(crate) fn dimension_measure_space(&self, i: usize) -> DimensionMeasureSpace {
+1 -1
View File
@@ -721,7 +721,7 @@ impl super::OpenCADStudio {
return;
}
let mut state = MTextEditorState::new(pos, initial, height, handle);
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos {
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().input.last_move_pos {
state.screen_anchor = p;
}
// Seed attachment / line-spacing / box width from the entity being edited.
+7 -7
View File
@@ -99,7 +99,7 @@ impl OpenCADStudio {
// after it takes keyboard focus. Gate input separately from its metadata.
let allowed = self.spacemouse_focused && self.active_modal.is_none();
let camera = scene.navigation_camera();
let (w, h) = scene.selection.borrow().vp_size;
let (w, h) = scene.selection.borrow().view.vp_size;
let bounds = scene.active_model_tile_bounds(w, h);
let aspect = scene
.active_viewport
@@ -211,17 +211,17 @@ impl OpenCADStudio {
.acknowledge(&view.target, &self.navigation_target());
self.spacemouse_pivot = (view.pivot_visible && moving)
.then_some((self.navigation_target(), view.pivot));
let (w, h) = self.tabs[i].scene.selection.borrow().vp_size;
let (w, h) = self.tabs[i].scene.selection.borrow().view.vp_size;
self.reproject_box_anchor(i, w, h);
// The pointer may be stationary while the left hand navigates.
// Refresh the active command's snap/preview against the new
// camera, using the existing pointer path without a click.
let cursor = {
let selection = self.tabs[i].scene.selection.borrow();
(!selection.left_down
&& !selection.middle_down
(!selection.input.left_down
&& !selection.input.middle_down
&& self.tabs[i].active_cmd.is_some())
.then_some(selection.last_move_pos)
.then_some(selection.input.last_move_pos)
.flatten()
};
if let Some(cursor) = cursor {
@@ -246,7 +246,7 @@ impl OpenCADStudio {
return None;
}
let scene = &self.tabs[self.active_tab].scene;
let (w, h) = scene.selection.borrow().vp_size;
let (w, h) = scene.selection.borrow().view.vp_size;
let rect = scene
.active_viewport
.and_then(|handle| scene.viewport_screen_rect(handle, (w, h)))
@@ -367,7 +367,7 @@ mod tests {
.scene
.selection
.borrow_mut()
.vp_size = (800., 600.);
.view.vp_size = (800., 600.);
app.spacemouse_focused = true;
app.spacemouse.test_connect();
app.sync_spacemouse();
+1 -1
View File
@@ -2065,7 +2065,7 @@ impl OpenCADStudio {
let doc = &scene.document;
let header = &doc.header;
let camera = scene.camera.borrow();
let (viewport_width, viewport_height) = scene.selection.borrow().vp_size;
let (viewport_width, viewport_height) = scene.selection.borrow().view.vp_size;
let aspect = if viewport_height > 0.0 {
viewport_width as f64 / viewport_height as f64
} else {
+1 -1
View File
@@ -214,7 +214,7 @@ impl super::OpenCADStudio {
creation,
screen_anchor: iced::Point::new(60.0, 90.0),
};
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos {
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().input.last_move_pos {
state.screen_anchor = p;
}
self.text_inline = Some(state);
+14 -14
View File
@@ -1177,13 +1177,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.tabs[i].zoom_dynamic_mode = false;
{
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.middle_down = false;
sel.middle_last_pos = None;
sel.orbit_pivot = None;
sel.box_anchor = None;
sel.box_anchor_world = None;
sel.box_current = None;
sel.box_crossing_locked = false;
sel.input.middle_down = false;
sel.input.middle_last_pos = None;
sel.orbit.pivot = None;
sel.gesture.box_anchor = None;
sel.gesture.box_anchor_world = None;
sel.gesture.box_current = None;
sel.gesture.box_crossing_locked = false;
}
if was_pan {
self.command_line.push_output(crate::t!("PAN ended.").as_ref());
@@ -1230,8 +1230,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
{
let mut sel = self.tabs[i_e].scene.selection.borrow_mut();
if sel.context_menu.is_some() {
sel.context_menu = None;
if sel.menu.open_at.is_some() {
sel.menu.open_at = None;
return Task::none();
}
}
@@ -1261,13 +1261,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.tabs[i].scene.deselect_all();
self.refresh_properties();
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.box_anchor = None;
sel.gesture.box_anchor = None;
// Also drop the world-space anchor, or the next pan/zoom
// re-projects it back into `box_anchor` and the cancelled
// marquee springs back to life (reproject_box_anchor).
sel.box_anchor_world = None;
sel.box_current = None;
sel.box_crossing = false;
sel.gesture.box_anchor_world = None;
sel.gesture.box_current = None;
sel.gesture.box_crossing = false;
}
Task::none()
}
@@ -2086,7 +2086,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
pub(super) fn on_qselect_open(&mut self) -> Task<Message> {
let i = self.active_tab;
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
self.tabs[i].scene.selection.borrow_mut().menu.open_at = None;
let remembered = self.qselect_settings.clone();
let scope = remembered.as_ref().map_or(
crate::app::QSelectScope::CurrentSpace,
+20 -20
View File
@@ -19,7 +19,7 @@ impl OpenCADStudio {
pub(in crate::app) fn context_menu_open(&self) -> bool {
self.tabs
.get(self.active_tab)
.is_some_and(|tab| tab.scene.selection.borrow().context_menu.is_some())
.is_some_and(|tab| tab.scene.selection.borrow().menu.open_at.is_some())
}
/// Everything the menu builder needs, read from the active tab. Called by
@@ -71,7 +71,7 @@ impl OpenCADStudio {
.scene
.selection
.borrow()
.context_menu_ui
.menu.ui
.open_submenu;
build_context_menu(&self.context_menu_context(), open)
}
@@ -80,10 +80,10 @@ impl OpenCADStudio {
pub(in crate::app) fn close_context_menu(&mut self) -> Task<Message> {
let i = self.active_tab;
let mut sel = self.tabs[i].scene.selection.borrow_mut();
if sel.context_menu.is_none() {
if sel.menu.open_at.is_none() {
return Task::none();
}
sel.close_context_menu();
sel.menu.close();
drop(sel);
self.focus_cmd_input()
}
@@ -91,7 +91,7 @@ impl OpenCADStudio {
pub(in crate::app) fn on_context_menu_submenu_toggle(&mut self, id: SubmenuId) -> Task<Message> {
let i = self.active_tab;
let mut sel = self.tabs[i].scene.selection.borrow_mut();
let ui = &mut sel.context_menu_ui;
let ui = &mut sel.menu.ui;
ui.open_submenu = if ui.open_submenu == Some(id) { None } else { Some(id) };
drop(sel);
// Keep the keyboard highlight on the header that was toggled.
@@ -101,8 +101,8 @@ impl OpenCADStudio {
.iter()
.position(|s| s.header == Some(id));
let mut sel = self.tabs[i].scene.selection.borrow_mut();
if sel.context_menu_ui.highlighted.is_some() {
sel.context_menu_ui.highlighted = header;
if sel.menu.ui.highlighted.is_some() {
sel.menu.ui.highlighted = header;
}
Task::none()
}
@@ -118,10 +118,10 @@ impl OpenCADStudio {
let i = self.active_tab;
{
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.close_context_menu();
sel.menu.close();
// A pick counts as "another interaction" for the Enter-first
// right-click cycle: the next right-click acts as Enter again.
sel.right_click_entered = false;
sel.input.right_click_entered = false;
}
let focus = self.focus_cmd_input();
let task = match action {
@@ -174,7 +174,7 @@ impl OpenCADStudio {
if rows.is_empty() {
return Task::none();
}
let current = self.tabs[i].scene.selection.borrow().context_menu_ui.highlighted;
let current = self.tabs[i].scene.selection.borrow().menu.ui.highlighted;
let next_enabled = |from: usize, step: isize| -> usize {
let n = rows.len() as isize;
let mut idx = from as isize;
@@ -193,7 +193,7 @@ impl OpenCADStudio {
None => menu.default_index(),
Some(cur) => next_enabled(cur, 1),
};
self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = Some(idx);
self.tabs[i].scene.selection.borrow_mut().menu.ui.highlighted = Some(idx);
Task::none()
}
ContextMenuNav::Up => {
@@ -201,7 +201,7 @@ impl OpenCADStudio {
None => menu.default_index(),
Some(cur) => next_enabled(cur, -1),
};
self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = Some(idx);
self.tabs[i].scene.selection.borrow_mut().menu.ui.highlighted = Some(idx);
Task::none()
}
ContextMenuNav::Enter => {
@@ -220,7 +220,7 @@ impl OpenCADStudio {
if unique {
self.on_context_menu_pick(rows[idx].action.clone())
} else {
self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted =
self.tabs[i].scene.selection.borrow_mut().menu.ui.highlighted =
Some(idx);
Task::none()
}
@@ -244,7 +244,7 @@ impl OpenCADStudio {
ArrowKey::Left | ArrowKey::Right => {
// On a submenu header, Right expands and Left collapses.
let i = self.active_tab;
let ui = self.tabs[i].scene.selection.borrow().context_menu_ui.clone();
let ui = self.tabs[i].scene.selection.borrow().menu.ui.clone();
let menu = self.current_context_menu();
let rows = menu.selectable();
let header = ui.highlighted.and_then(|c| rows.get(c)).and_then(|r| r.header);
@@ -410,7 +410,7 @@ mod tests {
fn app() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[0].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let _ = app.update(Message::ViewportMove(Point::new(200.0, 150.0)));
app
}
@@ -428,7 +428,7 @@ mod tests {
}
fn menu_open(app: &OpenCADStudio) -> bool {
app.tabs[0].scene.selection.borrow().context_menu.is_some()
app.tabs[0].scene.selection.borrow().menu.open_at.is_some()
}
fn active(app: &OpenCADStudio) -> Option<&'static str> {
@@ -491,7 +491,7 @@ mod tests {
let _ = app.update(Message::ViewportRightPress);
{
let mut sel = app.tabs[0].scene.selection.borrow_mut();
sel.right_press_time = iced::time::Instant::now()
sel.input.right_press_time = iced::time::Instant::now()
.checked_sub(std::time::Duration::from_millis(400));
}
let _ = app.update(Message::ViewportRightRelease);
@@ -558,7 +558,7 @@ mod tests {
let _ = app.update(Message::ContextMenuPick(MenuAction::Command("LINE".into())));
assert!(!menu_open(&app));
assert_eq!(active(&app), Some("LINE"));
assert!(!app.tabs[0].scene.selection.borrow().right_click_entered);
assert!(!app.tabs[0].scene.selection.borrow().input.right_click_entered);
});
}
@@ -582,7 +582,7 @@ mod tests {
right_click(&mut app);
let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Down)); // Enter row
assert_eq!(
app.tabs[0].scene.selection.borrow().context_menu_ui.highlighted,
app.tabs[0].scene.selection.borrow().menu.ui.highlighted,
Some(0)
);
let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Down)); // Cancel
@@ -853,7 +853,7 @@ mod transparent_tests {
fn line_app() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[0].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let _ = app.dispatch_command("LINE");
let _ = app.feed_command(StepInput::Point(DVec3::ZERO));
app
+3 -3
View File
@@ -640,7 +640,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
// produced a one-time jump plus a stuck drag. Column moves
// arrive via Message::XrefColMove only.
if self.dock_dragging.is_some() {
let avail = self.tabs[self.active_tab].scene.selection.borrow().vp_size.1;
let avail = self.tabs[self.active_tab].scene.selection.borrow().view.vp_size.1;
let side = if point.x < self.win_size.0 * 0.5 {
DockSide::Left
} else {
@@ -1671,7 +1671,7 @@ mod tests {
app.show_block_palette = false;
app.show_properties = true;
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (1600.0, 900.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
app.win_size = (1600.0, 900.0).into();
let id = crate::ui::dock::PanelId::Properties;
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
@@ -1698,7 +1698,7 @@ mod tests {
app.show_block_palette = true;
app.show_properties = true;
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (1600.0, 900.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 900.0);
app.win_size = (1600.0, 900.0).into();
let id = crate::ui::dock::PanelId::Properties;
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
+2 -2
View File
@@ -2014,7 +2014,7 @@ impl OpenCADStudio {
#[cfg(not(target_arch = "wasm32"))]
let interaction_task = {
let wires = self.tabs[i].scene.hit_test_wires();
let screen_height = self.tabs[i].scene.selection.borrow().vp_size.1;
let screen_height = self.tabs[i].scene.selection.borrow().view.vp_size.1;
self.prepare_interaction_index_task(i, wires, screen_height)
.unwrap_or_else(Task::none)
};
@@ -5574,7 +5574,7 @@ impl OpenCADStudio {
/// canvas outside the sheet, matching Model-space plotting.
fn display_plot_window(&self) -> Option<(f64, f64, f64, f64)> {
let scene = &self.tabs[self.active_tab].scene;
let (canvas_w, canvas_h) = scene.selection.borrow().vp_size;
let (canvas_w, canvas_h) = scene.selection.borrow().view.vp_size;
let viewport = if scene.current_layout == "Model" {
scene.active_model_tile_bounds(canvas_w, canvas_h)
} else {
+36 -36
View File
@@ -963,7 +963,7 @@ impl OpenCADStudio {
.scene
.selection
.borrow_mut()
.context_menu = None;
.menu.open_at = None;
// Same guard as typed MTP/M2P: point step, not entity pick.
let i = self.active_tab;
let allowed = self.tabs[i].active_cmd.as_ref().is_some_and(|c| {
@@ -2535,7 +2535,7 @@ impl OpenCADStudio {
Message::Command(cmd) => {
// Close viewport context menu if open.
let i = self.active_tab;
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
self.tabs[i].scene.selection.borrow_mut().menu.open_at = None;
// Any command also dismisses the Isolate action menu.
self.isolate_popup_open = false;
// "Pick window" (PLOTWINDOW) from Page Setup needs the backdrop
@@ -3762,7 +3762,7 @@ impl OpenCADStudio {
.scene
.selection
.borrow_mut()
.last_move_pos = Some(p);
.input.last_move_pos = Some(p);
return Task::none();
}
self.focus_model_pane(idx);
@@ -3828,45 +3828,45 @@ impl OpenCADStudio {
// Shift+RMB: the one-shot snap override menu at the cursor —
// pick a snap for just the next point, then it expires (#337).
if self.shift_down {
let pos = self.tabs[i].scene.selection.borrow().last_move_pos;
let pos = self.tabs[i].scene.selection.borrow().input.last_move_pos;
if let Some(p) = pos {
self.snap_override_popup = Some(p);
}
return Task::none();
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
let Some(p) = sel.last_move_pos else {
let Some(p) = sel.input.last_move_pos else {
return Task::none();
};
sel.context_menu = None;
sel.right_down = true;
sel.right_press_pos = Some(p);
sel.right_press_time = Some(iced::time::Instant::now());
sel.right_last_pos = Some(p);
sel.right_dragging = false;
sel.menu.open_at = None;
sel.input.right_down = true;
sel.input.right_press_pos = Some(p);
sel.input.right_press_time = Some(iced::time::Instant::now());
sel.input.right_last_pos = Some(p);
sel.input.right_dragging = false;
Task::none()
}
Message::ViewportRightRelease => {
let i = self.active_tab;
let mut sel = self.tabs[i].scene.selection.borrow_mut();
let Some(click_pos) = sel.last_move_pos else {
let Some(click_pos) = sel.input.last_move_pos else {
return Task::none();
};
if !sel.right_down {
if !sel.input.right_down {
return Task::none();
}
let was_click = !sel.right_dragging;
let was_click = !sel.input.right_dragging;
// How long the button was held, for the time-sensitive mode.
let held_ms = sel
.right_press_time
.input.right_press_time
.map_or(0, |t| t.elapsed().as_millis() as i32);
sel.right_down = false;
sel.right_press_pos = None;
sel.right_press_time = None;
sel.right_last_pos = None;
sel.right_dragging = false;
sel.orbit_pivot = None;
sel.input.right_down = false;
sel.input.right_press_pos = None;
sel.input.right_press_time = None;
sel.input.right_last_pos = None;
sel.input.right_dragging = false;
sel.orbit.pivot = None;
if !was_click {
return Task::none();
}
@@ -3881,7 +3881,7 @@ impl OpenCADStudio {
// line is empty. Pending text always runs and resets the Enter
// cycle so the next right-click acts as Enter again.
if !self.command_line.input.trim().is_empty() {
sel.right_click_entered = false;
sel.input.right_click_entered = false;
drop(sel);
return self.update(Message::CommandFinalize);
}
@@ -3904,19 +3904,19 @@ impl OpenCADStudio {
held_ms >= self.right_click_hold_ms
}
super::settings::RightClickMode::EnterFirst => {
!(has_cmd && !sel.right_click_entered)
!(has_cmd && !sel.input.right_click_entered)
}
};
if !open_menu {
sel.right_click_entered = true;
sel.input.right_click_entered = true;
drop(sel);
// CommandFinalize is Enter during a command and "repeat
// the last command" when idle — exactly the quick
// right-click.
return self.update(Message::CommandFinalize);
}
sel.right_click_entered = false;
sel.open_context_menu(click_pos);
sel.input.right_click_entered = false;
sel.menu.open(click_pos);
drop(sel);
// Take the keyboard away from the command-line field so keys
// reach the menu through the global subscription; the field
@@ -3929,11 +3929,11 @@ impl OpenCADStudio {
Message::ViewportMiddleRelease => {
let i = self.active_tab;
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.middle_down = false;
sel.middle_last_pos = None;
sel.input.middle_down = false;
sel.input.middle_last_pos = None;
// End of a Shift+MMB orbit — drop the captured pivot so the next
// gesture recomputes it against the current selection. (#229)
sel.orbit_pivot = None;
sel.orbit.pivot = None;
drop(sel);
self.arm_hover_after_navigation(i);
Task::none()
@@ -4051,7 +4051,7 @@ impl OpenCADStudio {
.scene
.selection
.borrow()
.last_move_pos
.input.last_move_pos
.unwrap_or(self.cursor_pos);
self.update_grip_hover(i, p);
Task::none()
@@ -4101,7 +4101,7 @@ impl OpenCADStudio {
let (wires, screen_height) = if stale {
(
self.tabs[i].scene.hit_test_wires(),
self.tabs[i].scene.selection.borrow().vp_size.1,
self.tabs[i].scene.selection.borrow().view.vp_size.1,
)
} else {
(queued_wires, screen_height)
@@ -4114,7 +4114,7 @@ impl OpenCADStudio {
if self.active_interaction_index.is_none() && !self.tabs.is_empty() {
let i = self.active_tab.min(self.tabs.len() - 1);
let wires = self.tabs[i].scene.hit_test_wires();
let screen_height = self.tabs[i].scene.selection.borrow().vp_size.1;
let screen_height = self.tabs[i].scene.selection.borrow().view.vp_size.1;
self.prepare_interaction_index_task(i, wires, screen_height)
.unwrap_or_else(Task::none)
} else {
@@ -5874,7 +5874,7 @@ impl OpenCADStudio {
return Task::none();
}
let i = self.active_tab;
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
self.tabs[i].scene.selection.borrow_mut().menu.open_at = None;
// A selected constraint-glyph pill takes Delete before entity
// erase — the two selections are mutually exclusive (see
// `Scene::selected_constraint`).
@@ -6321,7 +6321,7 @@ impl OpenCADStudio {
Message::DrawOrderPickRef(above) => {
let i = self.active_tab;
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
self.tabs[i].scene.selection.borrow_mut().menu.open_at = None;
let to_move: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
if to_move.is_empty() {
self.command_line
@@ -6337,7 +6337,7 @@ impl OpenCADStudio {
Message::SelectSimilar => {
let i = self.active_tab;
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
self.tabs[i].scene.selection.borrow_mut().menu.open_at = None;
let added = self.tabs[i].scene.select_similar();
self.command_line
.push_output(crate::tf!("Select Similar: {} added.", added).as_ref());
@@ -6347,7 +6347,7 @@ impl OpenCADStudio {
Message::InvertSelection => {
let i = self.active_tab;
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
self.tabs[i].scene.selection.borrow_mut().menu.open_at = None;
let count = self.tabs[i].scene.invert_selection();
self.command_line.push_output(
crate::tf!("Invert Selection: {} object(s) selected.", count).as_ref(),
+210 -213
View File
@@ -441,7 +441,7 @@ impl OpenCADStudio {
.abs();
if !self.point_size_relative && mag == 0.0 {
let wpp = self.tabs[i].scene.world_per_pixel().unwrap_or(0.0);
let viewport_height = self.tabs[i].scene.selection.borrow().vp_size.1;
let viewport_height = self.tabs[i].scene.selection.borrow().view.vp_size.1;
mag = if wpp > 0.0 {
crate::entities::point::relative_world_size(0.0, wpp, viewport_height)
} else {
@@ -800,7 +800,7 @@ impl OpenCADStudio {
self.grip_popup = None;
return;
}
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
let bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
@@ -1024,7 +1024,7 @@ impl OpenCADStudio {
{
return Task::none();
}
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
let (ox, oy) = match self.tabs[i]
.scene
.active_viewport
@@ -1121,7 +1121,7 @@ impl OpenCADStudio {
// slide the origin / rotate the axis. Short-circuits pan & snap.
if let Some(kind) = self.ucs_grip_drag {
self.drag_ucs_grip(i, kind, p);
self.tabs[i].scene.selection.borrow_mut().last_move_pos = Some(p);
self.tabs[i].scene.selection.borrow_mut().input.last_move_pos = Some(p);
return Task::none();
}
@@ -1129,7 +1129,7 @@ impl OpenCADStudio {
// hit-area overlay and moves over the rest of the viewport.
// `hover_id` returns None outside the cube box, which clears
// any stale highlight from the previous `CursorMoved`.
let (svw, svh) = self.tabs[i].scene.selection.borrow().vp_size;
let (svw, svh) = self.tabs[i].scene.selection.borrow().view.vp_size;
let cube_tile = match self.tabs[i]
.scene
.active_viewport
@@ -1148,7 +1148,7 @@ impl OpenCADStudio {
VIEWCUBE_PX,
));
let navigating = self.tabs[i].scene.selection.borrow().middle_down;
let navigating = self.tabs[i].scene.selection.borrow().input.middle_down;
if navigating {
self.clear_navigation_hover(i);
} else {
@@ -1166,61 +1166,61 @@ impl OpenCADStudio {
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.last_move_pos = Some(p);
sel.input.last_move_pos = Some(p);
if sel.left_down {
let press = sel.left_press_pos.unwrap_or(p);
if sel.input.left_down {
let press = sel.input.left_press_pos.unwrap_or(p);
let dx = p.x - press.x;
let dy = p.y - press.y;
let dist2 = dx * dx + dy * dy;
let elapsed_ms = sel
.left_press_time
.input.left_press_time
.map(|t| Instant::now().duration_since(t).as_millis())
.unwrap_or(u128::MAX);
if !sel.left_dragging && elapsed_ms >= POLY_START_DELAY_MS && dist2 > 9.0 {
sel.left_dragging = true;
if !sel.input.left_dragging && elapsed_ms >= POLY_START_DELAY_MS && dist2 > 9.0 {
sel.input.left_dragging = true;
if self.pick_drag_rect {
// PICKDRAG 1 (#226): press-drag spans a RECTANGLE
// marquee — drive the existing box machinery (its
// overlay and completion) instead of the lasso.
sel.box_anchor = Some(press);
sel.box_current = Some(p);
if !sel.box_crossing_locked {
sel.box_crossing = p.x < press.x;
sel.gesture.box_anchor = Some(press);
sel.gesture.box_current = Some(p);
if !sel.gesture.box_crossing_locked {
sel.gesture.box_crossing = p.x < press.x;
}
} else {
sel.poly_active = true;
sel.poly_crossing = p.x < press.x;
sel.poly_points.clear();
sel.poly_points.push(press);
sel.poly_points.push(p);
sel.gesture.poly_active = true;
sel.gesture.poly_crossing = p.x < press.x;
sel.gesture.poly_points.clear();
sel.gesture.poly_points.push(press);
sel.gesture.poly_points.push(p);
}
} else if sel.left_dragging && sel.poly_active {
if sel.poly_points.last().map_or(true, |lp| {
} else if sel.input.left_dragging && sel.gesture.poly_active {
if sel.gesture.poly_points.last().map_or(true, |lp| {
let ddx = p.x - lp.x;
let ddy = p.y - lp.y;
ddx * ddx + ddy * ddy > 16.0
}) {
sel.poly_points.push(p);
sel.gesture.poly_points.push(p);
}
} else if sel.left_dragging {
if let Some(a) = sel.box_anchor {
sel.box_current = Some(p);
if !sel.box_crossing_locked {
sel.box_crossing = p.x < a.x;
} else if sel.input.left_dragging {
if let Some(a) = sel.gesture.box_anchor {
sel.gesture.box_current = Some(p);
if !sel.gesture.box_crossing_locked {
sel.gesture.box_crossing = p.x < a.x;
}
}
}
} else if sel.box_anchor.is_some() {
sel.box_current = Some(p);
if let Some(a) = sel.box_anchor {
if !sel.box_crossing_locked {
sel.box_crossing = p.x < a.x;
} else if sel.gesture.box_anchor.is_some() {
sel.gesture.box_current = Some(p);
if let Some(a) = sel.gesture.box_anchor {
if !sel.gesture.box_crossing_locked {
sel.gesture.box_crossing = p.x < a.x;
}
}
}
let (mid_down, mid_last, vp_size) = (sel.middle_down, sel.middle_last_pos, sel.vp_size);
let (mid_down, mid_last, vp_size) = (sel.input.middle_down, sel.input.middle_last_pos, sel.view.vp_size);
if mid_down {
if let Some(last) = mid_last {
let (dx, dy) = (p.x - last.x, p.y - last.y);
@@ -1257,9 +1257,9 @@ impl OpenCADStudio {
// zone keeps the +/- glyph from strobing when a slow drag
// wobbles a pixel either side of where it started.
if dy.abs() >= ZOOM_DIR_FLIP_PX {
sel.zoom_dir_out = dy > 0.0;
sel.input.zoom_dir_out = dy > 0.0;
}
sel.middle_last_pos = Some(p);
sel.input.middle_last_pos = Some(p);
return Task::none();
}
// Shift+MMB drag orbits the model view instead of panning
@@ -1275,20 +1275,20 @@ impl OpenCADStudio {
self.tabs[i]
.scene
.record_nav_perf(crate::scene::NavPerfOp::Rotate, move_started);
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
self.tabs[i].scene.selection.borrow_mut().input.middle_last_pos = Some(p);
return Task::none();
} else if self.tabs[i].scene.current_layout == "Model" {
if sel.orbit_pivot.is_none() {
if sel.orbit.pivot.is_none() {
let scene = &self.tabs[i].scene;
let bounds = scene.active_model_tile_bounds(vp_size.0, vp_size.1);
sel.orbit_pivot = Some(
sel.orbit.pivot = Some(
scene
.orbit_pivot()
.or_else(|| scene.view_center_surface_pivot(bounds))
.unwrap_or_else(|| scene.camera.borrow().target),
);
}
let pivot = sel.orbit_pivot;
let pivot = sel.orbit.pivot;
drop(sel);
self.tabs[i].scene.refresh_projection_bounds();
self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy, pivot);
@@ -1296,27 +1296,27 @@ impl OpenCADStudio {
self.tabs[i]
.scene
.record_nav_perf(crate::scene::NavPerfOp::Rotate, move_started);
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
self.tabs[i].scene.selection.borrow_mut().input.middle_last_pos = Some(p);
return Task::none();
}
// Paper sheet is top-locked. Shift+MMB keeps its existing
// pan fallback; the explicit orbit tool does nothing here.
if self.tabs[i].orbit_mode {
sel.middle_last_pos = Some(p);
sel.input.middle_last_pos = Some(p);
return Task::none();
}
}
// Drop `sel` before calling mutable scene methods.
drop(sel);
self.pan_active_view(i, dx, dy, move_started);
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
self.tabs[i].scene.selection.borrow_mut().input.middle_last_pos = Some(p);
return Task::none();
}
sel.middle_last_pos = Some(p);
sel.input.middle_last_pos = Some(p);
}
let dragging = sel.left_down || sel.right_down || sel.middle_down;
let vp_size = sel.vp_size;
let dragging = sel.input.left_down || sel.input.right_down || sel.input.middle_down;
let vp_size = sel.view.vp_size;
drop(sel);
// The active pane already follows the cursor (each pane's own
@@ -2593,7 +2593,7 @@ impl OpenCADStudio {
.active_cmd
.as_ref()
.and_then(|c| c.window_first_corner());
self.tabs[i].scene.selection.borrow_mut().preview_box = if is_window_corner {
self.tabs[i].scene.selection.borrow_mut().gesture.preview_box = if is_window_corner {
window_first.map(|c1| (proj(c1), p_full, true))
} else {
None
@@ -2957,27 +2957,27 @@ impl OpenCADStudio {
self.tabs[i].scene.clear_preview_wire();
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.left_down = false;
sel.left_press_pos = None;
sel.left_press_time = None;
sel.left_dragging = false;
sel.right_down = false;
sel.right_press_pos = None;
sel.right_press_time = None;
sel.right_last_pos = None;
sel.right_dragging = false;
sel.right_click_entered = false;
sel.middle_down = false;
sel.middle_last_pos = None;
sel.orbit_pivot = None;
sel.box_anchor = None;
sel.box_anchor_world = None;
sel.box_current = None;
sel.box_crossing = false;
sel.box_crossing_locked = false;
sel.poly_active = false;
sel.poly_points.clear();
sel.poly_crossing = false;
sel.input.left_down = false;
sel.input.left_press_pos = None;
sel.input.left_press_time = None;
sel.input.left_dragging = false;
sel.input.right_down = false;
sel.input.right_press_pos = None;
sel.input.right_press_time = None;
sel.input.right_last_pos = None;
sel.input.right_dragging = false;
sel.input.right_click_entered = false;
sel.input.middle_down = false;
sel.input.middle_last_pos = None;
sel.orbit.pivot = None;
sel.gesture.box_anchor = None;
sel.gesture.box_anchor_world = None;
sel.gesture.box_current = None;
sel.gesture.box_crossing = false;
sel.gesture.box_crossing_locked = false;
sel.gesture.poly_active = false;
sel.gesture.poly_points.clear();
sel.gesture.poly_crossing = false;
drop(sel);
// Clear the rollover highlight when the cursor leaves the
// viewport so it doesn't stick while the mouse is over the
@@ -3045,7 +3045,7 @@ impl OpenCADStudio {
world: glam::DVec3,
from: Option<glam::DVec3>,
) -> Option<crate::snap::SnapResult> {
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
if vw <= 1.0 || vh <= 1.0 {
return None;
}
@@ -3088,7 +3088,7 @@ impl OpenCADStudio {
/// or rotate the chosen axis to point at it, keeping a right-handed frame
/// with Z fixed. Live — the commit (persist) happens on release.
fn drag_ucs_grip(&mut self, i: usize, kind: crate::app::UcsGripKind, p_full: Point) {
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
if vw <= 1.0 || vh <= 1.0 {
return;
}
@@ -3312,7 +3312,7 @@ impl OpenCADStudio {
/// canvas-relative point the viewport handlers expect.
pub(super) fn pane_canvas_point(&self, idx: usize, local: Point) -> Point {
let i = self.active_tab;
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
let o = self.tabs[i].scene.pane_origin_px(idx, vw, vh);
Point::new(o.x + local.x, o.y + local.y)
}
@@ -3323,7 +3323,7 @@ impl OpenCADStudio {
) -> Task<Message> {
let i = self.active_tab;
self.tabs[i].scene.model_panes.resize(ev.split, ev.ratio);
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
self.tabs[i].scene.sync_tiles_from_panes(vw, vh);
self.tabs[i].scene.camera_generation += 1;
Task::none()
@@ -3344,7 +3344,7 @@ impl OpenCADStudio {
if let iced::widget::pane_grid::DragEvent::Dropped { pane, target } = ev {
let i = self.active_tab;
self.tabs[i].scene.model_panes.drop(pane, target);
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
self.tabs[i].scene.sync_tiles_from_panes(vw, vh);
self.tabs[i].scene.camera_generation += 1;
}
@@ -3362,7 +3362,7 @@ impl OpenCADStudio {
if self.tabs[i].scene.current_layout != "Model" {
return None;
}
let vp_size = self.tabs[i].scene.selection.borrow().vp_size;
let vp_size = self.tabs[i].scene.selection.borrow().view.vp_size;
let scope = self.tabs[i].current_parametric_scope();
let id = self.tabs[i].scene.constraint_glyph_hit(
scope,
@@ -3391,7 +3391,7 @@ impl OpenCADStudio {
.scene
.selection
.borrow_mut()
.right_click_entered = false;
.input.right_click_entered = false;
// A click in the viewport dismisses any open ribbon dropdown
// (e.g. the annotation style combo), which has no backdrop of
// its own to catch outside clicks.
@@ -3419,7 +3419,7 @@ impl OpenCADStudio {
// reaches here is outside the menu.
{
let mut sel = self.tabs[i].scene.selection.borrow_mut();
if sel.context_menu.take().is_some() {
if sel.menu.open_at.take().is_some() {
return Task::none();
}
}
@@ -3441,11 +3441,11 @@ impl OpenCADStudio {
}
let (p, vp_size) = {
let sel = self.tabs[i].scene.selection.borrow();
let p = match sel.last_move_pos {
let p = match sel.input.last_move_pos {
Some(p) => p,
None => return Task::none(),
};
(p, sel.vp_size)
(p, sel.view.vp_size)
};
let (vw, vh) = vp_size;
@@ -3488,16 +3488,16 @@ impl OpenCADStudio {
self.clear_navigation_hover(i);
self.tabs[i].scene.remember_current_view();
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.middle_down = true;
sel.middle_last_pos = Some(p);
sel.input.middle_down = true;
sel.input.middle_last_pos = Some(p);
if self.tabs[i].zoom_dynamic_mode {
sel.box_anchor = Some(p);
sel.box_current = Some(p);
sel.box_crossing = false;
sel.box_crossing_locked = true;
sel.gesture.box_anchor = Some(p);
sel.gesture.box_current = Some(p);
sel.gesture.box_crossing = false;
sel.gesture.box_crossing_locked = true;
// A fresh drag starts neutral: the magnifier shows "+" until
// the pointer has actually moved far enough to mean zoom out.
sel.zoom_dir_out = false;
sel.input.zoom_dir_out = false;
}
return Task::none();
}
@@ -3743,15 +3743,15 @@ impl OpenCADStudio {
// Selection steps and drag-fence picks still wait for the release.
let picks_on_press = self.tabs[i].active_cmd.as_ref().is_some_and(|command| {
!command.is_selection_gathering() && !command.accepts_drag_selection()
}) && self.tabs[i].scene.selection.borrow().box_anchor.is_none();
}) && self.tabs[i].scene.selection.borrow().gesture.box_anchor.is_none();
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.left_down = true;
sel.input.left_down = true;
// Stored in full-canvas space (like ViewportMove's cursor and
// the overlay box / lasso drawing); release maps it into the
// active tile. Tile-local here would double-offset the anchor.
sel.left_press_pos = Some(p_full);
sel.left_press_time = Some(Instant::now());
sel.left_dragging = false;
sel.input.left_press_pos = Some(p_full);
sel.input.left_press_time = Some(Instant::now());
sel.input.left_dragging = false;
drop(sel);
if picks_on_press {
// The command path clears `left_down`, so the real release
@@ -3776,14 +3776,14 @@ impl OpenCADStudio {
// left drag (exit is Esc / another command). Mirror of the press.
if self.tabs[i].orbit_mode || self.tabs[i].pan_mode || self.tabs[i].zoom_dynamic_mode {
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.middle_down = false;
sel.middle_last_pos = None;
sel.orbit_pivot = None;
sel.box_anchor = None;
sel.box_anchor_world = None;
sel.box_current = None;
sel.box_crossing_locked = false;
sel.zoom_dir_out = false;
sel.input.middle_down = false;
sel.input.middle_last_pos = None;
sel.orbit.pivot = None;
sel.gesture.box_anchor = None;
sel.gesture.box_anchor_world = None;
sel.gesture.box_current = None;
sel.gesture.box_crossing_locked = false;
sel.input.zoom_dir_out = false;
drop(sel);
self.arm_hover_after_navigation(i);
return Task::none();
@@ -3797,19 +3797,19 @@ impl OpenCADStudio {
self.tabs[i].snap_result = None;
self.snapper.from_point = None;
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.left_down = false;
sel.left_press_pos = None;
sel.left_dragging = false;
sel.input.left_down = false;
sel.input.left_press_pos = None;
sel.input.left_dragging = false;
return Task::none();
}
let (p, is_click, is_down) = {
let sel = self.tabs[i].scene.selection.borrow();
let p = match sel.last_move_pos {
let p = match sel.input.last_move_pos {
Some(p) => p,
None => return Task::none(),
};
(p, !sel.left_dragging, sel.left_down)
(p, !sel.input.left_dragging, sel.input.left_down)
};
// Grip editing: click-move-click (plus legacy press-drag).
@@ -3850,7 +3850,7 @@ impl OpenCADStudio {
// Inside a floating viewport the pane is the viewport's own rect
// and camera (see the MoveCursor path); pick / snap then run in
// model space exactly where the GPU draws the content.
let canvas_sz = self.tabs[i].scene.selection.borrow().vp_size;
let canvas_sz = self.tabs[i].scene.selection.borrow().view.vp_size;
let edit_frame = self.tabs[i].scene.viewport_edit_frame(canvas_sz);
let (tile_vw, tile_vh, tile_off) = match &edit_frame {
Some((_, full)) => (full.width, full.height, iced::Point::new(full.x, full.y)),
@@ -3872,7 +3872,7 @@ impl OpenCADStudio {
.as_ref()
.map(|c| c.is_selection_gathering())
.unwrap_or(false);
let selection_box_active = self.tabs[i].scene.selection.borrow().box_anchor.is_some();
let selection_box_active = self.tabs[i].scene.selection.borrow().gesture.box_anchor.is_some();
let selection_pick_add = self.pick_add
|| self.tabs[i]
.active_cmd
@@ -4523,11 +4523,11 @@ impl OpenCADStudio {
{
let anchor_world = self.cursor_model_point(i, &edit_cam, p, bounds);
let mut selection = self.tabs[i].scene.selection.borrow_mut();
selection.box_anchor = Some(p_full);
selection.box_current = Some(p_full);
selection.box_anchor_world = Some(anchor_world);
if !selection.box_crossing_locked {
selection.box_crossing = false;
selection.gesture.box_anchor = Some(p_full);
selection.gesture.box_current = Some(p_full);
selection.gesture.box_anchor_world = Some(anchor_world);
if !selection.gesture.box_crossing_locked {
selection.gesture.box_crossing = false;
}
None
} else {
@@ -4632,29 +4632,29 @@ impl OpenCADStudio {
if let Some(r) = result {
let task = self.apply_cmd_result(r);
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.left_down = false;
sel.left_press_pos = None;
sel.left_press_time = None;
sel.left_dragging = false;
sel.input.left_down = false;
sel.input.left_press_pos = None;
sel.input.left_press_time = None;
sel.input.left_dragging = false;
return task;
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.left_down = false;
sel.left_press_pos = None;
sel.left_press_time = None;
sel.left_dragging = false;
sel.input.left_down = false;
sel.input.left_press_pos = None;
sel.input.left_press_time = None;
sel.input.left_dragging = false;
return Task::none();
}
let (is_down2, is_dragging, box_anchor, box_crossing, _vp_size, poly_drag) = {
let sel = self.tabs[i].scene.selection.borrow();
(
sel.left_down,
sel.left_dragging,
sel.box_anchor,
sel.box_crossing,
sel.vp_size,
sel.poly_active,
sel.input.left_down,
sel.input.left_dragging,
sel.gesture.box_anchor,
sel.gesture.box_crossing,
sel.view.vp_size,
sel.gesture.poly_active,
)
};
@@ -4683,7 +4683,7 @@ impl OpenCADStudio {
.scene
.selection
.borrow()
.poly_points
.gesture.poly_points
.iter()
.map(|point| iced::Point {
x: point.x - tile_off.x,
@@ -4749,18 +4749,18 @@ impl OpenCADStudio {
});
if let Some(result) = result {
let mut selection = self.tabs[i].scene.selection.borrow_mut();
selection.left_down = false;
selection.left_press_pos = None;
selection.left_press_time = None;
selection.left_dragging = false;
selection.poly_active = false;
selection.poly_points.clear();
selection.poly_crossing = false;
selection.box_anchor = None;
selection.box_anchor_world = None;
selection.box_current = None;
selection.box_crossing = false;
selection.box_crossing_locked = false;
selection.input.left_down = false;
selection.input.left_press_pos = None;
selection.input.left_press_time = None;
selection.input.left_dragging = false;
selection.gesture.poly_active = false;
selection.gesture.poly_points.clear();
selection.gesture.poly_crossing = false;
selection.gesture.box_anchor = None;
selection.gesture.box_anchor_world = None;
selection.gesture.box_current = None;
selection.gesture.box_crossing = false;
selection.gesture.box_crossing_locked = false;
drop(selection);
return self.apply_cmd_result(result);
}
@@ -4896,16 +4896,15 @@ properties={:.1}ms picked={}",
let sel = self.tabs[i].scene.selection.borrow();
// Map lasso points into the active tile.
let pts: Vec<iced::Point> = sel
.poly_points
.gesture.poly_points
.iter()
.map(|pp| iced::Point {
x: pp.x - tile_off.x,
y: pp.y - tile_off.y,
})
.collect();
(pts, sel.poly_crossing)
(pts, sel.gesture.poly_crossing)
};
self.tabs[i].scene.selection.borrow_mut().poly_last_crossing = crossing;
let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
let handles = self.tabs[i].scene.path_hit_handles(
&poly_pts,
@@ -4937,12 +4936,12 @@ properties={:.1}ms picked={}",
selection_just_completed = true;
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.poly_active = false;
sel.poly_points.clear();
sel.poly_crossing = false;
sel.box_anchor = None;
sel.box_anchor_world = None;
sel.box_current = None;
sel.gesture.poly_active = false;
sel.gesture.poly_points.clear();
sel.gesture.poly_crossing = false;
sel.gesture.box_anchor = None;
sel.gesture.box_anchor_world = None;
sel.gesture.box_current = None;
} else {
if box_anchor.is_none() {
// A parametric-constraint glyph draws above its geometry
@@ -5121,11 +5120,11 @@ was_selected={}",
// Full-canvas space: ViewportMove updates
// box_current in canvas coords and the overlay
// draws there; release maps back into the tile.
sel.box_anchor = Some(p_full);
sel.box_current = Some(p_full);
sel.box_anchor_world = Some(anchor_world);
if !sel.box_crossing_locked {
sel.box_crossing = false;
sel.gesture.box_anchor = Some(p_full);
sel.gesture.box_current = Some(p_full);
sel.gesture.box_anchor_world = Some(anchor_world);
if !sel.gesture.box_crossing_locked {
sel.gesture.box_crossing = false;
}
}
}
@@ -5165,15 +5164,15 @@ was_selected={}",
});
if let Some(result) = command_result {
let mut selection = self.tabs[i].scene.selection.borrow_mut();
selection.left_down = false;
selection.left_press_pos = None;
selection.left_press_time = None;
selection.left_dragging = false;
selection.box_anchor = None;
selection.box_anchor_world = None;
selection.box_current = None;
selection.box_crossing = false;
selection.box_crossing_locked = false;
selection.input.left_down = false;
selection.input.left_press_pos = None;
selection.input.left_press_time = None;
selection.input.left_dragging = false;
selection.gesture.box_anchor = None;
selection.gesture.box_anchor_world = None;
selection.gesture.box_current = None;
selection.gesture.box_crossing = false;
selection.gesture.box_crossing_locked = false;
drop(selection);
return self.apply_cmd_result(result);
}
@@ -5311,22 +5310,20 @@ properties={:.1}ms picked={}",
);
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.box_last = Some((a, p));
sel.box_last_crossing = crossing;
sel.box_anchor = None;
sel.box_anchor_world = None;
sel.box_current = None;
sel.box_crossing = false;
sel.box_crossing_locked = false;
sel.gesture.box_anchor = None;
sel.gesture.box_anchor_world = None;
sel.gesture.box_current = None;
sel.gesture.box_crossing = false;
sel.gesture.box_crossing_locked = false;
selection_just_completed = true;
}
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.left_down = false;
sel.left_press_pos = None;
sel.left_press_time = None;
sel.left_dragging = false;
sel.input.left_down = false;
sel.input.left_press_pos = None;
sel.input.left_press_time = None;
sel.input.left_dragging = false;
}
if selection_just_completed {
@@ -5364,7 +5361,7 @@ properties={:.1}ms picked={}",
self.last_vp_click_pos = Some(p);
if is_double_model {
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
let bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
@@ -5513,7 +5510,7 @@ properties={:.1}ms picked={}",
self.last_vp_click_pos = Some(p);
if is_double {
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
let bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
@@ -5660,7 +5657,7 @@ properties={:.1}ms picked={}",
}
let (cursor, canvas_size) = {
let selection = self.tabs[i].scene.selection.borrow();
(selection.last_move_pos, selection.vp_size)
(selection.input.last_move_pos, selection.view.vp_size)
};
let Some(cursor) = cursor else {
return;
@@ -5690,18 +5687,18 @@ properties={:.1}ms picked={}",
let now = Instant::now();
let is_double = {
let sel = self.tabs[i].scene.selection.borrow();
sel.middle_last_press_time
sel.input.middle_last_press_time
.map(|t| now.duration_since(t).as_millis() < 300)
.unwrap_or(false)
};
{
let mut sel = self.tabs[i].scene.selection.borrow_mut();
let Some(p) = sel.last_move_pos else {
let Some(p) = sel.input.last_move_pos else {
return Task::none();
};
sel.middle_down = true;
sel.middle_last_pos = Some(p);
sel.middle_last_press_time = Some(now);
sel.input.middle_down = true;
sel.input.middle_last_pos = Some(p);
sel.input.middle_last_press_time = Some(now);
}
if is_double {
self.tabs[i].scene.fit_all();
@@ -5752,7 +5749,7 @@ properties={:.1}ms picked={}",
fn pan_active_view(&mut self, i: usize, dx: f32, dy: f32, started: Instant) {
// Pan scale uses the active tile's size (ortho size is relative to
// viewport height), so a tiled pane pans at the correct rate.
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
let bounds = self.tabs[i].scene.active_model_tile_bounds(vw, vh);
if self.tabs[i].scene.active_viewport.is_some() {
self.tabs[i].scene.pan_active_viewport(dx, dy, bounds);
@@ -5786,8 +5783,8 @@ properties={:.1}ms picked={}",
let i = self.active_tab;
self.tabs[i].scene.remember_current_view();
self.clear_navigation_hover(i);
let cursor = self.tabs[i].scene.selection.borrow().last_move_pos;
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let cursor = self.tabs[i].scene.selection.borrow().input.last_move_pos;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
let bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
@@ -5847,7 +5844,7 @@ properties={:.1}ms picked={}",
/// drawing instead of staying frozen at its original pixel. No-op when no
/// box is in progress. (#234)
pub(in crate::app) fn reproject_box_anchor(&mut self, i: usize, vw: f32, vh: f32) {
let world = self.tabs[i].scene.selection.borrow().box_anchor_world;
let world = self.tabs[i].scene.selection.borrow().gesture.box_anchor_world;
let Some(world) = world else { return };
let tile_b = self.tabs[i].scene.active_model_tile_bounds(vw, vh);
let tile_local = iced::Rectangle {
@@ -5862,7 +5859,7 @@ properties={:.1}ms picked={}",
.borrow()
.project(world, tile_local)
{
self.tabs[i].scene.selection.borrow_mut().box_anchor =
self.tabs[i].scene.selection.borrow_mut().gesture.box_anchor =
Some(iced::Point::new(sp.x + tile_b.x, sp.y + tile_b.y));
}
}
@@ -5878,7 +5875,7 @@ properties={:.1}ms picked={}",
return Task::none();
}
let rot = self.tabs[i].scene.active_view_rotation_mat();
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
// The ViewCube draws in the top-right of whichever area
// owns it: the full canvas in model space, or the active
// viewport's screen rectangle in a paper layout. Map the
@@ -6018,12 +6015,12 @@ properties={:.1}ms picked={}",
let deferred_command = self.tabs[i].active_cmd.as_ref().is_some_and(|command| {
command.needs_entity_pick() && command.entity_pick_deferred_hover()
});
let navigating = self.tabs[i].scene.selection.borrow().middle_down;
let navigating = self.tabs[i].scene.selection.borrow().input.middle_down;
if (self.tabs[i].active_cmd.is_some() && !deferred_command) || navigating {
self.clear_navigation_hover(i);
return Task::none();
}
let cursor = self.tabs[i].scene.selection.borrow().last_move_pos;
let cursor = self.tabs[i].scene.selection.borrow().input.last_move_pos;
self.constraint_glyph_tooltip = cursor.and_then(|point| {
self.constraint_glyph_under(i, point).map(|(kind, _)| kind)
});
@@ -6037,7 +6034,7 @@ properties={:.1}ms picked={}",
// Inside a viewport, hover-pick through the viewport camera +
// model wires so the rollover highlights the entity under the
// cursor (dwell.point / dwell.tile_size are already pane-local).
let canvas_sz = self.tabs[i].scene.selection.borrow().vp_size;
let canvas_sz = self.tabs[i].scene.selection.borrow().view.vp_size;
let edit_cam = self.tabs[i]
.scene
.viewport_edit_frame(canvas_sz)
@@ -6818,7 +6815,7 @@ mod zoom_dynamic_cursor_tests {
fn zoom_armed_app() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[0].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let _ = app.dispatch_command("ZOOM DYNAMIC");
app
}
@@ -6828,7 +6825,7 @@ mod zoom_dynamic_cursor_tests {
}
fn zoom_dir_out(app: &OpenCADStudio) -> bool {
app.tabs[0].scene.selection.borrow().zoom_dir_out
app.tabs[0].scene.selection.borrow().input.zoom_dir_out
}
/// The magnifier has to carry the sign of the zoom actually under way:
@@ -6946,7 +6943,7 @@ mod selection_preview_tests {
app.ortho_mode = false;
app.polar_mode = false;
app.constraint_solve_mode = true;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let scene = &mut app.tabs[i].scene;
let [outer, inner, upper, lower] = [
([0.0, 0.0], [0.0, 5.0]),
@@ -6987,7 +6984,7 @@ mod selection_preview_tests {
let handles = if let Ok(path) = std::env::var("OCS_GRIP_TEST_DRAWING") {
let result = app.automation_op(&serde_json::json!({"op":"open","path":path}).to_string());
assert_eq!(result["ok"], true, "{result}");
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
[0x57F, 0x57D, 0x57C, 0x57A, 0x57E, 0x57B].map(Handle::new)
} else {
[outer, inner, upper, lower, top, right]
@@ -7067,7 +7064,7 @@ mod selection_preview_tests {
app.snapper.otrack_enabled = false;
app.ortho_mode = false;
app.polar_mode = false;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let a = app.tabs[i]
.scene
.add_entity(EntityType::Line(Line::from_points(
@@ -7148,7 +7145,7 @@ mod selection_preview_tests {
app.ortho_mode = false;
app.polar_mode = false;
app.constraint_solve_mode = true;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let mut rectangle = LwPolyline::from_points(vec![
Vector2::new(0.0, 0.0),
Vector2::new(4.0, 0.0),
@@ -7165,7 +7162,7 @@ mod selection_preview_tests {
let handle = if let Ok(path) = std::env::var("OCS_GRIP_TEST_DRAWING") {
let result = app.automation_op(&serde_json::json!({"op":"open","path":path}).to_string());
assert_eq!(result["ok"], true, "{result}");
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
Handle::new(0x556)
} else {
handle
@@ -7236,7 +7233,7 @@ mod selection_preview_tests {
app.ortho_mode = false;
app.polar_mode = false;
app.constraint_solve_mode = true;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let end = if vertical { Vector3::new(0.0, 4.0, 0.0) } else { Vector3::new(4.0, 0.0, 0.0) };
let handle = app.tabs[i].scene.add_entity(EntityType::Line(Line::from_points(Vector3::ZERO, end)));
app.tabs[i].scene.parametric_constraint_set_mut(ParametricScope::ModelSpace).add(
@@ -7263,7 +7260,7 @@ mod selection_preview_tests {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let bounds = iced::Rectangle::with_size(iced::Size::new(800.0, 600.0));
let cursor = |app: &OpenCADStudio, x: f64, y: f64| {
let at = app.tabs[i].scene.camera.borrow().project(glam::DVec3::new(x, y, 0.0), bounds);
@@ -7301,7 +7298,7 @@ mod selection_preview_tests {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
// Base away from the model origin so the UCS icon cannot swallow
// the press (it sits at the origin in a fresh drawing).
let handle = app.tabs[i].scene.add_entity(EntityType::XLine(
@@ -7317,7 +7314,7 @@ mod selection_preview_tests {
let bounds = iced::Rectangle::with_size(iced::Size::new(800.0, 600.0));
let g1world = app.tabs[i].selected_grips[1].world;
let g1cursor = app.tabs[i].scene.camera.borrow().project(g1world, bounds).unwrap();
app.tabs[i].scene.selection.borrow_mut().last_move_pos =
app.tabs[i].scene.selection.borrow_mut().input.last_move_pos =
Some(iced::Point::new(g1cursor.x, g1cursor.y));
let _ = app.on_viewport_left_press();
assert_eq!(
@@ -7345,7 +7342,7 @@ mod selection_preview_tests {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let handle = app.tabs[i].scene.add_entity(EntityType::Line(Line::from_points(
Vector3::new(-1.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
@@ -7403,9 +7400,9 @@ mod selection_preview_tests {
record.add_value(XDataValue::String("https://example.com/linked".into()));
line.common.extended_data.add_record(record);
let handle = app.tabs[i].scene.add_entity(EntityType::Line(line));
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let _ = app.run_command_line("ZOOM EXTENTS");
app.tabs[i].scene.selection.borrow_mut().last_move_pos = Some(Point::new(400.0, 300.0));
app.tabs[i].scene.selection.borrow_mut().input.last_move_pos = Some(Point::new(400.0, 300.0));
app.ctrl_down = mode != "plain";
match mode {
"pan" => app.tabs[i].pan_mode = true,
@@ -7429,9 +7426,9 @@ mod selection_preview_tests {
if mode == "link" {
assert!(task.units() > 0);
let selection = app.tabs[i].scene.selection.borrow();
assert!(!selection.left_down && selection.box_anchor.is_none());
assert!(!selection.input.left_down && selection.gesture.box_anchor.is_none());
} else if matches!(mode, "pan" | "orbit" | "zoom") {
assert!(app.tabs[i].scene.selection.borrow().middle_down, "{mode}");
assert!(app.tabs[i].scene.selection.borrow().input.middle_down, "{mode}");
}
}
}
@@ -7444,7 +7441,7 @@ mod selection_preview_tests {
let i = app.active_tab;
app.model_space.selection_preview = preview;
let _ = app.run_command_line("LINE 0,0 10,10");
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let _ = app.run_command_line("ZOOM EXTENTS");
// The pick only runs once the cursor has been still for the dwell
@@ -7495,7 +7492,7 @@ mod selection_preview_tests {
app.snapper.otrack_enabled = false;
app.ortho_mode = false;
app.polar_mode = false;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
app.tabs[i].scene.add_entity(EntityType::Line(Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 25.0),
@@ -7550,7 +7547,7 @@ mod selection_preview_tests {
app.snapper.otrack_enabled = false;
app.ortho_mode = false;
app.polar_mode = false;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
// In-app BOX equivalent: kernel box committed as a SAT solid.
let base =
@@ -7644,7 +7641,7 @@ mod selection_preview_tests {
app.snapper.otrack_enabled = false;
app.ortho_mode = false;
app.polar_mode = false;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let base =
crate::scene::model::solid_model::box_solid([0.0, 0.0, 2.5], 10.0, 10.0, 5.0)
@@ -7712,7 +7709,7 @@ mod selection_preview_tests {
app.snapper.otrack_enabled = false;
app.ortho_mode = false;
app.polar_mode = false;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let base =
crate::scene::model::solid_model::box_solid([0.0, 0.0, 2.5], 10.0, 10.0, 5.0)
@@ -7779,7 +7776,7 @@ mod selection_preview_tests {
app.snapper.otrack_enabled = false;
app.ortho_mode = false;
app.polar_mode = false;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let base =
crate::scene::model::solid_model::box_solid([0.0, 0.0, 2.5], 10.0, 10.0, 5.0)
@@ -7840,7 +7837,7 @@ mod selection_preview_tests {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let base =
crate::scene::model::solid_model::box_solid([0.0, 0.0, 2.5], 10.0, 10.0, 5.0)
@@ -7925,7 +7922,7 @@ mod selection_preview_tests {
app.snapper.otrack_enabled = false;
app.ortho_mode = false;
app.polar_mode = false;
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
app.tabs[i].scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let base =
crate::scene::model::solid_model::box_solid([0.0, 0.0, 2.5], 10.0, 10.0, 5.0)
+20 -20
View File
@@ -263,13 +263,13 @@ impl OpenCADStudio {
tab.scene
.active_viewport
.and_then(|h| {
let (cw, ch) = tab.scene.selection.borrow().vp_size;
let (cw, ch) = tab.scene.selection.borrow().view.vp_size;
tab.scene.viewport_screen_rect(h, (cw, ch))
})
.map(|r| r.width)
.unwrap_or(f32::INFINITY)
} else {
let (vw, vh) = tab.scene.selection.borrow().vp_size;
let (vw, vh) = tab.scene.selection.borrow().view.vp_size;
tab.scene.active_model_tile_bounds(vw, vh).width
};
let viewcube_visible = self.show_viewcube
@@ -351,7 +351,7 @@ impl OpenCADStudio {
let size_probe: Element<'_, Message> = responsive(move |size| {
{
let mut sel = scene.selection.borrow_mut();
sel.vp_size = (size.width, size.height);
sel.view.vp_size = (size.width, size.height);
}
scene.sync_tiles_from_panes(size.width, size.height);
Space::new().width(Fill).height(Fill)
@@ -406,7 +406,7 @@ impl OpenCADStudio {
let grid_overlay = if self.layout_settling || tab.is_start {
Space::new().into()
} else {
let (vw, vh) = tab.scene.selection.borrow().vp_size;
let (vw, vh) = tab.scene.selection.borrow().view.vp_size;
let model_basis = {
let (o, ux, uy, uz) = tab.ucs_xform().axes();
let (ux, uy, uz) = super::helpers::drafting_axes(
@@ -527,7 +527,7 @@ bg={bg_ms:.1}ms n={view_count}"
let grips: Vec<crate::ui::overlay::GripMarker> = if show_grips
&& !tab.selected_grips.is_empty()
{
let (vw, vh) = sel_ref.vp_size;
let (vw, vh) = sel_ref.view.vp_size;
// Overlays project through the active tile's camera, so
// they must use the active tile's screen rectangle (with
// its canvas offset) — not the whole canvas — or they
@@ -645,7 +645,7 @@ bg={bg_ms:.1}ms n={view_count}"
let grip_clip = if grips.is_empty() {
None
} else {
let (vw, vh) = sel_ref.vp_size;
let (vw, vh) = sel_ref.view.vp_size;
Some(
tab.scene
.viewport_edit_frame((vw, vh))
@@ -654,7 +654,7 @@ bg={bg_ms:.1}ms n={view_count}"
)
};
let (vw, vh) = sel_ref.vp_size;
let (vw, vh) = sel_ref.view.vp_size;
// Active tile rectangle (canvas-offset included) so grid / UCS
// icon / crosshair project through the active pane's camera at
// the correct place and scale.
@@ -814,7 +814,7 @@ bg={bg_ms:.1}ms n={view_count}"
// Model-space pane dividers (none in paper / single-pane layouts).
let dividers = if !is_paper {
let (vw, vh) = sel_ref.vp_size;
let (vw, vh) = sel_ref.view.vp_size;
tab.scene.model_pane_dividers(vw, vh)
} else {
vec![]
@@ -824,8 +824,8 @@ bg={bg_ms:.1}ms n={view_count}"
// target pane under the cursor.
let (pane_move_rect, pane_drop_rect) = match self.pane_move_from {
Some(from) if !is_paper => {
let (vw, vh) = sel_ref.vp_size;
let cursor = sel_ref.last_move_pos;
let (vw, vh) = sel_ref.view.vp_size;
let cursor = sel_ref.input.last_move_pos;
let tiles = tab.scene.model_tiles.borrow();
let px = |t: &crate::scene::ModelTile| iced::Rectangle {
x: t.rect.x * vw,
@@ -882,7 +882,7 @@ bg={bg_ms:.1}ms n={view_count}"
let scope = tab.current_parametric_scope();
tab.scene.cached_glyph_placements(
scope,
sel_ref.vp_size,
sel_ref.view.vp_size,
self.show_constraint_values,
self.constraint_bar_display,
self.constraint_bar_mode,
@@ -1009,7 +1009,7 @@ bg={bg_ms:.1}ms n={view_count}"
.as_ref()
.and_then(|command| command.dyn_label_point(w))
.and_then(|world| {
let (vw, vh) = tab.scene.selection.borrow().vp_size;
let (vw, vh) = tab.scene.selection.borrow().view.vp_size;
let (camera, bounds) =
tab.scene.viewport_edit_frame((vw, vh)).unwrap_or_else(|| {
(
@@ -1085,7 +1085,7 @@ bg={bg_ms:.1}ms n={view_count}"
opposite + width_axis * width + height_axis * (height * 0.5);
let rectangle_center =
opposite + width_axis * (width * 0.5) + height_axis * (height * 0.5);
let (vw, vh) = tab.scene.selection.borrow().vp_size;
let (vw, vh) = tab.scene.selection.borrow().view.vp_size;
let (camera, bounds) =
tab.scene.viewport_edit_frame((vw, vh)).unwrap_or_else(|| {
(
@@ -1282,7 +1282,7 @@ bg={bg_ms:.1}ms n={view_count}"
// viewport drawing / selection is unaffected. In a paper layout
// the active viewport gets its own picker (below) instead.
if !is_paper && !tab.is_start {
let (vw, vh) = tab.scene.selection.borrow().vp_size;
let (vw, vh) = tab.scene.selection.borrow().view.vp_size;
let rect = tab.scene.active_model_tile_bounds(vw, vh);
// Unified control chip: split buttons + render-mode picker +
// grid / grid-snap toggles, for the active Model tile.
@@ -1328,7 +1328,7 @@ bg={bg_ms:.1}ms n={view_count}"
let active_vp_rect: Option<(codec::Handle, iced::Rectangle)> =
if is_paper && !tab.is_start {
tab.scene.active_viewport.and_then(|h| {
let (cw, ch) = tab.scene.selection.borrow().vp_size;
let (cw, ch) = tab.scene.selection.borrow().view.vp_size;
tab.scene
.viewport_screen_rect(h, (cw, ch))
.map(|rect| (h, rect))
@@ -1342,7 +1342,7 @@ bg={bg_ms:.1}ms n={view_count}"
// outline inward when the viewport ran off the top/left edge, so
// its drawn border no longer matched the real viewport — clicks
// that looked outside landed in (and activated) another viewport.
let (cw, ch) = tab.scene.selection.borrow().vp_size;
let (cw, ch) = tab.scene.selection.borrow().view.vp_size;
let x = rect.x.max(0.0);
let y = rect.y.max(0.0);
let vw = ((rect.x + rect.width).min(cw) - x).max(1.0);
@@ -1435,7 +1435,7 @@ bg={bg_ms:.1}ms n={view_count}"
// Place the ViewCube hit area in the active model tile's top-right
// corner so it tracks the active panel in a tiled layout. The hit
// test in update.rs already maps clicks through the active tile.
let (vw, vh) = tab.scene.selection.borrow().vp_size;
let (vw, vh) = tab.scene.selection.borrow().view.vp_size;
let rect = tab.scene.active_model_tile_bounds(vw, vh);
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
@@ -1832,7 +1832,7 @@ bg={bg_ms:.1}ms n={view_count}"
if !tab.is_start {
let (ctx_pos, highlighted) = {
let sel = tab.scene.selection.borrow();
(sel.context_menu, sel.context_menu_ui.highlighted)
(sel.menu.open_at, sel.menu.ui.highlighted)
};
if let Some(p) = ctx_pos {
let menu = self.current_context_menu();
@@ -1848,7 +1848,7 @@ bg={bg_ms:.1}ms n={view_count}"
// In-place MText editor (toolbar + text area), anchored at the
// insertion-point click.
if !tab.is_start {
let canvas = tab.scene.selection.borrow().vp_size;
let canvas = tab.scene.selection.borrow().view.vp_size;
if let Some(ed) = &self.mtext_editor {
let styles: Vec<String> = tab
.scene
@@ -2010,7 +2010,7 @@ bg={bg_ms:.1}ms n={view_count}"
} else {
visible + 1
};
let edge_h = tab.scene.selection.borrow().vp_size.1;
let edge_h = tab.scene.selection.borrow().view.vp_size.1;
let slot_h = edge_h / final_count as f32;
let index = self.dock_drag_target.map(|(_, i)| i).unwrap_or(0);
let slot = index.min(final_count.saturating_sub(1));
+3 -3
View File
@@ -430,7 +430,7 @@ impl Scene {
/// wrong whenever a tiled viewport is active.
pub(super) fn active_camera_aspect(&self) -> f32 {
if self.current_layout == "Model" {
let (canvas_w, canvas_h) = self.selection.borrow().vp_size;
let (canvas_w, canvas_h) = self.selection.borrow().view.vp_size;
let tiles = self.model_tiles.borrow();
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
if let Some(tile) = tiles.get(active) {
@@ -803,7 +803,7 @@ impl Scene {
z: view_dir.z as f64,
};
entry.view_height = view_height as f64;
let (canvas_width, canvas_height) = self.selection.borrow().vp_size;
let (canvas_width, canvas_height) = self.selection.borrow().view.vp_size;
let viewport_width = (upper_right.x - lower_left.x).abs() * canvas_width as f64;
let viewport_height = (upper_right.y - lower_left.y).abs() * canvas_height as f64;
if viewport_width > 1e-9 && viewport_height > 1e-9 {
@@ -1556,7 +1556,7 @@ impl Scene {
return;
};
let (canvas_w, canvas_h) = self.selection.borrow().vp_size;
let (canvas_w, canvas_h) = self.selection.borrow().view.vp_size;
let fallback = self.last_render_aspect.get().max(0.01);
let aspects: Vec<f32> = self
.model_tiles
+4 -4
View File
@@ -3068,7 +3068,7 @@ impl super::Scene {
/// `(id, anchor, outward_screen_direction, label, is_conflicting,
/// hover_points)` for every enabled, visible constraint whose glyph
/// projects on-screen.
/// `vp_size` is the full canvas size (as `SelectionState::vp_size`
/// `vp_size` is the full canvas size (as `SelectionView::vp_size`
/// reports it), matching what `viewport_edit_frame`/
/// `active_model_tile_bounds` expect. Mirrors the projection
/// `crate::app::view` builds its own render list with. The memoised
@@ -3808,7 +3808,7 @@ mod tests {
Vector3::new(1.0, 0.0, 0.0),
),
));
scene.selection.borrow_mut().vp_size = (800.0, 600.0);
scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let id = scene.parametric_constraint_set_mut(scope).add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
@@ -3848,7 +3848,7 @@ mod tests {
Vector3::new(1.0, 0.0, 0.0),
),
));
scene.selection.borrow_mut().vp_size = (800.0, 600.0);
scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let id = scene.parametric_constraint_set_mut(scope).add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
@@ -3878,7 +3878,7 @@ mod tests {
Vector3::new(1.0, 0.0, 0.0),
),
));
scene.selection.borrow_mut().vp_size = (800.0, 600.0);
scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let id = scene.parametric_constraint_set_mut(scope).add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
+87 -40
View File
@@ -2,9 +2,29 @@ use iced::time::Instant;
use iced::Point;
/// Mouse / selection interaction state for the viewport.
///
/// Split (#32) into five sub-structs by family so a per-frame clone can copy
/// only the family it needs and gesture clears touch one family at a time.
/// The `RefCell` around this struct (`Scene::selection`) is untouched.
#[derive(Clone, Default)]
pub struct SelectionState {
pub view: SelectionView,
pub gesture: SelectionGesture,
pub input: SelectionInput,
pub menu: SelectionMenu,
pub orbit: SelectionOrbit,
}
/// View-dependent snapshot: the canvas size selection geometry is expressed in.
#[derive(Clone, Default)]
pub struct SelectionView {
pub vp_size: (f32, f32),
}
/// One box-or-lasso gesture: anchor/current corners, crossing sense, and the
/// in-progress polygon. Cleared together by [`SelectionGesture::clear_left`].
#[derive(Clone, Default)]
pub struct SelectionGesture {
pub box_anchor: Option<Point>,
/// World point under the box-selection anchor, so the anchor can be
/// re-projected to screen when the camera zooms/pans mid-drag instead of
@@ -12,14 +32,12 @@ pub struct SelectionState {
/// (#234)
pub box_anchor_world: Option<glam::DVec3>,
pub box_current: Option<Point>,
pub box_last: Option<(Point, Point)>,
pub box_crossing: bool,
/// Set when a Window / Crossing selection keyword fixed the sense of the
/// box being drawn. Dragging normally decides it from the direction the
/// corner travels, which would immediately overwrite what the user just
/// asked for, so that derivation stands down while this holds.
pub box_crossing_locked: 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
@@ -29,14 +47,25 @@ pub struct SelectionState {
pub poly_active: bool,
pub poly_points: Vec<Point>,
pub poly_crossing: bool,
pub poly_last_crossing: bool,
/// Canvas position the right-click context menu is anchored at while it
/// is open. `None` = closed. Its rows are rebuilt from app state every
/// frame (`ui::popup::context_menu::build_context_menu`).
pub context_menu: Option<Point>,
/// Transient UI state of the open context menu (expanded submenu,
/// keyboard highlight). Reset every time the menu opens.
pub context_menu_ui: ContextMenuUi,
}
impl SelectionGesture {
/// End the in-progress box/lasso gesture (left-button release path).
pub fn clear_left(&mut self) {
self.box_anchor = None;
self.box_anchor_world = None;
self.box_current = None;
self.box_crossing = false;
self.box_crossing_locked = false;
self.poly_active = false;
self.poly_points.clear();
self.poly_crossing = false;
}
}
/// Raw button/pointer state: press positions, drag flags, orbit pivot input.
#[derive(Clone, Default)]
pub struct SelectionInput {
pub last_move_pos: Option<Point>,
pub left_down: bool,
pub left_press_pos: Option<Point>,
@@ -47,10 +76,6 @@ pub struct SelectionState {
pub right_press_time: Option<Instant>,
pub right_dragging: bool,
pub right_last_pos: Option<Point>,
/// World point the current orbit drag revolves around (selection or model
/// centre), captured when the drag starts so it stays fixed for the whole
/// gesture. `None` when no orbit is in progress. (#229)
pub orbit_pivot: Option<glam::DVec3>,
/// While a command is active, a right-click acts as Enter; the *next*
/// consecutive right-click opens the context menu instead. This tracks
/// whether the previous right-click already fired Enter. Reset by any
@@ -66,6 +91,52 @@ pub struct SelectionState {
pub zoom_dir_out: bool,
}
impl SelectionInput {
/// Release the left button (click-commit path).
pub fn clear_left_buttons(&mut self) {
self.left_down = false;
self.left_press_pos = None;
self.left_press_time = None;
self.left_dragging = false;
}
}
/// Right-click context-menu state.
#[derive(Clone, Default)]
pub struct SelectionMenu {
/// Canvas position the right-click context menu is anchored at while it
/// is open. `None` = closed. Its rows are rebuilt from app state every
/// frame (`ui::popup::context_menu::build_context_menu`).
pub open_at: Option<Point>,
/// Transient UI state of the open context menu (expanded submenu,
/// keyboard highlight). Reset every time the menu opens.
pub ui: ContextMenuUi,
}
impl SelectionMenu {
/// Open the context menu at `at`, discarding the previous menu's
/// expanded-submenu / highlight state.
pub fn open(&mut self, at: Point) {
self.open_at = Some(at);
self.ui = ContextMenuUi::default();
}
/// Close the context menu (no-op when it is not open).
pub fn close(&mut self) {
self.open_at = None;
self.ui = ContextMenuUi::default();
}
}
/// Orbit-drag state.
#[derive(Clone, Default)]
pub struct SelectionOrbit {
/// World point the current orbit drag revolves around (selection or model
/// centre), captured when the drag starts so it stays fixed for the whole
/// gesture. `None` when no orbit is in progress. (#229)
pub pivot: Option<glam::DVec3>,
}
/// Transient state of the open right-click context menu.
#[derive(Clone, Default)]
pub struct ContextMenuUi {
@@ -78,36 +149,12 @@ pub struct ContextMenuUi {
}
impl SelectionState {
/// Open the context menu at `at`, discarding the previous menu's
/// expanded-submenu / highlight state.
pub fn open_context_menu(&mut self, at: Point) {
self.context_menu = Some(at);
self.context_menu_ui = ContextMenuUi::default();
}
/// Close the context menu (no-op when it is not open).
pub fn close_context_menu(&mut self) {
self.context_menu = None;
self.context_menu_ui = ContextMenuUi::default();
}
/// End every left-button selection gesture without disturbing the previous
/// completed-window record or command-owned preview marquee. Grip editing
/// owns the left button while engaged and calls this before/after placement
/// so a small pointer move cannot also arm a box or lasso selection.
pub fn clear_left_selection_gesture(&mut self) {
self.left_down = false;
self.left_press_pos = None;
self.left_press_time = None;
self.left_dragging = false;
self.box_anchor = None;
self.box_anchor_world = None;
self.box_current = None;
self.box_crossing = false;
self.box_crossing_locked = false;
self.poly_active = false;
self.poly_points.clear();
self.poly_crossing = false;
self.input.clear_left_buttons();
self.gesture.clear_left();
}
}
+1 -1
View File
@@ -281,7 +281,7 @@ impl Scene {
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;
self.selection.borrow_mut().gesture.preview_box = None;
}
pub fn wire_models_for(&self, handles: &[codec::Handle]) -> Vec<WireModel> {
+1 -1
View File
@@ -4213,7 +4213,7 @@ impl Scene {
// `CursorMoved` handler — the cube overlay sits above the shader
// and would otherwise mask the move event from `Program::update`.
let hover_region = show_interaction.then(|| self.viewcube_hover.get()).flatten();
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
self.selection.borrow_mut().view.vp_size = (bounds.width, bounds.height);
if bounds.height > 0.0 {
self.set_render_aspect(bounds.width / bounds.height);
self.set_render_pixel_scale(bounds.width, bounds.height);
+17 -17
View File
@@ -1222,7 +1222,7 @@ impl canvas::Program<Message> for SelectionCanvas {
// under way. Hovering before the drag starts shows the neutral "+"
// magnifier rather than guessing a direction.
if self.nav != NavCursor::None && cursor.is_over(bounds) {
let dragging = self.selection.borrow().middle_down;
let dragging = self.selection.borrow().input.middle_down;
return match self.nav {
NavCursor::Pan => {
if dragging {
@@ -1233,7 +1233,7 @@ impl canvas::Program<Message> for SelectionCanvas {
}
NavCursor::Orbit => mouse::Interaction::AllScroll,
NavCursor::Zoom => {
if dragging && self.selection.borrow().zoom_dir_out {
if dragging && self.selection.borrow().input.zoom_dir_out {
mouse::Interaction::ZoomOut
} else {
mouse::Interaction::ZoomIn
@@ -1338,7 +1338,7 @@ impl canvas::Program<Message> for SelectionCanvas {
}
// Ghost card dragged under the cursor — a 0.32× preview of the
// source pane, centred on the cursor.
if let Some(c) = self.selection.borrow().last_move_pos {
if let Some(c) = self.selection.borrow().input.last_move_pos {
let gw = (src.width * 0.32).clamp(60.0, 280.0);
let gh = (src.height * 0.32).clamp(40.0, 200.0);
let g = canvas::Path::rectangle(
@@ -1408,27 +1408,27 @@ impl canvas::Program<Message> for SelectionCanvas {
// the view zooms. Suppress the drawing, not the state — the anchor is
// still re-projected as the camera moves.
if self.nav != NavCursor::Zoom {
if let (Some(a), Some(b)) = (self.selection.borrow().box_anchor, self.selection.borrow().box_current) {
draw_marquee(&mut frame, a, b, self.selection.borrow().box_crossing, theme, &self.selection_visual, self.crosshair_bg);
if let (Some(a), Some(b)) = (self.selection.borrow().gesture.box_anchor, self.selection.borrow().gesture.box_current) {
draw_marquee(&mut frame, a, b, self.selection.borrow().gesture.box_crossing, theme, &self.selection_visual, self.crosshair_bg);
}
}
// Preview marquee for point-picked windows (STRETCH) — same look, no pick.
if let Some((a, b, crossing)) = self.selection.borrow().preview_box {
if let Some((a, b, crossing)) = self.selection.borrow().gesture.preview_box {
draw_marquee(&mut frame, a, b, crossing, theme, &self.selection_visual, self.crosshair_bg);
}
if self.selection.borrow().poly_active && self.selection.borrow().poly_points.len() > 1 {
let crossing = self.selection.borrow().poly_crossing;
if self.selection.borrow().gesture.poly_active && self.selection.borrow().gesture.poly_points.len() > 1 {
let crossing = self.selection.borrow().gesture.poly_crossing;
let base = resolve_selection_base_color(crossing, theme, &self.selection_visual, self.crosshair_bg);
let canvas_light = crate::ui::style::common::canvas_is_light(self.crosshair_bg);
if self.selection_visual.area && self.selection_visual.opacity > 0 {
let alpha = selection_fill_alpha(self.selection_visual.opacity as f32, canvas_light);
let fill = base.scale_alpha(alpha);
if let Some(cur) = self.selection.borrow().last_move_pos {
let start = self.selection.borrow().poly_points[0];
if let Some(cur) = self.selection.borrow().input.last_move_pos {
let start = self.selection.borrow().gesture.poly_points[0];
let fill_path = canvas::Path::new(|p| {
p.move_to(start);
for pt in &self.selection.borrow().poly_points[1..] {
for pt in &self.selection.borrow().gesture.poly_points[1..] {
p.line_to(*pt);
}
p.line_to(cur);
@@ -1440,8 +1440,8 @@ impl canvas::Program<Message> for SelectionCanvas {
let stroke_alpha = if canvas_light { 0.95 } else { 0.90 };
let stroke = base.scale_alpha(stroke_alpha);
let path = canvas::Path::new(|p| {
p.move_to(self.selection.borrow().poly_points[0]);
for pt in &self.selection.borrow().poly_points[1..] {
p.move_to(self.selection.borrow().gesture.poly_points[0]);
for pt in &self.selection.borrow().gesture.poly_points[1..] {
p.line_to(*pt);
}
});
@@ -1459,9 +1459,9 @@ impl canvas::Program<Message> for SelectionCanvas {
..Default::default()
};
frame.stroke(&path, stroke_style.clone());
if let Some(cur) = self.selection.borrow().last_move_pos {
let start = self.selection.borrow().poly_points[0];
let last = *self.selection.borrow().poly_points.last().unwrap();
if let Some(cur) = self.selection.borrow().input.last_move_pos {
let start = self.selection.borrow().gesture.poly_points[0];
let last = *self.selection.borrow().gesture.poly_points.last().unwrap();
let preview = canvas::Path::new(|p| {
p.move_to(last);
p.line_to(cur);
@@ -1898,7 +1898,7 @@ impl canvas::Program<Message> for SelectionCanvas {
&& !self.suppressed
&& self.crosshair.cursor_type == CursorType::Crosshair
{
if let Some(cp) = self.selection.borrow().last_move_pos {
if let Some(cp) = self.selection.borrow().input.last_move_pos {
let [r, g, b, a] = self.crosshair.color.map_or_else(
|| {
crate::scene::view::render::adapt_to_bg(
+1 -1
View File
@@ -34,7 +34,7 @@ pub const MENU_WIDTH_WITH_HINTS: f32 = 236.0;
pub const RECENT_LIMIT: usize = 10;
/// Identifies an inline-expanding (accordion) submenu. Only one is open at a
/// time; the open one is tracked in `SelectionState::context_menu_ui`.
/// time; the open one is tracked in `SelectionMenu::ui`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SubmenuId {
RecentInput,