From 7aafd9c710a11762426119c8303206f2de4eba9d Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Tue, 6 Oct 2026 20:04:17 +0100 Subject: [PATCH] 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. --- benches/performance_benchmarks.rs | 25 +- src/app/automation.rs | 16 +- src/app/command_driver/mod.rs | 2 +- src/app/command_driver/utilities.rs | 6 +- src/app/commands/mod.rs | 101 +++---- src/app/control/actions.rs | 2 +- src/app/control/mod.rs | 8 +- src/app/control/vision.rs | 2 +- src/app/dim_viewport.rs | 5 +- src/app/mtext_editor.rs | 2 +- src/app/navigation.rs | 14 +- src/app/properties.rs | 2 +- src/app/text_inline.rs | 2 +- src/app/update/command.rs | 28 +- src/app/update/context_menu.rs | 40 +-- src/app/update/dialog.rs | 6 +- src/app/update/file.rs | 4 +- src/app/update/mod.rs | 72 ++--- src/app/update/viewport.rs | 423 ++++++++++++++-------------- src/app/view/mod.rs | 40 +-- src/scene/camera_ops.rs | 6 +- src/scene/parametric_constraints.rs | 8 +- src/scene/pick/selection_state.rs | 127 ++++++--- src/scene/preview.rs | 2 +- src/scene/view/render.rs | 2 +- src/ui/overlay.rs | 34 +-- src/ui/popup/context_menu.rs | 2 +- 27 files changed, 512 insertions(+), 469 deletions(-) diff --git a/benches/performance_benchmarks.rs b/benches/performance_benchmarks.rs index 2cc078a0d..dede3f7c8 100644 --- a/benches/performance_benchmarks.rs +++ b/benches/performance_benchmarks.rs @@ -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, diff --git a/src/app/automation.rs b/src/app/automation.rs index d9830b568..876fba186 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -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); } diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index 33edae656..b82bef978 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -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, diff --git a/src/app/command_driver/utilities.rs b/src/app/command_driver/utilities.rs index 2b56ce63d..943c3630d 100644 --- a/src/app/command_driver/utilities.rs +++ b/src/app/command_driver/utilities.rs @@ -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.") diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index fb642f484..d1b05da03 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -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] diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 7b83731e8..b9147c084 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -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", diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index f77225cfd..c1203877c 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -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(); } diff --git a/src/app/control/vision.rs b/src/app/control/vision.rs index 7aa0f3d30..f44121e37 100644 --- a/src/app/control/vision.rs +++ b/src/app/control/vision.rs @@ -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); diff --git a/src/app/dim_viewport.rs b/src/app/dim_viewport.rs index 24a1099b4..db3fc9322 100644 --- a/src/app/dim_viewport.rs +++ b/src/app/dim_viewport.rs @@ -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 { diff --git a/src/app/mtext_editor.rs b/src/app/mtext_editor.rs index 28037ccd8..f9cd5def9 100644 --- a/src/app/mtext_editor.rs +++ b/src/app/mtext_editor.rs @@ -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. diff --git a/src/app/navigation.rs b/src/app/navigation.rs index b14aff5e8..abbb1dfbd 100644 --- a/src/app/navigation.rs +++ b/src/app/navigation.rs @@ -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(); diff --git a/src/app/properties.rs b/src/app/properties.rs index a6acbe114..8dd35fe59 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -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 { diff --git a/src/app/text_inline.rs b/src/app/text_inline.rs index dd1a7bdd9..928534f56 100644 --- a/src/app/text_inline.rs +++ b/src/app/text_inline.rs @@ -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); diff --git a/src/app/update/command.rs b/src/app/update/command.rs index feca3900f..870692e7e 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1177,13 +1177,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { 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 { } { 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 { 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 { pub(super) fn on_qselect_open(&mut self) -> Task { 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, diff --git a/src/app/update/context_menu.rs b/src/app/update/context_menu.rs index 7b4f91a22..1f2df688e 100644 --- a/src/app/update/context_menu.rs +++ b/src/app/update/context_menu.rs @@ -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 { 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 { 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 diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 90f00c5a6..985e2c424 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -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)); diff --git a/src/app/update/file.rs b/src/app/update/file.rs index a31cb0886..b707e7440 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -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 { diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 849d66a45..d0e063b76 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -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(), diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index d10c67fda..d38d7a4a5 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -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, ) -> Option { - 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 { 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 = 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) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index d3caa7a3c..3cbd1ec0b 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -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 = 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 = 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)); diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 679947034..a1aa4bc7b 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -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 = self .model_tiles diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 1adb6bf23..8e95288b1 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -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)], diff --git a/src/scene/pick/selection_state.rs b/src/scene/pick/selection_state.rs index b4fc62baa..2fd67d48b 100644 --- a/src/scene/pick/selection_state.rs +++ b/src/scene/pick/selection_state.rs @@ -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, /// 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, pub box_current: Option, - 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, 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, - /// 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, pub left_down: bool, pub left_press_pos: Option, @@ -47,10 +76,6 @@ pub struct SelectionState { pub right_press_time: Option, pub right_dragging: bool, pub right_last_pos: Option, - /// 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, /// 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, + /// 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, +} + /// 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(); } } - diff --git a/src/scene/preview.rs b/src/scene/preview.rs index c1e7f8c1b..5eacd934c 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -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 { diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 5f42b353e..5ea035186 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -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); diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 4e1450962..e32cb2866 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -1222,7 +1222,7 @@ impl canvas::Program 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 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 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 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 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 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 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( diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs index 9a0192cce..d1a1919c8 100644 --- a/src/ui/popup/context_menu.rs +++ b/src/ui/popup/context_menu.rs @@ -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,