diff --git a/src/app/mod.rs b/src/app/mod.rs index d880c9918..6ee56bbe4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2036,6 +2036,28 @@ pub enum Message { SnapAngleInputChanged(String), /// Change the polar tracking increment in degrees. PolarIncrementChanged(f32), + /// Toggle the navigation cube (NAVVCUBE). + ShowViewCubeChanged(bool), + /// Toggle the UCS icon (UCSICON). + ShowUcsIconChanged(bool), + /// Toggle drawing the UCS icon at the origin (UCSICON ORigin). + UcsIconAtOriginChanged(bool), + /// Toggle selection cycling from Options; the status-bar pill toggles the same flag. + SelectionCyclingChanged(bool), + /// Change isolines per surface in the current drawing (ISOLINES). + IsolinesChanged(i16), + /// Toggle silhouette edges in the current drawing (DISPSILH). + DispSilhChanged(bool), + /// Change surface density U in the current drawing (SURFU). + SurfaceUChanged(i16), + /// Change surface density V in the current drawing (SURFV). + SurfaceVChanged(i16), + /// Change the surface type in the current drawing (SURFTYPE). + SurfaceTypeChanged(i16), + /// Toggle recording composite-solid history in the current drawing (SOLIDHIST). + SolidHistChanged(bool), + /// Change when solid history is shown in the current drawing (SHOWHIST). + ShowHistChanged(i16), /// Restore Model Space display/canvas appearance to defaults. RestoreModelSpaceDisplayDefaults, /// Restore Selection visual effect settings to defaults. diff --git a/src/app/properties.rs b/src/app/properties.rs index 8fd1faec1..bcbbd5aa3 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -4149,6 +4149,29 @@ mod grip_limit_tests { ); } + /// The ViewCube, the UCS icon and selection cycling all have commands and + /// status-bar pills, but their values lived only on the app struct: turn + /// the ViewCube off, restart, and it came straight back. Nothing failed — + /// the setting simply evaporated — which is exactly the kind of thing a + /// round-trip test catches and a person does not. + #[test] + fn the_view_toggles_survive_a_save_and_load() { + let mut app = OpenCADStudio::new_for_test(); + app.show_viewcube = false; + app.show_ucs_icon = false; + app.ucs_icon_at_origin = false; + app.selection_cycling = true; + + let saved = app.current_settings(); + let mut restored = OpenCADStudio::new_for_test(); + restored.apply_settings(&saved); + + assert!(!restored.show_viewcube, "the ViewCube must stay off"); + assert!(!restored.show_ucs_icon); + assert!(!restored.ucs_icon_at_origin); + assert!(restored.selection_cycling); + } + /// The Options card writes these four straight onto the app and relies on /// the snapshot/restore pair to carry them across a restart. Three of them /// had no UI until now and so no reason for anyone to notice if the pair diff --git a/src/app/settings.rs b/src/app/settings.rs index 5aded5e11..3032b0a60 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -184,6 +184,15 @@ pub struct UserSettings { pub grip_object_limit: i32, /// Which Options page was showing when the dialog was last closed. pub options_tab: crate::ui::window::options::OptionsTab, + /// Show the navigation cube (NAVVCUBE). + pub show_viewcube: bool, + /// Show the UCS icon (UCSICON). + pub show_ucs_icon: bool, + /// Draw the UCS icon at the origin rather than in the corner + /// (UCSICON ORigin / NOorigin). + pub ucs_icon_at_origin: bool, + /// Selection cycling: a click where objects overlap opens a picker. + pub selection_cycling: bool, /// CURSORTYPE: crosshair or the platform pointer over the drawing. pub cursor_type: CursorType, /// Explicit crosshair RGB. `None` keeps automatic background contrast. @@ -340,6 +349,10 @@ impl Default for UserSettings { cursor_size: 5, pick_box: 3, options_tab: crate::ui::window::options::OptionsTab::General, + show_viewcube: true, + show_ucs_icon: true, + ucs_icon_at_origin: true, + selection_cycling: false, double_click_block_refedit: false, double_click_block_attedit: true, grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 2803bdcf0..86694f6b2 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -355,6 +355,24 @@ impl OpenCADStudio { } } + /// Write a tessellation-affecting header variable on the active drawing. + /// + /// These decide how solids and surfaces are tessellated, so the geometry + /// has to be rebuilt for the change to appear — the same reason `LTSCALE` + /// bumps it. Marking the tab modified is what makes the new value reach + /// the file. + pub(in crate::app) fn set_drawing_tessellation_var( + &mut self, + write: impl FnOnce(&mut acadrust::document::HeaderVariables), + ) { + let i = self.active_tab; + if let Some(tab) = self.tabs.get_mut(i) { + write(&mut tab.scene.document.header); + tab.scene.bump_geometry(); + tab.dirty = true; + } + } + /// Rebuild text geometry in every open drawing. /// /// `TEXTFILL` is a process-global, so a change to it invalidates the text @@ -377,6 +395,10 @@ impl OpenCADStudio { cursor_size: self.cursor_size, pick_box: self.pick_box, options_tab: self.options_tab, + show_viewcube: self.show_viewcube, + show_ucs_icon: self.show_ucs_icon, + ucs_icon_at_origin: self.ucs_icon_at_origin, + selection_cycling: self.selection_cycling, double_click_block_refedit: self.double_click_block_refedit, double_click_block_attedit: self.double_click_block_attedit, grip_object_limit: self.grip_object_limit, @@ -435,6 +457,13 @@ impl OpenCADStudio { self.cursor_size = s.cursor_size.clamp(1, 100); self.pick_box = s.pick_box.clamp(0, 50); self.options_tab = s.options_tab; + // These four drive real features with commands and status-bar pills, + // but they lived only on the app struct: turning the ViewCube off and + // restarting brought it straight back. + self.show_viewcube = s.show_viewcube; + self.show_ucs_icon = s.show_ucs_icon; + self.ucs_icon_at_origin = s.ucs_icon_at_origin; + self.selection_cycling = s.selection_cycling; self.double_click_block_refedit = s.double_click_block_refedit; self.double_click_block_attedit = s.double_click_block_attedit; self.grip_object_limit = s.grip_object_limit.clamp(0, 32767); diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 463db49af..3e88cb454 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -6287,6 +6287,82 @@ impl OpenCADStudio { Task::none() } + Message::ShowViewCubeChanged(show) => { + self.show_viewcube = show; + self.persist_settings_if_changed(); + Task::none() + } + + Message::ShowUcsIconChanged(show) => { + self.show_ucs_icon = show; + self.persist_settings_if_changed(); + Task::none() + } + + Message::UcsIconAtOriginChanged(at_origin) => { + self.ucs_icon_at_origin = at_origin; + self.persist_settings_if_changed(); + Task::none() + } + + // The five below are drawing variables: they go into the header of + // the active drawing and mark it modified, exactly as SETVAR does. + // The four that feed solid tessellation also bump the geometry, or + // the change is invisible until an unrelated edit rebuilds it. + Message::IsolinesChanged(value) => { + self.set_drawing_tessellation_var(|header| header.isolines = value.max(0)); + Task::none() + } + + Message::DispSilhChanged(on) => { + self.set_drawing_tessellation_var(|header| header.display_silhouette = on); + Task::none() + } + + Message::SurfaceUChanged(value) => { + self.set_drawing_tessellation_var(|header| { + header.surface_u_density = value.clamp(0, 200) + }); + Task::none() + } + + Message::SurfaceVChanged(value) => { + self.set_drawing_tessellation_var(|header| { + header.surface_v_density = value.clamp(0, 200) + }); + Task::none() + } + + Message::SurfaceTypeChanged(value) => { + self.set_drawing_tessellation_var(|header| header.surface_type = value); + Task::none() + } + + Message::SolidHistChanged(record) => { + let i = self.active_tab; + if let Some(tab) = self.tabs.get_mut(i) { + tab.scene.document.header.record_solid_history = record; + tab.dirty = true; + } + Task::none() + } + + Message::ShowHistChanged(mode) => { + let i = self.active_tab; + if let Some(tab) = self.tabs.get_mut(i) { + tab.scene.document.header.show_solid_history = mode.clamp(0, 2); + tab.scene.bump_geometry(); + tab.dirty = true; + } + Task::none() + } + + Message::SelectionCyclingChanged(on) => { + self.selection_cycling = on; + 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 207f28454..e9b5278f6 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -191,6 +191,7 @@ impl OpenCADStudio { pick_add: self.pick_add, pick_drag_rect: self.pick_drag_rect, grip_object_limit: self.grip_object_limit, + selection_cycling: self.selection_cycling, }, crate::ui::window::options::AppPrefs { savetime_min: self.savetime_min, @@ -205,8 +206,30 @@ impl OpenCADStudio { qdim_snap_priority: self.quick_dimension_snap_priority, annotation_auto_scale: self.annotation_auto_scale, polar_increment_deg: self.polar_increment_deg, + show_viewcube: self.show_viewcube, + show_ucs_icon: self.show_ucs_icon, + ucs_icon_at_origin: self.ucs_icon_at_origin, }, &self.snap_angle_input, + { + let header = self + .tabs + .get(self.active_tab) + .map(|tab| &tab.scene.document.header); + crate::ui::window::options::DrawingPrefs { + available: header.is_some(), + isolines: header.map_or(4, |h| h.isolines), + display_silhouette: header + .is_some_and(|h| h.display_silhouette), + surface_u: header.map_or(6, |h| h.surface_u_density), + surface_v: header.map_or(6, |h| h.surface_v_density), + surface_type: header.map_or(6, |h| h.surface_type), + record_solid_history: header + .is_some_and(|h| h.record_solid_history), + show_solid_history: header + .map_or(1, |h| h.show_solid_history), + } + }, 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 b597d1715..a367f24be 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -28,6 +28,7 @@ pub enum OptionsTab { OpenAndSave, Display, Drafting, + Modeling, Selection, UserPreferences, Drawing, @@ -65,6 +66,35 @@ pub struct AppPrefs { pub annotation_auto_scale: i8, /// Polar tracking increment in degrees. pub polar_increment_deg: f32, + /// NAVVCUBE: show the navigation cube. + pub show_viewcube: bool, + /// UCSICON: show the UCS icon. + pub show_ucs_icon: bool, + /// UCSICON ORigin: draw it at the origin rather than the corner. + pub ucs_icon_at_origin: bool, +} + +/// Values read from the current drawing's header rather than from preferences. +/// +/// These are saved in the DWG, so they follow the drawing rather than the +/// application — the page labels them as such. `available` is false when no +/// drawing is open and the controls are not drawn at all. +#[derive(Clone, Copy)] +pub struct DrawingPrefs { + pub available: bool, + /// ISOLINES. + pub isolines: i16, + /// DISPSILH. + pub display_silhouette: bool, + /// SURFU / SURFV. + pub surface_u: i16, + pub surface_v: i16, + /// SURFTYPE: 5, 6 or 8. + pub surface_type: i16, + /// SOLIDHIST. + pub record_solid_history: bool, + /// SHOWHIST, 0..=2. + pub show_solid_history: i16, } /// The Selection-card settings that live on `UserSettings` rather than on the @@ -83,6 +113,8 @@ pub struct SelectionPrefs { pub pick_drag_rect: bool, /// GRIPOBJLIMIT, 0..=32767; 0 = no limit. pub grip_object_limit: i32, + /// Selection cycling: a click where objects overlap opens a picker. + pub selection_cycling: bool, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -109,6 +141,7 @@ pub fn view_window<'a>( selection: SelectionPrefs, prefs: AppPrefs, snap_angle_input: &'a str, + drawing_prefs: DrawingPrefs, double_click_block_refedit: bool, double_click_block_attedit: bool, cursor_type: CursorType, @@ -783,6 +816,15 @@ pub fn view_window<'a>( ] .spacing(8) .align_y(iced::Center), + Space::new().height(10), + row![ + iced::widget::checkbox(selection.selection_cycling) + .on_toggle(Message::SelectionCyclingChanged) + .size(15), + text(crate::t!("Clicking overlapping objects opens a picker")).size(12), + ] + .spacing(8) + .align_y(iced::Center), Space::new().height(24), row![ text(crate::t!("Visual Effect Settings")).size(15), @@ -1225,12 +1267,210 @@ pub fn view_window<'a>( .spacing(0) .width(sizing.width); + + let surface_type_options = [ + (5i16, "Quadratic B-spline"), + (6, "Cubic B-spline"), + (8, "Bezier"), + ] + .into_iter() + .map(|(value, label)| Labelled { + value, + label: format!("{} ({value})", crate::t!(label)), + }) + .collect::>(); + let selected_surface_type = surface_type_options + .iter() + .find(|choice| choice.value == drawing_prefs.surface_type) + .cloned(); + + let show_history_options = [ + (0i16, "Never"), + (1, "As set per solid"), + (2, "Always"), + ] + .into_iter() + .map(|(value, label)| Labelled { + value, + label: crate::t!(label).into_owned(), + }) + .collect::>(); + let selected_show_history = show_history_options + .iter() + .find(|choice| choice.value == drawing_prefs.show_solid_history.clamp(0, 2)) + .cloned(); + + let mut modeling = column![ + text(crate::t!("3D Modeling")).size(15), + Space::new().height(10), + text(crate::t!("Display Tools")).size(15), + Space::new().height(10), + row![ + iced::widget::checkbox(prefs.show_viewcube) + .on_toggle(Message::ShowViewCubeChanged) + .size(15), + text(crate::t!("Show the navigation cube (NAVVCUBE)")).size(12), + ] + .spacing(8) + .align_y(iced::Center), + Space::new().height(10), + row![ + iced::widget::checkbox(prefs.show_ucs_icon) + .on_toggle(Message::ShowUcsIconChanged) + .size(15), + text(crate::t!("Show the UCS icon (UCSICON)")).size(12), + ] + .spacing(8) + .align_y(iced::Center), + Space::new().height(10), + row![ + iced::widget::checkbox(prefs.ucs_icon_at_origin) + .on_toggle(Message::UcsIconAtOriginChanged) + .size(15), + text(crate::t!("Place the UCS icon at the origin (UCSICON ORigin)")).size(12), + ] + .spacing(8) + .align_y(iced::Center), + ]; + + if drawing_prefs.available { + modeling = modeling + .push(Space::new().height(24)) + .push( + row![ + text(crate::t!("Display Resolution")).size(15), + Space::new().width(10), + text(crate::t!("applies to the current drawing")).size(11), + ] + .align_y(iced::Center), + ) + .push(Space::new().height(10)) + .push( + row![ + text(crate::t!("Isolines per surface")).size(12).width(150), + slider( + 0..=64, + drawing_prefs.isolines.clamp(0, 64), + Message::IsolinesChanged, + ) + .step(1i16) + .width(Fill), + text(drawing_prefs.isolines.clamp(0, 64).to_string()) + .size(11) + .width(44), + ] + .spacing(10) + .align_y(iced::Center), + ) + .push(Space::new().height(10)) + .push( + row![ + iced::widget::checkbox(drawing_prefs.display_silhouette) + .on_toggle(Message::DispSilhChanged) + .size(15), + text(crate::t!("Show silhouette edges on solids (DISPSILH)")).size(12), + ] + .spacing(8) + .align_y(iced::Center), + ) + .push(Space::new().height(10)) + .push( + row![ + text(crate::t!("Surface density U")).size(12).width(150), + slider( + 0..=200, + drawing_prefs.surface_u.clamp(0, 200), + Message::SurfaceUChanged, + ) + .step(1i16) + .width(Fill), + text(drawing_prefs.surface_u.clamp(0, 200).to_string()) + .size(11) + .width(44), + ] + .spacing(10) + .align_y(iced::Center), + ) + .push(Space::new().height(10)) + .push( + row![ + text(crate::t!("Surface density V")).size(12).width(150), + slider( + 0..=200, + drawing_prefs.surface_v.clamp(0, 200), + Message::SurfaceVChanged, + ) + .step(1i16) + .width(Fill), + text(drawing_prefs.surface_v.clamp(0, 200).to_string()) + .size(11) + .width(44), + ] + .spacing(10) + .align_y(iced::Center), + ) + .push(Space::new().height(10)) + .push( + row![ + text(crate::t!("Surface type")).size(12).width(150), + iced::widget::pick_list( + selected_surface_type, + surface_type_options, + |choice| choice.label.clone(), + ) + .on_select(|choice| Message::SurfaceTypeChanged(choice.value)) + .width(Fill), + ] + .spacing(10) + .align_y(iced::Center), + ) + .push(Space::new().height(24)) + .push( + row![ + text(crate::t!("Solid History")).size(15), + Space::new().width(10), + text(crate::t!("applies to the current drawing")).size(11), + ] + .align_y(iced::Center), + ) + .push(Space::new().height(10)) + .push( + row![ + iced::widget::checkbox(drawing_prefs.record_solid_history) + .on_toggle(Message::SolidHistChanged) + .size(15), + text(crate::t!("Record the history of composite solids (SOLIDHIST)")) + .size(12), + ] + .spacing(8) + .align_y(iced::Center), + ) + .push(Space::new().height(10)) + .push( + row![ + text(crate::t!("Show solid history")).size(12).width(150), + iced::widget::pick_list( + selected_show_history, + show_history_options, + |choice| choice.label.clone(), + ) + .on_select(|choice| Message::ShowHistChanged(choice.value)) + .width(Fill), + ] + .spacing(10) + .align_y(iced::Center), + ); + } + + let modeling = modeling.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::Modeling => modeling.into(), OptionsTab::UserPreferences => user_prefs.into(), OptionsTab::Drawing => drawing.into(), }; @@ -1251,6 +1491,7 @@ pub fn view_window<'a>( 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!("3D Modeling"), OptionsTab::Modeling), tab_button(crate::t!("Selection"), OptionsTab::Selection), tab_button(crate::t!("User Preferences"), OptionsTab::UserPreferences), tab_button(crate::t!("Drawing"), OptionsTab::Drawing),