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

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

No files matched your search

+3 -3
View File
@@ -430,7 +430,7 @@ impl Scene {
/// wrong whenever a tiled viewport is active.
pub(super) fn active_camera_aspect(&self) -> f32 {
if self.current_layout == "Model" {
let (canvas_w, canvas_h) = self.selection.borrow().vp_size;
let (canvas_w, canvas_h) = self.selection.borrow().view.vp_size;
let tiles = self.model_tiles.borrow();
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
if let Some(tile) = tiles.get(active) {
@@ -803,7 +803,7 @@ impl Scene {
z: view_dir.z as f64,
};
entry.view_height = view_height as f64;
let (canvas_width, canvas_height) = self.selection.borrow().vp_size;
let (canvas_width, canvas_height) = self.selection.borrow().view.vp_size;
let viewport_width = (upper_right.x - lower_left.x).abs() * canvas_width as f64;
let viewport_height = (upper_right.y - lower_left.y).abs() * canvas_height as f64;
if viewport_width > 1e-9 && viewport_height > 1e-9 {
@@ -1556,7 +1556,7 @@ impl Scene {
return;
};
let (canvas_w, canvas_h) = self.selection.borrow().vp_size;
let (canvas_w, canvas_h) = self.selection.borrow().view.vp_size;
let fallback = self.last_render_aspect.get().max(0.01);
let aspects: Vec<f32> = self
.model_tiles
+4 -4
View File
@@ -3068,7 +3068,7 @@ impl super::Scene {
/// `(id, anchor, outward_screen_direction, label, is_conflicting,
/// hover_points)` for every enabled, visible constraint whose glyph
/// projects on-screen.
/// `vp_size` is the full canvas size (as `SelectionState::vp_size`
/// `vp_size` is the full canvas size (as `SelectionView::vp_size`
/// reports it), matching what `viewport_edit_frame`/
/// `active_model_tile_bounds` expect. Mirrors the projection
/// `crate::app::view` builds its own render list with. The memoised
@@ -3808,7 +3808,7 @@ mod tests {
Vector3::new(1.0, 0.0, 0.0),
),
));
scene.selection.borrow_mut().vp_size = (800.0, 600.0);
scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let id = scene.parametric_constraint_set_mut(scope).add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
@@ -3848,7 +3848,7 @@ mod tests {
Vector3::new(1.0, 0.0, 0.0),
),
));
scene.selection.borrow_mut().vp_size = (800.0, 600.0);
scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let id = scene.parametric_constraint_set_mut(scope).add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
@@ -3878,7 +3878,7 @@ mod tests {
Vector3::new(1.0, 0.0, 0.0),
),
));
scene.selection.borrow_mut().vp_size = (800.0, 600.0);
scene.selection.borrow_mut().view.vp_size = (800.0, 600.0);
let id = scene.parametric_constraint_set_mut(scope).add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
+87 -40
View File
@@ -2,9 +2,29 @@ use iced::time::Instant;
use iced::Point;
/// Mouse / selection interaction state for the viewport.
///
/// Split (#32) into five sub-structs by family so a per-frame clone can copy
/// only the family it needs and gesture clears touch one family at a time.
/// The `RefCell` around this struct (`Scene::selection`) is untouched.
#[derive(Clone, Default)]
pub struct SelectionState {
pub view: SelectionView,
pub gesture: SelectionGesture,
pub input: SelectionInput,
pub menu: SelectionMenu,
pub orbit: SelectionOrbit,
}
/// View-dependent snapshot: the canvas size selection geometry is expressed in.
#[derive(Clone, Default)]
pub struct SelectionView {
pub vp_size: (f32, f32),
}
/// One box-or-lasso gesture: anchor/current corners, crossing sense, and the
/// in-progress polygon. Cleared together by [`SelectionGesture::clear_left`].
#[derive(Clone, Default)]
pub struct SelectionGesture {
pub box_anchor: Option<Point>,
/// World point under the box-selection anchor, so the anchor can be
/// re-projected to screen when the camera zooms/pans mid-drag instead of
@@ -12,14 +32,12 @@ pub struct SelectionState {
/// (#234)
pub box_anchor_world: Option<glam::DVec3>,
pub box_current: Option<Point>,
pub box_last: Option<(Point, Point)>,
pub box_crossing: bool,
/// Set when a Window / Crossing selection keyword fixed the sense of the
/// box being drawn. Dragging normally decides it from the direction the
/// corner travels, which would immediately overwrite what the user just
/// asked for, so that derivation stands down while this holds.
pub box_crossing_locked: bool,
pub box_last_crossing: bool,
/// A preview-only selection marquee `(anchor, current, crossing)` in canvas
/// pixels, drawn identically to a real box-selection (green crossing fill /
/// blue window fill) but with NO hit-test behaviour. Commands that pick a
@@ -29,14 +47,25 @@ pub struct SelectionState {
pub poly_active: bool,
pub poly_points: Vec<Point>,
pub poly_crossing: bool,
pub poly_last_crossing: bool,
/// Canvas position the right-click context menu is anchored at while it
/// is open. `None` = closed. Its rows are rebuilt from app state every
/// frame (`ui::popup::context_menu::build_context_menu`).
pub context_menu: Option<Point>,
/// Transient UI state of the open context menu (expanded submenu,
/// keyboard highlight). Reset every time the menu opens.
pub context_menu_ui: ContextMenuUi,
}
impl SelectionGesture {
/// End the in-progress box/lasso gesture (left-button release path).
pub fn clear_left(&mut self) {
self.box_anchor = None;
self.box_anchor_world = None;
self.box_current = None;
self.box_crossing = false;
self.box_crossing_locked = false;
self.poly_active = false;
self.poly_points.clear();
self.poly_crossing = false;
}
}
/// Raw button/pointer state: press positions, drag flags, orbit pivot input.
#[derive(Clone, Default)]
pub struct SelectionInput {
pub last_move_pos: Option<Point>,
pub left_down: bool,
pub left_press_pos: Option<Point>,
@@ -47,10 +76,6 @@ pub struct SelectionState {
pub right_press_time: Option<Instant>,
pub right_dragging: bool,
pub right_last_pos: Option<Point>,
/// World point the current orbit drag revolves around (selection or model
/// centre), captured when the drag starts so it stays fixed for the whole
/// gesture. `None` when no orbit is in progress. (#229)
pub orbit_pivot: Option<glam::DVec3>,
/// While a command is active, a right-click acts as Enter; the *next*
/// consecutive right-click opens the context menu instead. This tracks
/// whether the previous right-click already fired Enter. Reset by any
@@ -66,6 +91,52 @@ pub struct SelectionState {
pub zoom_dir_out: bool,
}
impl SelectionInput {
/// Release the left button (click-commit path).
pub fn clear_left_buttons(&mut self) {
self.left_down = false;
self.left_press_pos = None;
self.left_press_time = None;
self.left_dragging = false;
}
}
/// Right-click context-menu state.
#[derive(Clone, Default)]
pub struct SelectionMenu {
/// Canvas position the right-click context menu is anchored at while it
/// is open. `None` = closed. Its rows are rebuilt from app state every
/// frame (`ui::popup::context_menu::build_context_menu`).
pub open_at: Option<Point>,
/// Transient UI state of the open context menu (expanded submenu,
/// keyboard highlight). Reset every time the menu opens.
pub ui: ContextMenuUi,
}
impl SelectionMenu {
/// Open the context menu at `at`, discarding the previous menu's
/// expanded-submenu / highlight state.
pub fn open(&mut self, at: Point) {
self.open_at = Some(at);
self.ui = ContextMenuUi::default();
}
/// Close the context menu (no-op when it is not open).
pub fn close(&mut self) {
self.open_at = None;
self.ui = ContextMenuUi::default();
}
}
/// Orbit-drag state.
#[derive(Clone, Default)]
pub struct SelectionOrbit {
/// World point the current orbit drag revolves around (selection or model
/// centre), captured when the drag starts so it stays fixed for the whole
/// gesture. `None` when no orbit is in progress. (#229)
pub pivot: Option<glam::DVec3>,
}
/// Transient state of the open right-click context menu.
#[derive(Clone, Default)]
pub struct ContextMenuUi {
@@ -78,36 +149,12 @@ pub struct ContextMenuUi {
}
impl SelectionState {
/// Open the context menu at `at`, discarding the previous menu's
/// expanded-submenu / highlight state.
pub fn open_context_menu(&mut self, at: Point) {
self.context_menu = Some(at);
self.context_menu_ui = ContextMenuUi::default();
}
/// Close the context menu (no-op when it is not open).
pub fn close_context_menu(&mut self) {
self.context_menu = None;
self.context_menu_ui = ContextMenuUi::default();
}
/// End every left-button selection gesture without disturbing the previous
/// completed-window record or command-owned preview marquee. Grip editing
/// owns the left button while engaged and calls this before/after placement
/// so a small pointer move cannot also arm a box or lasso selection.
pub fn clear_left_selection_gesture(&mut self) {
self.left_down = false;
self.left_press_pos = None;
self.left_press_time = None;
self.left_dragging = false;
self.box_anchor = None;
self.box_anchor_world = None;
self.box_current = None;
self.box_crossing = false;
self.box_crossing_locked = false;
self.poly_active = false;
self.poly_points.clear();
self.poly_crossing = false;
self.input.clear_left_buttons();
self.gesture.clear_left();
}
}
+1 -1
View File
@@ -281,7 +281,7 @@ impl Scene {
self.interim_wire = None;
// Drop any point-picked window marquee (STRETCH) so it doesn't linger
// after the command ends before the next mouse move. (#291)
self.selection.borrow_mut().preview_box = None;
self.selection.borrow_mut().gesture.preview_box = None;
}
pub fn wire_models_for(&self, handles: &[codec::Handle]) -> Vec<WireModel> {
+1 -1
View File
@@ -4213,7 +4213,7 @@ impl Scene {
// `CursorMoved` handler — the cube overlay sits above the shader
// and would otherwise mask the move event from `Program::update`.
let hover_region = show_interaction.then(|| self.viewcube_hover.get()).flatten();
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
self.selection.borrow_mut().view.vp_size = (bounds.width, bounds.height);
if bounds.height > 0.0 {
self.set_render_aspect(bounds.width / bounds.height);
self.set_render_pixel_scale(bounds.width, bounds.height);