diff --git a/src/app/mod.rs b/src/app/mod.rs index 357a8a73e..d880c9918 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -517,6 +517,9 @@ pub(super) struct OpenCADStudio { crosshair_color: Option<[u8; 3]>, /// Editable Options buffer for the crosshair colour. crosshair_color_input: String, + /// Edit buffer for the SNAPANG field on the Options Drafting page. Kept + /// separate from `snap_angle_deg` so a half-typed angle is not parsed. + snap_angle_input: String, /// Model-space lineweight preview scale, in percent (25..=200). lineweight_display_scale: i32, /// Isometric drafting state and active axis pair. @@ -2029,6 +2032,10 @@ pub enum Message { QdimSnapPriorityChanged(u8), /// Change which annotative objects pick up a new scale (ANNOAUTOSCALE). AnnoAutoScaleChanged(i8), + /// Edit the drafting rotation field; parsed when it holds a valid angle (SNAPANG). + SnapAngleInputChanged(String), + /// Change the polar tracking increment in degrees. + PolarIncrementChanged(f32), /// Restore Model Space display/canvas appearance to defaults. RestoreModelSpaceDisplayDefaults, /// Restore Selection visual effect settings to defaults. @@ -3429,6 +3436,7 @@ impl OpenCADStudio { cursor_type: settings::CursorType::Crosshair, crosshair_color: None, crosshair_color_input: String::new(), + snap_angle_input: "0".to_string(), lineweight_display_scale: 100, isometric_drafting: false, iso_plane: settings::IsoPlane::Left, diff --git a/src/app/settings.rs b/src/app/settings.rs index c66b0c1bb..5aded5e11 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -145,6 +145,18 @@ pub(crate) fn snaps_from_osmode(osmode: i32) -> (Vec, bool) { (modes, osmode & OSMODE_SUPPRESS == 0) } +/// Render a drafting angle without a trailing `.0`, so `22.5` but `30`. +/// +/// The polar pop-up formats its presets the same way; both are showing the +/// same kind of number to the same person. +pub fn format_snap_angle(deg: f32) -> String { + if (deg - deg.round()).abs() < 1e-4 { + format!("{}", deg.round() as i32) + } else { + format!("{deg}") + } +} + /// GRIPOBJLIMIT default: past this many selected objects, no grips are drawn. pub const DEFAULT_GRIP_OBJECT_LIMIT: i32 = 100; diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 61921204a..2803bdcf0 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -452,6 +452,9 @@ impl OpenCADStudio { } else { 0.0 }; + // The Options field edits a buffer rather than the value, so it has to + // be reseeded whenever the value is restored from behind it. + self.snap_angle_input = crate::app::settings::format_snap_angle(self.snap_angle_deg); // Ortho + running OSNAP are per-drawing (adopted from the header on // open / tab switch), not app-global, so they are not applied here. self.snapper.otrack_enabled = s.otrack; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 4b83b5732..463db49af 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -6266,6 +6266,27 @@ impl OpenCADStudio { Task::none() } + // Typed into freely; only committed when it parses, so clearing + // the field to retype does not snap the crosshair back to zero. + Message::SnapAngleInputChanged(value) => { + self.snap_angle_input = value; + if let Ok(angle) = self.snap_angle_input.trim().parse::() { + if angle.is_finite() { + self.snap_angle_deg = angle.rem_euclid(360.0); + self.persist_settings_if_changed(); + } + } + Task::none() + } + + Message::PolarIncrementChanged(deg) => { + if deg.is_finite() && deg > 0.0 { + self.polar_increment_deg = deg; + self.persist_settings_if_changed(); + } + Task::none() + } + Message::PickDragRectToggled(rectangle) => { self.pick_drag_rect = rectangle; self.persist_settings_if_changed(); diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 3f650f643..207f28454 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -204,7 +204,9 @@ impl OpenCADStudio { dimension_continue_mode: self.dimension_continue_mode, qdim_snap_priority: self.quick_dimension_snap_priority, annotation_auto_scale: self.annotation_auto_scale, + polar_increment_deg: self.polar_increment_deg, }, + &self.snap_angle_input, self.double_click_block_refedit, self.double_click_block_attedit, self.cursor_type, diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 14bbb2de6..b597d1715 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -27,6 +27,7 @@ pub enum OptionsTab { General, OpenAndSave, Display, + Drafting, Selection, UserPreferences, Drawing, @@ -62,6 +63,8 @@ pub struct AppPrefs { /// ANNOAUTOSCALE, -4..=4. The sign is on/off; the magnitude selects which /// objects a newly added scale reaches. pub annotation_auto_scale: i8, + /// Polar tracking increment in degrees. + pub polar_increment_deg: f32, } /// The Selection-card settings that live on `UserSettings` rather than on the @@ -105,6 +108,7 @@ pub fn view_window<'a>( cursor_size: i32, selection: SelectionPrefs, prefs: AppPrefs, + snap_angle_input: &'a str, double_click_block_refedit: bool, double_click_block_attedit: bool, cursor_type: CursorType, @@ -1150,11 +1154,83 @@ pub fn view_window<'a>( .spacing(0) .width(sizing.width); + + // Snap modes, grid and object snap stay in the Drafting Settings dialog. + // These two have no home: Drafting Settings shows the rotation angle but + // offers only Reset, and the polar increment lives solely in a status-bar + // pop-up. + let polar_options = [90.0f32, 45.0, 30.0, 22.5, 18.0, 15.0, 10.0, 5.0, 1.0] + .into_iter() + .map(|value| Labelled { + label: format!("{}°", crate::app::settings::format_snap_angle(value)), + value, + }) + .collect::>(); + let selected_polar = polar_options + .iter() + .find(|choice| (choice.value - prefs.polar_increment_deg).abs() < 1e-4) + .cloned() + .unwrap_or_else(|| Labelled { + value: prefs.polar_increment_deg, + label: format!( + "{}°", + crate::app::settings::format_snap_angle(prefs.polar_increment_deg) + ), + }); + + let drafting = column![ + text(crate::t!("Drafting")).size(15), + Space::new().height(10), + row![ + text(crate::t!("Drafting rotation")).size(12).width(150), + text_input("0", snap_angle_input) + .on_input(Message::SnapAngleInputChanged) + .width(110), + text(crate::t!("degrees")).size(11), + ] + .spacing(10) + .align_y(iced::Center), + Space::new().height(6), + text(crate::t!( + "Rotates the crosshair and the snap grid in the active UCS (SNAPANG)." + )) + .size(11) + .width(sizing.width), + Space::new().height(14), + row![ + text(crate::t!("Polar tracking increment")).size(12).width(150), + iced::widget::pick_list(Some(selected_polar), polar_options, |choice| { + choice.label.clone() + }) + .on_select(|choice| Message::PolarIncrementChanged(choice.value)) + .width(Fill), + ] + .spacing(10) + .align_y(iced::Center), + Space::new().height(24), + text(crate::t!("Snap and Grid")).size(15), + Space::new().height(10), + row![ + text(crate::t!("Snap modes, grid spacing and object snap")) + .size(12) + .width(Fill), + button(text(crate::t!("Drafting Settings…")).size(11)) + .on_press(Message::ToggleSnapPopup) + .padding([4, 10]) + .style(button::secondary), + ] + .spacing(10) + .align_y(iced::Center), + ] + .spacing(0) + .width(sizing.width); + let content: Element<'a, Message> = match active_tab { OptionsTab::General => general.into(), OptionsTab::Display => display_element.into(), OptionsTab::Selection => selection.into(), OptionsTab::OpenAndSave => open_and_save.into(), + OptionsTab::Drafting => drafting.into(), OptionsTab::UserPreferences => user_prefs.into(), OptionsTab::Drawing => drawing.into(), }; @@ -1174,6 +1250,7 @@ pub fn view_window<'a>( tab_button(crate::t!("General"), OptionsTab::General), tab_button(crate::t!("Open and Save"), OptionsTab::OpenAndSave), tab_button(crate::t!("Display"), OptionsTab::Display), + tab_button(crate::t!("Drafting"), OptionsTab::Drafting), tab_button(crate::t!("Selection"), OptionsTab::Selection), tab_button(crate::t!("User Preferences"), OptionsTab::UserPreferences), tab_button(crate::t!("Drawing"), OptionsTab::Drawing),