forked from 1b-agent/opencadstudio-upstream
Merge PR #293: snap plot-window corners to geometry endpoints
This commit is contained in:
commit
1e8097d944
4 files changed
+121
-2
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user