forked from 1b-agent/opencadstudio-upstream
feat(snap): editable Snap X/Y spacing with Equal lock, grid snap locks
Drafting Settings dialog gets text inputs for Snap X/Y spacing plus a working Equal X and Y checkbox (mirrored inputs, Y follows X on apply). Snapper gains fixed snap_spacing_x/y (persisted in UserSettings, default 10) decoupled from the adaptive visible-grid step. Grid snap no longer aperture-gated: with Snap On it rounds every point to the grid, object snaps still override by tier. Adds parse/lock regression tests.
This commit is contained in:
1 parent
1057790456
commit
624fe53382
8 files changed
+238
-36
No files matched your search
@@ -3,10 +3,19 @@ use crate::ui::window::drafting_settings::{DraftingSettingsState, DraftingSettin
|
|||||||
|
|
||||||
impl DraftingSettingsState {
|
impl DraftingSettingsState {
|
||||||
pub(crate) fn from_app(app: &OpenCADStudio) -> Self {
|
pub(crate) fn from_app(app: &OpenCADStudio) -> Self {
|
||||||
|
let snap_x = app.snapper.snap_spacing_x;
|
||||||
|
let snap_y = app.snapper.snap_spacing_y;
|
||||||
|
// The dialog has no stored "equal" flag, so infer it: equal when
|
||||||
|
// X and Y match. This keeps the checkbox checked for the common
|
||||||
|
// case (including fresh defaults of 10/10).
|
||||||
|
let snap_equal = (snap_x - snap_y).abs() < 1e-6;
|
||||||
Self {
|
Self {
|
||||||
active_tab: DraftingSettingsTab::SnapAndGrid,
|
active_tab: DraftingSettingsTab::SnapAndGrid,
|
||||||
snap_on: app.snapper.grid_snap(),
|
snap_on: app.snapper.grid_snap(),
|
||||||
grid_on: app.show_grid,
|
grid_on: app.show_grid,
|
||||||
|
snap_x_input: crate::ui::window::drafting_settings::format_snap_spacing(snap_x),
|
||||||
|
snap_y_input: crate::ui::window::drafting_settings::format_snap_spacing(snap_y),
|
||||||
|
snap_equal,
|
||||||
isometric: app.isometric_drafting,
|
isometric: app.isometric_drafting,
|
||||||
iso_plane: app.iso_plane,
|
iso_plane: app.iso_plane,
|
||||||
snap_angle_deg: app.snap_angle_deg,
|
snap_angle_deg: app.snap_angle_deg,
|
||||||
@@ -33,24 +42,42 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn apply_drafting_settings(&mut self) {
|
pub(super) fn apply_drafting_settings(&mut self) -> bool {
|
||||||
if let Some(state) = &self.drafting_settings_state {
|
let Some(state) = &self.drafting_settings_state.clone() else {
|
||||||
self.show_grid = state.grid_on;
|
return true;
|
||||||
self.snapper.grid_snap_on = state.snap_on;
|
};
|
||||||
self.isometric_drafting = state.isometric;
|
let x = crate::ui::window::drafting_settings::parse_snap_spacing(&state.snap_x_input);
|
||||||
self.iso_plane = state.iso_plane;
|
let y = crate::ui::window::drafting_settings::parse_snap_spacing(&state.snap_y_input);
|
||||||
self.snap_angle_deg = state.snap_angle_deg;
|
let (Some(sx), Some(sy)) = (x, y) else {
|
||||||
self.polar_mode = state.polar_on;
|
self.command_line
|
||||||
self.ortho_mode = state.ortho_on;
|
.push_error(crate::t!("Snap X and Y spacings must be positive numbers.").as_ref());
|
||||||
self.polar_increment_deg = state.polar_increment_deg;
|
return false;
|
||||||
self.snapper.snap_enabled = state.osnap_on;
|
};
|
||||||
self.snapper.otrack_enabled = state.otrack_on;
|
// When locked, Y follows X so the two can never diverge.
|
||||||
self.snapper.enabled = state.snap_modes.clone();
|
let sy = if state.snap_equal { sx } else { sy };
|
||||||
self.dyn_input = state.dyn_input_on;
|
if let Some(live) = &mut self.drafting_settings_state {
|
||||||
self.quick_properties = state.quick_props_on;
|
if live.snap_equal {
|
||||||
self.selection_cycling = state.selection_cycling_on;
|
live.snap_y_input = live.snap_x_input.clone();
|
||||||
self.sync_vport_display(self.active_tab);
|
}
|
||||||
}
|
}
|
||||||
|
self.show_grid = state.grid_on;
|
||||||
|
self.snapper.grid_snap_on = state.snap_on;
|
||||||
|
self.snapper.snap_spacing_x = sx;
|
||||||
|
self.snapper.snap_spacing_y = sy;
|
||||||
|
self.isometric_drafting = state.isometric;
|
||||||
|
self.iso_plane = state.iso_plane;
|
||||||
|
self.snap_angle_deg = state.snap_angle_deg;
|
||||||
|
self.polar_mode = state.polar_on;
|
||||||
|
self.ortho_mode = state.ortho_on;
|
||||||
|
self.polar_increment_deg = state.polar_increment_deg;
|
||||||
|
self.snapper.snap_enabled = state.osnap_on;
|
||||||
|
self.snapper.otrack_enabled = state.otrack_on;
|
||||||
|
self.snapper.enabled = state.snap_modes.clone();
|
||||||
|
self.dyn_input = state.dyn_input_on;
|
||||||
|
self.quick_properties = state.quick_props_on;
|
||||||
|
self.selection_cycling = state.selection_cycling_on;
|
||||||
|
self.sync_vport_display(self.active_tab);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +91,9 @@ mod tests {
|
|||||||
active_tab: DraftingSettingsTab::SnapAndGrid,
|
active_tab: DraftingSettingsTab::SnapAndGrid,
|
||||||
snap_on: false,
|
snap_on: false,
|
||||||
grid_on: true,
|
grid_on: true,
|
||||||
|
snap_x_input: "10".to_string(),
|
||||||
|
snap_y_input: "10".to_string(),
|
||||||
|
snap_equal: true,
|
||||||
isometric: false,
|
isometric: false,
|
||||||
iso_plane: crate::app::settings::IsoPlane::Left,
|
iso_plane: crate::app::settings::IsoPlane::Left,
|
||||||
snap_angle_deg: 0.0,
|
snap_angle_deg: 0.0,
|
||||||
@@ -96,6 +126,9 @@ mod tests {
|
|||||||
active_tab: DraftingSettingsTab::SnapAndGrid,
|
active_tab: DraftingSettingsTab::SnapAndGrid,
|
||||||
snap_on: false,
|
snap_on: false,
|
||||||
grid_on: true,
|
grid_on: true,
|
||||||
|
snap_x_input: "10".to_string(),
|
||||||
|
snap_y_input: "10".to_string(),
|
||||||
|
snap_equal: true,
|
||||||
isometric: false,
|
isometric: false,
|
||||||
iso_plane: crate::app::settings::IsoPlane::Left,
|
iso_plane: crate::app::settings::IsoPlane::Left,
|
||||||
snap_angle_deg: 0.0,
|
snap_angle_deg: 0.0,
|
||||||
@@ -166,5 +199,29 @@ mod tests {
|
|||||||
let mut modded = base.clone();
|
let mut modded = base.clone();
|
||||||
modded.selection_cycling_on = true;
|
modded.selection_cycling_on = true;
|
||||||
assert!(modded.is_dirty(&base));
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.snap_x_input = "5".to_string();
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.snap_y_input = "5".to_string();
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
|
||||||
|
let mut modded = base.clone();
|
||||||
|
modded.snap_equal = false;
|
||||||
|
assert!(modded.is_dirty(&base));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_snap_spacing() {
|
||||||
|
use crate::ui::window::drafting_settings::{format_snap_spacing, parse_snap_spacing};
|
||||||
|
assert_eq!(parse_snap_spacing("10"), Some(10.0));
|
||||||
|
assert_eq!(parse_snap_spacing(" 2.5 "), Some(2.5));
|
||||||
|
assert_eq!(parse_snap_spacing("0"), None);
|
||||||
|
assert_eq!(parse_snap_spacing("-1"), None);
|
||||||
|
assert_eq!(parse_snap_spacing("abc"), None);
|
||||||
|
assert_eq!(parse_snap_spacing(""), None);
|
||||||
|
assert_eq!(format_snap_spacing(10.0), "10");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2741,6 +2741,9 @@ pub enum Message {
|
|||||||
DraftingSettingsTabChanged(crate::ui::window::drafting_settings::DraftingSettingsTab),
|
DraftingSettingsTabChanged(crate::ui::window::drafting_settings::DraftingSettingsTab),
|
||||||
DraftingSettingsToggleGrid,
|
DraftingSettingsToggleGrid,
|
||||||
DraftingSettingsToggleSnap,
|
DraftingSettingsToggleSnap,
|
||||||
|
DraftingSettingsSnapXChanged(String),
|
||||||
|
DraftingSettingsSnapYChanged(String),
|
||||||
|
DraftingSettingsToggleEqualSnap,
|
||||||
DraftingSettingsToggleIsometric,
|
DraftingSettingsToggleIsometric,
|
||||||
DraftingSettingsSetIsoPlane(crate::app::settings::IsoPlane),
|
DraftingSettingsSetIsoPlane(crate::app::settings::IsoPlane),
|
||||||
DraftingSettingsResetRotation,
|
DraftingSettingsResetRotation,
|
||||||
|
|||||||
@@ -434,6 +434,13 @@ pub struct UserSettings {
|
|||||||
deserialize_with = "deserialize_commandline_fade_ms"
|
deserialize_with = "deserialize_commandline_fade_ms"
|
||||||
)]
|
)]
|
||||||
pub commandline_fade_ms: i32,
|
pub commandline_fade_ms: i32,
|
||||||
|
/// SNAPUNIT X/Y spacing used by grid snap. 10 matches the Drafting
|
||||||
|
/// Settings dialog defaults; older configs without these keys fall
|
||||||
|
/// back via `default_snap_spacing`.
|
||||||
|
#[serde(default = "default_snap_spacing")]
|
||||||
|
pub snap_spacing_x: f32,
|
||||||
|
#[serde(default = "default_snap_spacing")]
|
||||||
|
pub snap_spacing_y: f32,
|
||||||
/// Most-recently-inserted block names, most recent first, capped to 20.
|
/// Most-recently-inserted block names, most recent first, capped to 20.
|
||||||
/// Used to rank INSERT suggestions without touching the drawing file.
|
/// Used to rank INSERT suggestions without touching the drawing file.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -455,6 +462,20 @@ fn default_commandline_fade_ms() -> i32 {
|
|||||||
3000
|
3000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default SNAPUNIT spacing shown in the Drafting Settings dialog.
|
||||||
|
fn default_snap_spacing() -> f32 {
|
||||||
|
10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamp a snap spacing to the positive range the dialog accepts.
|
||||||
|
pub fn sanitize_snap_spacing(v: f32) -> f32 {
|
||||||
|
if v.is_finite() && v > 0.0 && v <= 1e9 {
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
10.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize_commandline_fade_ms<'de, D>(de: D) -> Result<i32, D::Error>
|
fn deserialize_commandline_fade_ms<'de, D>(de: D) -> Result<i32, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
@@ -552,6 +573,8 @@ impl Default for UserSettings {
|
|||||||
language: crate::i18n::Language::default(),
|
language: crate::i18n::Language::default(),
|
||||||
cliprompt_lines: 3,
|
cliprompt_lines: 3,
|
||||||
commandline_fade_ms: 3000,
|
commandline_fade_ms: 3000,
|
||||||
|
snap_spacing_x: 10.0,
|
||||||
|
snap_spacing_y: 10.0,
|
||||||
block_mru: Vec::new(),
|
block_mru: Vec::new(),
|
||||||
block_freq: std::collections::HashMap::new(),
|
block_freq: std::collections::HashMap::new(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -443,6 +443,8 @@ impl OpenCADStudio {
|
|||||||
commandline_fade_ms: crate::app::settings::clamp_commandline_fade_ms(
|
commandline_fade_ms: crate::app::settings::clamp_commandline_fade_ms(
|
||||||
self.commandline_fade_ms,
|
self.commandline_fade_ms,
|
||||||
),
|
),
|
||||||
|
snap_spacing_x: self.snapper.snap_spacing_x,
|
||||||
|
snap_spacing_y: self.snapper.snap_spacing_y,
|
||||||
block_mru: self.block_mru.clone(),
|
block_mru: self.block_mru.clone(),
|
||||||
block_freq: self.block_freq.clone(),
|
block_freq: self.block_freq.clone(),
|
||||||
}
|
}
|
||||||
@@ -540,6 +542,10 @@ impl OpenCADStudio {
|
|||||||
crate::app::settings::clamp_commandline_fade_ms(s.commandline_fade_ms);
|
crate::app::settings::clamp_commandline_fade_ms(s.commandline_fade_ms);
|
||||||
self.command_line
|
self.command_line
|
||||||
.set_commandline_fade_ms(self.commandline_fade_ms.clamp(0, 60000) as u32);
|
.set_commandline_fade_ms(self.commandline_fade_ms.clamp(0, 60000) as u32);
|
||||||
|
self.snapper.snap_spacing_x =
|
||||||
|
crate::app::settings::sanitize_snap_spacing(s.snap_spacing_x);
|
||||||
|
self.snapper.snap_spacing_y =
|
||||||
|
crate::app::settings::sanitize_snap_spacing(s.snap_spacing_y);
|
||||||
// Block usage: clone but cap to sane sizes (MRU 20, freq map 200)
|
// Block usage: clone but cap to sane sizes (MRU 20, freq map 200)
|
||||||
self.block_mru = s.block_mru.iter().take(20).cloned().collect();
|
self.block_mru = s.block_mru.iter().take(20).cloned().collect();
|
||||||
self.block_freq = s
|
self.block_freq = s
|
||||||
|
|||||||
+37
-8
@@ -4484,6 +4484,33 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::DraftingSettingsSnapXChanged(value) => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.snap_x_input = value.clone();
|
||||||
|
if state.snap_equal {
|
||||||
|
state.snap_y_input = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsSnapYChanged(value) => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.snap_y_input = value.clone();
|
||||||
|
if state.snap_equal {
|
||||||
|
state.snap_x_input = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::DraftingSettingsToggleEqualSnap => {
|
||||||
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
|
state.snap_equal = !state.snap_equal;
|
||||||
|
if state.snap_equal {
|
||||||
|
state.snap_y_input = state.snap_x_input.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::DraftingSettingsToggleIsometric => {
|
Message::DraftingSettingsToggleIsometric => {
|
||||||
if let Some(state) = &mut self.drafting_settings_state {
|
if let Some(state) = &mut self.drafting_settings_state {
|
||||||
state.isometric = !state.isometric;
|
state.isometric = !state.isometric;
|
||||||
@@ -4574,17 +4601,19 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::DraftingSettingsApply => {
|
Message::DraftingSettingsApply => {
|
||||||
self.apply_drafting_settings();
|
if self.apply_drafting_settings() {
|
||||||
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::DraftingSettingsOk => {
|
Message::DraftingSettingsOk => {
|
||||||
self.apply_drafting_settings();
|
if self.apply_drafting_settings() {
|
||||||
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
||||||
self.drafting_settings_close_confirm = false;
|
self.drafting_settings_close_confirm = false;
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
self.close_active_modal();
|
self.close_active_modal();
|
||||||
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::DraftingSettingsClose => {
|
Message::DraftingSettingsClose => {
|
||||||
|
|||||||
@@ -1928,7 +1928,9 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Sync grid-snap spacing to the adaptive spacing of the visible grid.
|
// Sync the adaptive visible-grid step. It drives display and
|
||||||
|
// snap-aperture tolerances — grid *snap* itself locks to the
|
||||||
|
// fixed SNAPUNIT X/Y spacing from the Drafting Settings dialog.
|
||||||
self.snapper.grid_spacing = grid_spacing;
|
self.snapper.grid_spacing = grid_spacing;
|
||||||
// Cursor and wires are model-space; the snap result is model.
|
// Cursor and wires are model-space; the snap result is model.
|
||||||
let snap_cursor = cursor_world;
|
let snap_cursor = cursor_world;
|
||||||
|
|||||||
+53
-6
@@ -149,6 +149,11 @@ pub struct Snapper {
|
|||||||
pub grid_snap_on: bool,
|
pub grid_snap_on: bool,
|
||||||
/// World-space grid spacing.
|
/// World-space grid spacing.
|
||||||
pub grid_spacing: f32,
|
pub grid_spacing: f32,
|
||||||
|
/// User-configurable snap spacing (SNAPUNIT X/Y). Used for grid-snap
|
||||||
|
/// positions; kept separate from the adaptive `grid_spacing`, which
|
||||||
|
/// tracks the visible grid step and drives tolerances.
|
||||||
|
pub snap_spacing_x: f32,
|
||||||
|
pub snap_spacing_y: f32,
|
||||||
/// Pixel-radius snap aperture, shared by OSNAP, tracking, polar and
|
/// Pixel-radius snap aperture, shared by OSNAP, tracking, polar and
|
||||||
/// extension so the catch distance is the same everywhere.
|
/// extension so the catch distance is the same everywhere.
|
||||||
pub osnap_radius_px: f32,
|
pub osnap_radius_px: f32,
|
||||||
@@ -216,6 +221,8 @@ impl Default for Snapper {
|
|||||||
enabled,
|
enabled,
|
||||||
grid_snap_on: false,
|
grid_snap_on: false,
|
||||||
grid_spacing: 1.0,
|
grid_spacing: 1.0,
|
||||||
|
snap_spacing_x: 10.0,
|
||||||
|
snap_spacing_y: 10.0,
|
||||||
osnap_radius_px: DEFAULT_OSNAP_RADIUS_PX,
|
osnap_radius_px: DEFAULT_OSNAP_RADIUS_PX,
|
||||||
otrack_enabled: false,
|
otrack_enabled: false,
|
||||||
tracking_points: Vec::new(),
|
tracking_points: Vec::new(),
|
||||||
@@ -1012,6 +1019,8 @@ impl Snapper {
|
|||||||
},
|
},
|
||||||
grid_snap_on: false,
|
grid_snap_on: false,
|
||||||
grid_spacing: self.grid_spacing,
|
grid_spacing: self.grid_spacing,
|
||||||
|
snap_spacing_x: self.snap_spacing_x,
|
||||||
|
snap_spacing_y: self.snap_spacing_y,
|
||||||
osnap_radius_px: self.osnap_radius_px,
|
osnap_radius_px: self.osnap_radius_px,
|
||||||
otrack_enabled: false,
|
otrack_enabled: false,
|
||||||
tracking_points: Vec::new(),
|
tracking_points: Vec::new(),
|
||||||
@@ -1084,9 +1093,16 @@ impl Snapper {
|
|||||||
// Object snaps therefore NEVER catch grid points; only when grid snap is
|
// Object snaps therefore NEVER catch grid points; only when grid snap is
|
||||||
// on can a grid corner be picked. It is evaluated first and at the
|
// on can a grid corner be picked. It is evaluated first and at the
|
||||||
// lowest priority, so any object snap inside the aperture overrides it.
|
// lowest priority, so any object snap inside the aperture overrides it.
|
||||||
|
// Unlike object snap, grid snap LOCKS: like AutoCAD SNAP, every point
|
||||||
|
// rounds to the grid — it is not gated by the aperture. (Gating it
|
||||||
|
// would break as soon as the fixed SNAPUNIT spacing differs from the
|
||||||
|
// adaptive visible-grid step on screen.)
|
||||||
if self.grid_snap_on {
|
if self.grid_snap_on {
|
||||||
let s = self.grid_spacing as f64;
|
let sx = self.snap_spacing_x as f64;
|
||||||
if s.abs() > 1e-9 {
|
let sy = self.snap_spacing_y as f64;
|
||||||
|
// Z has no independent spacing in the dialog; follow X.
|
||||||
|
let sz = sx;
|
||||||
|
if sx.abs() > 1e-9 && sy.abs() > 1e-9 && sz.abs() > 1e-9 {
|
||||||
// Round in the UCS grid frame, then map back to world.
|
// Round in the UCS grid frame, then map back to world.
|
||||||
let (ax, ay, az) = grid_axes;
|
let (ax, ay, az) = grid_axes;
|
||||||
let ax = ax.normalize_or(Vec3::X).as_dvec3();
|
let ax = ax.normalize_or(Vec3::X).as_dvec3();
|
||||||
@@ -1107,13 +1123,16 @@ impl Snapper {
|
|||||||
} else {
|
} else {
|
||||||
(rel.dot(ax), rel.dot(ay))
|
(rel.dot(ax), rel.dot(ay))
|
||||||
};
|
};
|
||||||
let ux = (ux / s).round() * s;
|
let ux = (ux / sx).round() * sx;
|
||||||
let uy = (uy / s).round() * s;
|
let uy = (uy / sy).round() * sy;
|
||||||
let uz = (rel.dot(az) / s).round() * s;
|
let uz = (rel.dot(az) / sz).round() * sz;
|
||||||
let gp = origin + ax * ux + ay * uy + az * uz;
|
let gp = origin + ax * ux + ay * uy + az * uz;
|
||||||
let screen = world_to_screen(gp, view_rot, eye, bounds);
|
let screen = world_to_screen(gp, view_rot, eye, bounds);
|
||||||
let d2 = dist2(screen, cursor_screen);
|
let d2 = dist2(screen, cursor_screen);
|
||||||
if d2 < radius2 && in_bounds(screen) {
|
// Lock, don't magnet: any in-pane grid point stands, so the
|
||||||
|
// cursor jumps grid-to-grid even when the SNAPUNIT spacing
|
||||||
|
// is far from the adaptive visible-grid step.
|
||||||
|
if in_bounds(screen) {
|
||||||
best = Some(SnapResult {
|
best = Some(SnapResult {
|
||||||
world: gp,
|
world: gp,
|
||||||
screen,
|
screen,
|
||||||
@@ -3815,4 +3834,32 @@ mod ext_tests {
|
|||||||
assert_eq!(res.world, DVec3::ZERO);
|
assert_eq!(res.world, DVec3::ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grid_snap_locks_beyond_aperture_with_independent_xy() {
|
||||||
|
let mut s = Snapper::default();
|
||||||
|
s.grid_snap_on = true;
|
||||||
|
s.snap_enabled = false; // grid only
|
||||||
|
s.snap_spacing_x = 1.0;
|
||||||
|
s.snap_spacing_y = 0.5;
|
||||||
|
s.osnap_radius_px = 15.0;
|
||||||
|
|
||||||
|
let view_rot = Mat4::IDENTITY;
|
||||||
|
let eye = DVec3::new(0.0, 0.0, 500.0);
|
||||||
|
let bounds = Rectangle { x: 0.0, y: 0.0, width: 1000.0, height: 1000.0 };
|
||||||
|
let wires: Vec<WireModel> = Vec::new();
|
||||||
|
|
||||||
|
// 500 px per world unit: the cursor sits ~160 px from the nearest
|
||||||
|
// grid corner — far outside the 15 px aperture — yet SNAP must still
|
||||||
|
// lock like AutoCAD instead of magnetizing only when near a point.
|
||||||
|
let cursor_world = DVec3::new(0.32, 0.47, 500.0);
|
||||||
|
let cursor_screen = world_to_screen(cursor_world, view_rot, eye, bounds);
|
||||||
|
let res = s.snap(
|
||||||
|
cursor_world, cursor_screen, &wires,
|
||||||
|
view_rot, eye, bounds, Vec3::ZERO, (Vec3::X, Vec3::Y, Vec3::Z), None,
|
||||||
|
).expect("grid snap must lock even outside the aperture");
|
||||||
|
assert_eq!(res.snap_type, SnapType::Grid);
|
||||||
|
assert!((res.world.x - 0.0).abs() < 1e-9, "x rounds to 0: {:?}", res.world);
|
||||||
|
assert!((res.world.y - 0.5).abs() < 1e-9, "y rounds to 0.5: {:?}", res.world);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::app::settings::IsoPlane;
|
use crate::app::settings::IsoPlane;
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::snap::{SnapType, ALL_SNAP_MODES};
|
use crate::snap::{SnapType, ALL_SNAP_MODES};
|
||||||
use iced::widget::{button, checkbox, column, container, row, scrollable, text, Space};
|
use iced::widget::{button, checkbox, column, container, row, scrollable, text, text_input, Space};
|
||||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
@@ -28,6 +28,9 @@ pub struct DraftingSettingsState {
|
|||||||
// Snap and Grid
|
// Snap and Grid
|
||||||
pub snap_on: bool,
|
pub snap_on: bool,
|
||||||
pub grid_on: bool,
|
pub grid_on: bool,
|
||||||
|
pub snap_x_input: String,
|
||||||
|
pub snap_y_input: String,
|
||||||
|
pub snap_equal: bool,
|
||||||
pub isometric: bool,
|
pub isometric: bool,
|
||||||
pub iso_plane: IsoPlane,
|
pub iso_plane: IsoPlane,
|
||||||
pub snap_angle_deg: f32,
|
pub snap_angle_deg: f32,
|
||||||
@@ -55,6 +58,9 @@ impl DraftingSettingsState {
|
|||||||
pub fn is_dirty(&self, saved: &Self) -> bool {
|
pub fn is_dirty(&self, saved: &Self) -> bool {
|
||||||
self.snap_on != saved.snap_on
|
self.snap_on != saved.snap_on
|
||||||
|| self.grid_on != saved.grid_on
|
|| self.grid_on != saved.grid_on
|
||||||
|
|| self.snap_x_input != saved.snap_x_input
|
||||||
|
|| self.snap_y_input != saved.snap_y_input
|
||||||
|
|| self.snap_equal != saved.snap_equal
|
||||||
|| self.isometric != saved.isometric
|
|| self.isometric != saved.isometric
|
||||||
|| self.iso_plane != saved.iso_plane
|
|| self.iso_plane != saved.iso_plane
|
||||||
|| (self.snap_angle_deg - saved.snap_angle_deg).abs() > 0.001
|
|| (self.snap_angle_deg - saved.snap_angle_deg).abs() > 0.001
|
||||||
@@ -71,6 +77,25 @@ impl DraftingSettingsState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format a snap spacing value for the dialog's text inputs.
|
||||||
|
pub fn format_snap_spacing(v: f32) -> String {
|
||||||
|
if (v - v.round()).abs() < 1e-4 {
|
||||||
|
format!("{}", v.round() as i32)
|
||||||
|
} else {
|
||||||
|
format!("{v}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a snap spacing input. Spacings must be positive and finite.
|
||||||
|
pub fn parse_snap_spacing(s: &str) -> Option<f32> {
|
||||||
|
let v: f32 = s.trim().parse().ok()?;
|
||||||
|
if v.is_finite() && v > 0.0 && v <= 1e9 {
|
||||||
|
Some(v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper for grouped sub-panels with a light border and header title.
|
/// Helper for grouped sub-panels with a light border and header title.
|
||||||
fn group<'a>(title: impl Into<String>, body: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
fn group<'a>(title: impl Into<String>, body: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
|
||||||
container(
|
container(
|
||||||
@@ -187,18 +212,28 @@ pub fn view_window<'a>(
|
|||||||
column![
|
column![
|
||||||
row![
|
row![
|
||||||
text(crate::t!("Snap X spacing:")).size(11).width(120),
|
text(crate::t!("Snap X spacing:")).size(11).width(120),
|
||||||
text("10.00").size(11),
|
text_input("10", &state.snap_x_input)
|
||||||
|
.on_input(Message::DraftingSettingsSnapXChanged)
|
||||||
|
.size(11)
|
||||||
|
.padding([4, 7])
|
||||||
|
.width(100),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
row![
|
row![
|
||||||
text(crate::t!("Snap Y spacing:")).size(11).width(120),
|
text(crate::t!("Snap Y spacing:")).size(11).width(120),
|
||||||
text("10.00").size(11),
|
text_input("10", &state.snap_y_input)
|
||||||
|
.on_input(Message::DraftingSettingsSnapYChanged)
|
||||||
|
.size(11)
|
||||||
|
.padding([4, 7])
|
||||||
|
.width(100),
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(iced::Center),
|
.align_y(iced::Center),
|
||||||
row![
|
row![
|
||||||
checkbox(true).size(14),
|
checkbox(state.snap_equal)
|
||||||
|
.on_toggle(|_| Message::DraftingSettingsToggleEqualSnap)
|
||||||
|
.size(14),
|
||||||
text(crate::t!("Equal X and Y spacing")).size(11),
|
text(crate::t!("Equal X and Y spacing")).size(11),
|
||||||
]
|
]
|
||||||
.spacing(6)
|
.spacing(6)
|
||||||
|
|||||||
Reference in new issue
Block a user