From d1da26feb6f917d9b29b435647f154146b8b09f1 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Sat, 10 Oct 2026 07:52:16 +0100 Subject: [PATCH] fix(properties): scope asym-scale opt-out per tab props_asym_scale was app-global keyed by raw handle values, but fresh documents restart numbering at seed 1, leaking one tab's unchecked Uniform box into another tab reusing the value. The set moves to DocumentTab; all uses tab-scoped. Also removes a dead destructure left in overlay.rs. Test: asym_scale_opt_out_does_not_leak_across_tabs with handle-reuse premise assert (RED first). Full suite: 2025 passed / 0 failed / 25 ignored, zero warnings. --- src/app/document.rs | 7 +++++++ src/app/mod.rs | 5 ----- src/app/properties.rs | 41 ++++++++++++++++++++++++++++++++++++++++- src/app/update/mod.rs | 6 +++--- src/ui/overlay.rs | 1 - 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/app/document.rs b/src/app/document.rs index f9bef2bbb..209bd502b 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -206,6 +206,12 @@ pub(super) struct DocumentTab { pub(super) dyn_active: usize, pub(super) history: HistoryState, pub(super) active_layer: String, + /// Block references whose properties panel shows per-axis Scale X/Y/Z even + /// though the three factors are currently equal — the user unchecked the + /// "Uniform scale" box for them (#427). Per-tab: fresh documents restart + /// handle numbering, so an app-global set keyed by raw handle leaks one + /// tab's opt-out into another tab reusing the value (#47). + pub(super) props_asym_scale: std::collections::HashSet, /// Node graph shown by the command line's graph button. pub(super) graph: crate::ui::node_graph::Graph, /// Currently active UCS. `None` means WCS (identity transform). @@ -649,6 +655,7 @@ impl DocumentTab { dyn_active: 0, history: HistoryState::default(), active_layer: "0".to_string(), + props_asym_scale: std::collections::HashSet::new(), graph: Default::default(), active_ucs: None, sketch_session: None, diff --git a/src/app/mod.rs b/src/app/mod.rs index cb9aa099d..37a73f74c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -419,10 +419,6 @@ pub(super) struct OpenCADStudio { discussions: Vec, /// True while the boot-time Discussions refresh is still in flight. discussions_loading: bool, - /// Block references whose properties panel shows per-axis Scale X/Y/Z even - /// though the three factors are currently equal — the user unchecked the - /// "Uniform scale" box for them (#427). Keyed by entity handle. - props_asym_scale: std::collections::HashSet, /// Collapsed Properties-panel section titles. This belongs to the app, /// rather than an individual document tab, so the same view preference is /// used by every currently open drawing/project. @@ -4110,7 +4106,6 @@ impl OpenCADStudio { videos_loading: false, discussions: Vec::new(), discussions_loading: false, - props_asym_scale: std::collections::HashSet::new(), collapsed_property_sections: rustc_hash::FxHashSet::default(), start_section: StartSection::default(), start_action_w: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)), diff --git a/src/app/properties.rs b/src/app/properties.rs index 3883c7f50..3237203e0 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -463,7 +463,7 @@ impl OpenCADStudio { if let codec::EntityType::Insert(ins) = entity { let eq = (ins.x_scale() - ins.y_scale()).abs() < 1e-12 && (ins.x_scale() - ins.z_scale()).abs() < 1e-12; - let uniform = eq && !self.props_asym_scale.contains(&handle.value()); + let uniform = eq && !self.tabs[i].props_asym_scale.contains(&handle.value()); for section in sections.iter_mut() { let Some(xi) = section.props.iter().position(|p| p.field == "x_scale") else { @@ -4373,6 +4373,45 @@ fn apply_insert_unit_scale(ins: &mut codec::entities::Insert, ratio: f64) -> boo true } +#[cfg(test)] +mod asym_scale_tests { + // #47: the uniform-scale opt-out must be per-tab. Fresh documents restart + // handle numbering (seed 1), so two tabs hand out identical handles — an + // app-global set leaks one tab's opt-out into the other. + use crate::app::OpenCADStudio; + + fn tab_with_insert(app: &mut OpenCADStudio) -> (usize, codec::Handle) { + app.tab_counter += 1; + let n = app.tab_counter; + app.tabs + .push(crate::app::document::DocumentTab::new_drawing(n)); + let idx = app.tabs.len() - 1; + app.active_tab = idx; + let ins = + codec::entities::Insert::new("B", codec::types::Vector3::ZERO); + let h = app + .commit_entity_handle(codec::EntityType::Insert(ins)) + .expect("insert commits"); + (idx, h) + } + + #[test] + fn asym_scale_opt_out_does_not_leak_across_tabs() { + let mut app = OpenCADStudio::new_for_test(); + let (a, ha) = tab_with_insert(&mut app); + let (b, hb) = tab_with_insert(&mut app); + assert_eq!( + ha, hb, + "premise: identically built fresh docs reuse handle values" + ); + app.tabs[a].props_asym_scale.insert(ha.value()); + assert!( + !app.tabs[b].props_asym_scale.contains(&hb.value()), + "tab B must not see tab A's opt-out" + ); + } +} + #[cfg(test)] mod insert_unit_scale_tests { use super::{apply_insert_unit_scale, format_unit_factor, insert_unit_scale, insunits_to_mm}; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 985df0ebb..f3dcbc669 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -6895,11 +6895,11 @@ impl OpenCADStudio { }; let Some((sx, sy, sz)) = scales else { return }; let eq = (sx - sy).abs() < 1e-12 && (sx - sz).abs() < 1e-12; - let checked = eq && !app.props_asym_scale.contains(&handle.value()); + let checked = eq && !app.tabs[i].props_asym_scale.contains(&handle.value()); if checked { - app.props_asym_scale.insert(handle.value()); + app.tabs[i].props_asym_scale.insert(handle.value()); } else { - app.props_asym_scale.remove(&handle.value()); + app.tabs[i].props_asym_scale.remove(&handle.value()); if let Some(codec::EntityType::Insert(ins)) = app.tabs[i].scene.document.get_entity_mut(handle) { diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index e8cd3f062..0b4804fa7 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -2961,7 +2961,6 @@ fn draw_axes( axes: (Vec3, Vec3, Vec3), bg_luminance: f32, ) { - let (ax, ay, az) = axes; let axis_stroke = |r: f32, g: f32, b: f32| canvas::Stroke { width: 1.5, style: canvas::Style::Solid(Color { r, g, b, a: 0.85 }),