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 {
|
||||
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 {
|
||||
active_tab: DraftingSettingsTab::SnapAndGrid,
|
||||
snap_on: app.snapper.grid_snap(),
|
||||
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,
|
||||
iso_plane: app.iso_plane,
|
||||
snap_angle_deg: app.snap_angle_deg,
|
||||
@@ -33,24 +42,42 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn apply_drafting_settings(&mut self) {
|
||||
if let Some(state) = &self.drafting_settings_state {
|
||||
self.show_grid = state.grid_on;
|
||||
self.snapper.grid_snap_on = state.snap_on;
|
||||
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);
|
||||
pub(super) fn apply_drafting_settings(&mut self) -> bool {
|
||||
let Some(state) = &self.drafting_settings_state.clone() else {
|
||||
return true;
|
||||
};
|
||||
let x = crate::ui::window::drafting_settings::parse_snap_spacing(&state.snap_x_input);
|
||||
let y = crate::ui::window::drafting_settings::parse_snap_spacing(&state.snap_y_input);
|
||||
let (Some(sx), Some(sy)) = (x, y) else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Snap X and Y spacings must be positive numbers.").as_ref());
|
||||
return false;
|
||||
};
|
||||
// When locked, Y follows X so the two can never diverge.
|
||||
let sy = if state.snap_equal { sx } else { sy };
|
||||
if let Some(live) = &mut self.drafting_settings_state {
|
||||
if live.snap_equal {
|
||||
live.snap_y_input = live.snap_x_input.clone();
|
||||
}
|
||||
}
|
||||
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,
|
||||
snap_on: false,
|
||||
grid_on: true,
|
||||
snap_x_input: "10".to_string(),
|
||||
snap_y_input: "10".to_string(),
|
||||
snap_equal: true,
|
||||
isometric: false,
|
||||
iso_plane: crate::app::settings::IsoPlane::Left,
|
||||
snap_angle_deg: 0.0,
|
||||
@@ -96,6 +126,9 @@ mod tests {
|
||||
active_tab: DraftingSettingsTab::SnapAndGrid,
|
||||
snap_on: false,
|
||||
grid_on: true,
|
||||
snap_x_input: "10".to_string(),
|
||||
snap_y_input: "10".to_string(),
|
||||
snap_equal: true,
|
||||
isometric: false,
|
||||
iso_plane: crate::app::settings::IsoPlane::Left,
|
||||
snap_angle_deg: 0.0,
|
||||
@@ -166,5 +199,29 @@ mod tests {
|
||||
let mut modded = base.clone();
|
||||
modded.selection_cycling_on = true;
|
||||
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),
|
||||
DraftingSettingsToggleGrid,
|
||||
DraftingSettingsToggleSnap,
|
||||
DraftingSettingsSnapXChanged(String),
|
||||
DraftingSettingsSnapYChanged(String),
|
||||
DraftingSettingsToggleEqualSnap,
|
||||
DraftingSettingsToggleIsometric,
|
||||
DraftingSettingsSetIsoPlane(crate::app::settings::IsoPlane),
|
||||
DraftingSettingsResetRotation,
|
||||
|
||||
@@ -434,6 +434,13 @@ pub struct UserSettings {
|
||||
deserialize_with = "deserialize_commandline_fade_ms"
|
||||
)]
|
||||
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.
|
||||
/// Used to rank INSERT suggestions without touching the drawing file.
|
||||
#[serde(default)]
|
||||
@@ -455,6 +462,20 @@ fn default_commandline_fade_ms() -> i32 {
|
||||
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>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
@@ -552,6 +573,8 @@ impl Default for UserSettings {
|
||||
language: crate::i18n::Language::default(),
|
||||
cliprompt_lines: 3,
|
||||
commandline_fade_ms: 3000,
|
||||
snap_spacing_x: 10.0,
|
||||
snap_spacing_y: 10.0,
|
||||
block_mru: Vec::new(),
|
||||
block_freq: std::collections::HashMap::new(),
|
||||
}
|
||||
|
||||
@@ -443,6 +443,8 @@ impl OpenCADStudio {
|
||||
commandline_fade_ms: crate::app::settings::clamp_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_freq: self.block_freq.clone(),
|
||||
}
|
||||
@@ -540,6 +542,10 @@ impl OpenCADStudio {
|
||||
crate::app::settings::clamp_commandline_fade_ms(s.commandline_fade_ms);
|
||||
self.command_line
|
||||
.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)
|
||||
self.block_mru = s.block_mru.iter().take(20).cloned().collect();
|
||||
self.block_freq = s
|
||||
|
||||
+37
-8
@@ -4484,6 +4484,33 @@ impl OpenCADStudio {
|
||||
}
|
||||
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 => {
|
||||
if let Some(state) = &mut self.drafting_settings_state {
|
||||
state.isometric = !state.isometric;
|
||||
@@ -4574,17 +4601,19 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsApply => {
|
||||
self.apply_drafting_settings();
|
||||
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
||||
self.persist_settings_if_changed();
|
||||
if self.apply_drafting_settings() {
|
||||
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DraftingSettingsOk => {
|
||||
self.apply_drafting_settings();
|
||||
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
||||
self.drafting_settings_close_confirm = false;
|
||||
self.persist_settings_if_changed();
|
||||
self.close_active_modal();
|
||||
if self.apply_drafting_settings() {
|
||||
self.drafting_settings_saved = self.drafting_settings_state.clone();
|
||||
self.drafting_settings_close_confirm = false;
|
||||
self.persist_settings_if_changed();
|
||||
self.close_active_modal();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
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;
|
||||
// Cursor and wires are model-space; the snap result is model.
|
||||
let snap_cursor = cursor_world;
|
||||
|
||||
Reference in new issue
Block a user