Make the view toggles survive a restart, and give them a page

The ViewCube, the UCS icon, its at-origin placement and selection cycling all
have commands and status-bar pills, and all four kept their value only on the
app struct. Turn the ViewCube off with NAVVCUBE, restart, and it is back.
Nothing failed — the setting evaporated — so a checkbox for any of them would
have been a checkbox that forgets. They are persisted first, with a
round-trip test, and only then given controls.

The new 3D Modeling page carries the three view tools plus the drawing
variables that decide how solids are tessellated: ISOLINES, DISPSILH, SURFU,
SURFV, SURFTYPE, SOLIDHIST and SHOWHIST. Those are saved in the DWG rather
than in preferences, so the page says so beside each group and hides them
entirely when no drawing is open.

Changing them also rebuilds the geometry. The SETVAR path never did, which is
why the comment beside LTSCALE's invalidation exists — the same reasoning
applies here, and without it a new isoline count does not appear until some
unrelated edit rebuilds the solid.

Selection cycling joins the Selection page next to the other two selection
modes, since that is what it is.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-10 08:41:26 +03:00
1 parent b5f4b4d1cc
commit 5a638f243a
7 files changed
+427

No files matched your search

+22
View File
@@ -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.
+23
View File
@@ -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
+13
View File
@@ -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,
+29
View File
@@ -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);
+76
View File
@@ -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();
+23
View File
@@ -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,
+241
View File
@@ -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::<Vec<_>>();
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::<Vec<_>>();
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),