Merge PR #293: snap plot-window corners to geometry endpoints

This commit is contained in:
Hakan Seven committed 2026-07-06 20:15:03 +03:00
commit 1e8097d944
4 files changed
+121 -2

No files matched your search

+13 -2
View File
@@ -889,8 +889,19 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
} else {
let (go, gr) = self.tabs[i].ucs_grid_basis();
self.snapper.from_point = self.last_point;
self.snapper
.snap(snap_cursor, p, &all_wires[..], view_rot, eye, bounds, go, gr)
let force_corners = self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.forces_endpoint_snap())
.unwrap_or(false);
if force_corners {
self.snapper.snap_forced_corners(
snap_cursor, p, &all_wires[..], view_rot, eye, bounds, go, gr,
)
} else {
self.snapper
.snap(snap_cursor, p, &all_wires[..], view_rot, eye, bounds, go, gr)
}
};
// Object Snap Tracking: update dwell, then align the cursor
+7
View File
@@ -530,6 +530,13 @@ pub trait CadCommand: Send {
false
}
/// True if this command should have Endpoint/Intersection object-snap
/// forced on while it is active, regardless of the global OSNAP toggle.
/// Used by PLOTWINDOW so the plot-area corners always snap to geometry.
fn forces_endpoint_snap(&self) -> bool {
false
}
/// Called when the text editor closes, either because the user committed or cancelled the edit.
fn on_editor_closed(&mut self, _committed: bool) -> CmdResult {
CmdResult::Cancel
+15
View File
@@ -22,6 +22,10 @@ impl CadCommand for PlotWindowCommand {
"PLOTWINDOW"
}
fn forces_endpoint_snap(&self) -> bool {
true
}
fn prompt(&self) -> String {
if self.p1.is_none() {
"PLOTWINDOW Specify first corner of plot window:".into()
@@ -81,3 +85,14 @@ impl CadCommand for PlotWindowCommand {
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["PLOTWINDOW", "PW"] }); // PlotWindowCommand
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plot_window_forces_endpoint_snap() {
let cmd = PlotWindowCommand::new();
assert!(cmd.forces_endpoint_snap());
}
}
+86
View File
@@ -1355,6 +1355,48 @@ impl Snapper {
best
}
/// The exact object-snap mode set used while corner snap is forced.
///
/// Endpoint only, deliberately: frame corners are line endpoints, so
/// Endpoint (a single O(n) pass over vertices) catches them. Intersection
/// snap is O(n²) over every pair of segments in the hit-test wire set — on a
/// large drawing viewed zoomed-out (exactly when a plot window is picked)
/// that froze the UI, so it is excluded from the forced set.
fn forced_corner_modes() -> HashSet<SnapType> {
let mut modes = HashSet::default();
modes.insert(SnapType::Endpoint);
modes
}
/// Run object snap with the active modes replaced by exactly Endpoint,
/// regardless of the global OSNAP master toggle or the user's configured
/// modes, then restore both afterward. Commands that always want corner
/// snapping (PLOTWINDOW) call this so the plot-area corners snap without
/// changing the user's global OSNAP settings.
#[allow(clippy::too_many_arguments)]
pub fn snap_forced_corners(
&mut self,
cursor_world: glam::DVec3,
cursor_screen: Point,
wires: &[WireModel],
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
grid_origin: Vec3,
grid_rot: Mat4,
) -> Option<SnapResult> {
let saved_master = self.snap_enabled;
let saved_modes = self.enabled.clone();
self.snap_enabled = true;
self.enabled = Self::forced_corner_modes();
let result = self.snap(
cursor_world, cursor_screen, wires, view_rot, eye, bounds, grid_origin, grid_rot,
);
self.snap_enabled = saved_master;
self.enabled = saved_modes;
result
}
}
// ── Object-snap priority ───────────────────────────────────────────────────
@@ -1873,3 +1915,47 @@ mod ext_tests {
assert!(circle_tangent_points(Vec3::new(1.0, 0.0, 0.0), c, 5.0).is_none());
}
}
#[cfg(test)]
mod forced_corner_tests {
use super::*;
#[test]
fn snap_forced_corners_restores_state() {
let mut s = Snapper::default();
// Master off, and a non-default configured mode set.
s.snap_enabled = false;
s.enabled.clear();
s.enabled.insert(SnapType::Midpoint);
let before = s.enabled.clone();
// Empty wire set → no candidate → None; we only assert the state
// is untouched afterward (the risky save/restore path).
let r = s.snap_forced_corners(
glam::DVec3::ZERO,
Point::new(0.0, 0.0),
&[],
Mat4::IDENTITY,
glam::DVec3::ZERO,
Rectangle { x: 0.0, y: 0.0, width: 800.0, height: 600.0 },
Vec3::ZERO,
Mat4::IDENTITY,
);
assert!(r.is_none());
assert!(!s.snap_enabled, "master toggle must be restored to off");
assert_eq!(s.enabled, before, "configured modes must be restored");
}
#[test]
fn forced_corner_modes_are_endpoint_only() {
let modes = Snapper::forced_corner_modes();
assert!(modes.contains(&SnapType::Endpoint));
// Intersection is O(n²) over the wire set and is deliberately excluded —
// forcing it froze the UI on large drawings.
assert!(!modes.contains(&SnapType::Intersection));
assert!(!modes.contains(&SnapType::Midpoint));
assert!(!modes.contains(&SnapType::Center));
assert_eq!(modes.len(), 1);
}
}