feat(sysvars): persist Ortho, running OSNAP and Isolate in the drawing
Ortho and the running object-snap set were app-global (settings.txt only) and Isolate/Hide was a session-only view state, so none survived save/reopen and none were per-drawing. Bind all three to the file like AutoCAD: - Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header .object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings (osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the per-drawing store, live state mirrors the active drawing: adopt header->live on open and tab-switch, stamp live->header on save and when leaving a tab, a fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now also update the live state. settings.txt stays as the boot/new-drawing default; the per-file header overrides it on open. - Isolate/Hide now set each entity's persisted invisible flag (DXF code 60, which acadrust round-trips in DXF and DWG) in addition to the session set; End Isolation clears it. The set is reseeded from invisible entities on load and after undo/redo, the commands are undoable and mark the tab dirty. The renderer already skipped invisible entities, so no render change was needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7cba5c8679
commit
da2c600953
7 changed files with 220 additions and 3 deletions
|
|
@ -444,7 +444,9 @@ impl OpenCADStudio {
|
||||||
.push_error("ISOLATEOBJECTS: select the objects to isolate first.");
|
.push_error("ISOLATEOBJECTS: select the objects to isolate first.");
|
||||||
} else {
|
} else {
|
||||||
let n = self.tabs[i].scene.selected.len();
|
let n = self.tabs[i].scene.selected.len();
|
||||||
|
self.push_undo_snapshot(i, "ISOLATEOBJECTS");
|
||||||
self.tabs[i].scene.isolate_selected();
|
self.tabs[i].scene.isolate_selected();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_info(&format!(
|
self.command_line.push_info(&format!(
|
||||||
"Isolated {n} object(s). UNISOLATEOBJECTS to restore."
|
"Isolated {n} object(s). UNISOLATEOBJECTS to restore."
|
||||||
));
|
));
|
||||||
|
|
@ -458,7 +460,9 @@ impl OpenCADStudio {
|
||||||
.push_error("HIDEOBJECTS: select the objects to hide first.");
|
.push_error("HIDEOBJECTS: select the objects to hide first.");
|
||||||
} else {
|
} else {
|
||||||
let n = self.tabs[i].scene.selected.len();
|
let n = self.tabs[i].scene.selected.len();
|
||||||
|
self.push_undo_snapshot(i, "HIDEOBJECTS");
|
||||||
self.tabs[i].scene.hide_selected();
|
self.tabs[i].scene.hide_selected();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(&format!("Hid {n} object(s). UNISOLATEOBJECTS to restore."));
|
.push_info(&format!("Hid {n} object(s). UNISOLATEOBJECTS to restore."));
|
||||||
}
|
}
|
||||||
|
|
@ -467,7 +471,9 @@ impl OpenCADStudio {
|
||||||
// UNISOLATEOBJECTS — bring back everything hidden by Isolate / Hide
|
// UNISOLATEOBJECTS — bring back everything hidden by Isolate / Hide
|
||||||
"UNISOLATEOBJECTS" => {
|
"UNISOLATEOBJECTS" => {
|
||||||
if self.tabs[i].scene.is_isolation_active() {
|
if self.tabs[i].scene.is_isolation_active() {
|
||||||
|
self.push_undo_snapshot(i, "UNISOLATEOBJECTS");
|
||||||
self.tabs[i].scene.end_isolation();
|
self.tabs[i].scene.end_isolation();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info("Isolation ended — all objects shown.");
|
.push_info("Isolation ended — all objects shown.");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1250,6 +1250,22 @@ impl OpenCADStudio {
|
||||||
if name == "TEXTFILL" {
|
if name == "TEXTFILL" {
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_geometry();
|
||||||
}
|
}
|
||||||
|
// ORTHOMODE / OSMODE set the header directly; mirror them into
|
||||||
|
// the live Ortho / running OSNAP so the constraint + status
|
||||||
|
// bar follow and the save-time stamp doesn't revert them.
|
||||||
|
if name == "ORTHOMODE" {
|
||||||
|
self.ortho_mode = self.tabs[i].scene.document.header.ortho_mode;
|
||||||
|
if self.ortho_mode {
|
||||||
|
self.polar_mode = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name == "OSMODE" {
|
||||||
|
let (modes, en) = crate::app::settings::snaps_from_osmode(
|
||||||
|
self.tabs[i].scene.document.header.object_snap_mode,
|
||||||
|
);
|
||||||
|
self.snapper.enabled = modes.into_iter().collect();
|
||||||
|
self.snapper.snap_enabled = en;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].scene.populate_hatches_from_document();
|
self.tabs[i].scene.populate_hatches_from_document();
|
||||||
self.tabs[i].scene.populate_images_from_document();
|
self.tabs[i].scene.populate_images_from_document();
|
||||||
self.tabs[i].scene.populate_meshes_from_document();
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
|
// Keep the Isolate/Hide set in step with the restored per-entity
|
||||||
|
// visibility so End Isolation stays correct after undo/redo.
|
||||||
|
self.tabs[i].scene.sync_hidden_from_invisible();
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.tabs[i].scene.images.clear();
|
self.tabs[i].scene.images.clear();
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,61 @@ fn snap_from_id(s: &str) -> Option<SnapType> {
|
||||||
SNAP_ORDER.iter().copied().find(|t| snap_id(*t) == s)
|
SNAP_ORDER.iter().copied().find(|t| snap_id(*t) == s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `$OSMODE` bit for each running object-snap mode (standard AutoCAD bitmask).
|
||||||
|
/// `None` for OCS-only snaps (Grid, ObjectPick) that have no standard bit.
|
||||||
|
fn snap_bit(s: SnapType) -> Option<i32> {
|
||||||
|
Some(match s {
|
||||||
|
SnapType::Endpoint => 1,
|
||||||
|
SnapType::Midpoint => 2,
|
||||||
|
SnapType::Center => 4,
|
||||||
|
SnapType::Node => 8,
|
||||||
|
SnapType::Quadrant => 16,
|
||||||
|
SnapType::Intersection => 32,
|
||||||
|
SnapType::Insertion => 64,
|
||||||
|
SnapType::Perpendicular => 128,
|
||||||
|
SnapType::Tangent => 256,
|
||||||
|
SnapType::Nearest => 512,
|
||||||
|
SnapType::ApparentIntersection => 2048,
|
||||||
|
SnapType::Extension => 4096,
|
||||||
|
SnapType::Parallel => 8192,
|
||||||
|
SnapType::Grid | SnapType::ObjectPick => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bit 14 of `$OSMODE` — object snap turned off (master suppress).
|
||||||
|
const OSMODE_SUPPRESS: i32 = 16384;
|
||||||
|
|
||||||
|
/// Encode the running-snap set + master toggle into an `$OSMODE` bitmask for the
|
||||||
|
/// drawing header. OCS-only snaps (Grid, ObjectPick) have no bit and are
|
||||||
|
/// dropped. A cleared master toggle sets the suppress bit.
|
||||||
|
pub(crate) fn osmode_from_snaps<'a>(
|
||||||
|
enabled: impl IntoIterator<Item = &'a SnapType>,
|
||||||
|
snap_enabled: bool,
|
||||||
|
) -> i32 {
|
||||||
|
let mut bits = 0;
|
||||||
|
for t in enabled {
|
||||||
|
if let Some(b) = snap_bit(*t) {
|
||||||
|
bits |= b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !snap_enabled {
|
||||||
|
bits |= OSMODE_SUPPRESS;
|
||||||
|
}
|
||||||
|
bits
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode an `$OSMODE` bitmask into `(running-snap set, master enabled)`. Only
|
||||||
|
/// the standard mappable modes are produced; the suppress bit maps (inverted) to
|
||||||
|
/// the master toggle.
|
||||||
|
pub(crate) fn snaps_from_osmode(osmode: i32) -> (Vec<SnapType>, bool) {
|
||||||
|
let modes = SNAP_ORDER
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|t| snap_bit(*t).is_some_and(|b| osmode & b != 0))
|
||||||
|
.collect();
|
||||||
|
(modes, osmode & OSMODE_SUPPRESS == 0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a persisted `r,g,b` background triplet (each 0–255). Returns `None`
|
/// Parse a persisted `r,g,b` background triplet (each 0–255). Returns `None`
|
||||||
/// for an empty or malformed value so a missing/garbage key falls back to the
|
/// for an empty or malformed value so a missing/garbage key falls back to the
|
||||||
/// app default rather than a wrong colour.
|
/// app default rather than a wrong colour.
|
||||||
|
|
@ -286,3 +341,45 @@ fn config_path() -> Option<PathBuf> {
|
||||||
p.push("OpenCADStudio");
|
p.push("OpenCADStudio");
|
||||||
Some(p.join("settings.txt"))
|
Some(p.join("settings.txt"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osmode_encodes_bits_and_suppress() {
|
||||||
|
// Endpoint(1) + Midpoint(2) + Intersection(32) = 35, master on.
|
||||||
|
let on = [SnapType::Endpoint, SnapType::Midpoint, SnapType::Intersection];
|
||||||
|
assert_eq!(osmode_from_snaps(on.iter(), true), 35);
|
||||||
|
// Master off sets the suppress bit (16384).
|
||||||
|
assert_eq!(osmode_from_snaps(on.iter(), false), 35 | 16384);
|
||||||
|
// OCS-only snaps carry no bit and are dropped.
|
||||||
|
assert_eq!(osmode_from_snaps([SnapType::Grid, SnapType::ObjectPick].iter(), true), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osmode_decodes_bits_and_suppress() {
|
||||||
|
let (modes, enabled) = snaps_from_osmode(35);
|
||||||
|
let set: std::collections::HashSet<_> = modes.into_iter().collect();
|
||||||
|
assert!(enabled);
|
||||||
|
assert_eq!(set.len(), 3);
|
||||||
|
assert!(set.contains(&SnapType::Endpoint));
|
||||||
|
assert!(set.contains(&SnapType::Midpoint));
|
||||||
|
assert!(set.contains(&SnapType::Intersection));
|
||||||
|
// Suppress bit → master off; the mode bits still decode.
|
||||||
|
let (_m, en) = snaps_from_osmode(35 | 16384);
|
||||||
|
assert!(!en);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn osmode_round_trips_every_mappable_mode() {
|
||||||
|
// All 13 running snaps + master on encode and decode back to the same set.
|
||||||
|
let all: Vec<SnapType> = SNAP_ORDER.to_vec();
|
||||||
|
let bits = osmode_from_snaps(all.iter(), true);
|
||||||
|
let (back, enabled) = snaps_from_osmode(bits);
|
||||||
|
assert!(enabled);
|
||||||
|
let a: std::collections::HashSet<_> = all.into_iter().collect();
|
||||||
|
let b: std::collections::HashSet<_> = back.into_iter().collect();
|
||||||
|
assert_eq!(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,45 @@ impl OpenCADStudio {
|
||||||
self.rebuild_ribbon_modules();
|
self.rebuild_ribbon_modules();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adopt the per-drawing sysvars stored in tab `i`'s document header —
|
||||||
|
/// Ortho (`$ORTHOMODE`) and the running object-snap set (`$OSMODE`) — into
|
||||||
|
/// the live app state, so each drawing keeps its own. Called when a drawing
|
||||||
|
/// becomes active (open, tab switch). The app-settings values seed the boot
|
||||||
|
/// default; the per-file header overrides it here.
|
||||||
|
pub(in crate::app) fn adopt_header_sysvars(&mut self, i: usize) {
|
||||||
|
if self.tabs[i].is_start {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (ortho, osmode) = {
|
||||||
|
let h = &self.tabs[i].scene.document.header;
|
||||||
|
(h.ortho_mode, h.object_snap_mode)
|
||||||
|
};
|
||||||
|
self.ortho_mode = ortho;
|
||||||
|
// Ortho and Polar are mutually exclusive (ToggleOrtho clears Polar).
|
||||||
|
if ortho {
|
||||||
|
self.polar_mode = false;
|
||||||
|
}
|
||||||
|
let (modes, snap_enabled) = crate::app::settings::snaps_from_osmode(osmode);
|
||||||
|
self.snapper.enabled = modes.into_iter().collect();
|
||||||
|
self.snapper.snap_enabled = snap_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stamp the live per-drawing sysvars (Ortho, running OSNAP) onto tab `i`'s
|
||||||
|
/// document header just before it is written to disk (or before switching
|
||||||
|
/// away from it), so they persist with the drawing.
|
||||||
|
pub(in crate::app) fn stamp_header_sysvars(&mut self, i: usize) {
|
||||||
|
if self.tabs[i].is_start {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let osmode = crate::app::settings::osmode_from_snaps(
|
||||||
|
self.snapper.enabled.iter(),
|
||||||
|
self.snapper.snap_enabled,
|
||||||
|
);
|
||||||
|
let h = &mut self.tabs[i].scene.document.header;
|
||||||
|
h.ortho_mode = self.ortho_mode;
|
||||||
|
h.object_snap_mode = osmode;
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply the persisted default background(s) to tab `idx`. No-op for the
|
/// Apply the persisted default background(s) to tab `idx`. No-op for the
|
||||||
/// start tab or when no default is set. Refreshes the tab's cached wires
|
/// start tab or when no default is set. Refreshes the tab's cached wires
|
||||||
/// and meshes so background-adaptive colours pick up the change.
|
/// and meshes so background-adaptive colours pick up the change.
|
||||||
|
|
@ -311,6 +350,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
self.tabs[i].scene.document = doc;
|
self.tabs[i].scene.document = doc;
|
||||||
// Follow the file's saved current UCS from the moment it opens.
|
// Follow the file's saved current UCS from the moment it opens.
|
||||||
self.tabs[i].adopt_active_ucs_from_header();
|
self.tabs[i].adopt_active_ucs_from_header();
|
||||||
|
// Adopt the drawing's own Ortho ($ORTHOMODE) and running OSNAP
|
||||||
|
// ($OSMODE) so each file keeps its state, like AutoCAD.
|
||||||
|
self.adopt_header_sysvars(i);
|
||||||
// Route shared CJK ideographs to the language matching this
|
// Route shared CJK ideographs to the language matching this
|
||||||
// drawing's code page (web per-language font split). Drop the
|
// drawing's code page (web per-language font split). Drop the
|
||||||
// glyph cache if it changed so Han re-resolves to the new
|
// glyph cache if it changed so Han re-resolves to the new
|
||||||
|
|
@ -406,6 +448,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
self.tabs[i].scene.selected = rustc_hash::FxHashSet::default();
|
self.tabs[i].scene.selected = rustc_hash::FxHashSet::default();
|
||||||
self.tabs[i].scene.preview_wires = vec![];
|
self.tabs[i].scene.preview_wires = vec![];
|
||||||
self.tabs[i].scene.current_layout = "Model".to_string();
|
self.tabs[i].scene.current_layout = "Model".to_string();
|
||||||
|
// Rebuild the Isolate/Hide set from the entities the file itself
|
||||||
|
// marks invisible (DXF code 60), so hidden objects stay hidden on
|
||||||
|
// reopen and End Isolation can bring them back.
|
||||||
|
self.tabs[i].scene.sync_hidden_from_invisible();
|
||||||
crate::io::linetypes::populate_document(&mut self.tabs[i].scene.document);
|
crate::io::linetypes::populate_document(&mut self.tabs[i].scene.document);
|
||||||
self.tabs[i].properties = PropertiesPanel::empty();
|
self.tabs[i].properties = PropertiesPanel::empty();
|
||||||
// Seed the current table / multileader style from the file's
|
// Seed the current table / multileader style from the file's
|
||||||
|
|
@ -594,6 +640,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
// reflects them even if they came from settings with no
|
// reflects them even if they came from settings with no
|
||||||
// in-session toggle (#121).
|
// in-session toggle (#121).
|
||||||
self.sync_vport_display(i);
|
self.sync_vport_display(i);
|
||||||
|
// Persist Ortho ($ORTHOMODE) + running OSNAP ($OSMODE) into the
|
||||||
|
// drawing header so they survive save/reopen (per-drawing).
|
||||||
|
self.stamp_header_sysvars(i);
|
||||||
// Native: save straight to the known path. Web has no path
|
// Native: save straight to the known path. Web has no path
|
||||||
// (downloads instead), so always go through the Save dialog.
|
// (downloads instead), so always go through the Save dialog.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -662,6 +711,8 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
let close = self.close_save_dialog_window();
|
let close = self.close_save_dialog_window();
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
||||||
|
// Persist Ortho / running OSNAP into the header (Save-As path).
|
||||||
|
self.stamp_header_sysvars(i);
|
||||||
self.sync_truck_solids_to_acis(i);
|
self.sync_truck_solids_to_acis(i);
|
||||||
|
|
||||||
// Native: write to the chosen path. Web: download the bytes
|
// Native: write to the chosen path. Web: download the bytes
|
||||||
|
|
|
||||||
|
|
@ -664,11 +664,17 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
// ── Document tabs ─────────────────────────────────────────────
|
// ── Document tabs ─────────────────────────────────────────────
|
||||||
Message::TabNew => {
|
Message::TabNew => {
|
||||||
|
// Preserve the outgoing drawing's Ortho / running OSNAP.
|
||||||
|
self.stamp_header_sysvars(self.active_tab);
|
||||||
self.tab_counter += 1;
|
self.tab_counter += 1;
|
||||||
let new_tab = super::document::DocumentTab::new_drawing(self.tab_counter);
|
let new_tab = super::document::DocumentTab::new_drawing(self.tab_counter);
|
||||||
self.tabs.push(new_tab);
|
self.tabs.push(new_tab);
|
||||||
self.active_tab = self.tabs.len() - 1;
|
self.active_tab = self.tabs.len() - 1;
|
||||||
let idx = self.active_tab;
|
let idx = self.active_tab;
|
||||||
|
// A fresh drawing inherits the app's current Ortho / OSNAP so
|
||||||
|
// creating one doesn't silently reset them (its header default is
|
||||||
|
// 0 = no snaps); saving then persists them into the file.
|
||||||
|
self.stamp_header_sysvars(idx);
|
||||||
self.apply_bg_default(idx);
|
self.apply_bg_default(idx);
|
||||||
self.sync_ribbon_layers();
|
self.sync_ribbon_layers();
|
||||||
self.sync_ribbon_styles();
|
self.sync_ribbon_styles();
|
||||||
|
|
@ -687,6 +693,10 @@ impl OpenCADStudio {
|
||||||
// The attribute editor is tab-scoped; leaving its tab
|
// The attribute editor is tab-scoped; leaving its tab
|
||||||
// drops it (its handle is that document's, not this one's).
|
// drops it (its handle is that document's, not this one's).
|
||||||
self.cancel_attr_editor();
|
self.cancel_attr_editor();
|
||||||
|
// Persist the outgoing drawing's Ortho / running OSNAP
|
||||||
|
// before leaving it, so switching back restores them.
|
||||||
|
let prev = self.active_tab;
|
||||||
|
self.stamp_header_sysvars(prev);
|
||||||
}
|
}
|
||||||
self.active_tab = idx;
|
self.active_tab = idx;
|
||||||
self.sync_ribbon_layers();
|
self.sync_ribbon_layers();
|
||||||
|
|
@ -698,6 +708,8 @@ impl OpenCADStudio {
|
||||||
self.sync_ribbon_from_selection();
|
self.sync_ribbon_from_selection();
|
||||||
// Grid/snap follow the newly active drawing's viewport.
|
// Grid/snap follow the newly active drawing's viewport.
|
||||||
self.adopt_view_display(idx);
|
self.adopt_view_display(idx);
|
||||||
|
// Ortho / running OSNAP follow the newly active drawing.
|
||||||
|
self.adopt_header_sysvars(idx);
|
||||||
// Shared CJK ideographs follow the newly active drawing's
|
// Shared CJK ideographs follow the newly active drawing's
|
||||||
// language; re-tessellate if it differs from the last. (#141)
|
// language; re-tessellate if it differs from the last. (#141)
|
||||||
if crate::scene::text::web_font::set_cjk_lang_from_codepage(
|
if crate::scene::text::web_font::set_cjk_lang_from_codepage(
|
||||||
|
|
|
||||||
|
|
@ -1951,12 +1951,16 @@ impl Scene {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let keep = self.selected.clone();
|
let keep = self.selected.clone();
|
||||||
self.hidden = self
|
let hide: Vec<Handle> = self
|
||||||
.document
|
.document
|
||||||
.entities()
|
.entities()
|
||||||
.map(|e| e.common().handle)
|
.map(|e| e.common().handle)
|
||||||
.filter(|h| !h.is_null() && !keep.contains(h))
|
.filter(|h| !h.is_null() && !keep.contains(h))
|
||||||
.collect();
|
.collect();
|
||||||
|
// Persist the hidden state on each entity (DXF code 60) so it survives
|
||||||
|
// save/reopen — the renderer already skips `invisible` entities.
|
||||||
|
self.set_invisible(&hide, true);
|
||||||
|
self.hidden = hide.into_iter().collect();
|
||||||
self.selected.clear();
|
self.selected.clear();
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
}
|
}
|
||||||
|
|
@ -1966,22 +1970,50 @@ impl Scene {
|
||||||
if self.selected.is_empty() {
|
if self.selected.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for h in self.selected.iter().copied() {
|
let sel: Vec<Handle> = self.selected.iter().copied().collect();
|
||||||
|
for h in sel.iter().copied() {
|
||||||
self.hidden.insert(h);
|
self.hidden.insert(h);
|
||||||
}
|
}
|
||||||
|
self.set_invisible(&sel, true);
|
||||||
self.selected.clear();
|
self.selected.clear();
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear isolation — bring every hidden entity back (End Isolation).
|
/// Clear isolation — bring every hidden entity back (End Isolation),
|
||||||
|
/// clearing the persisted invisible flag too so the reveal is saved.
|
||||||
pub fn end_isolation(&mut self) {
|
pub fn end_isolation(&mut self) {
|
||||||
if self.hidden.is_empty() {
|
if self.hidden.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let restore: Vec<Handle> = self.hidden.iter().copied().collect();
|
||||||
|
self.set_invisible(&restore, false);
|
||||||
self.hidden.clear();
|
self.hidden.clear();
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the persisted visibility flag (DXF code 60) on each handle.
|
||||||
|
fn set_invisible(&mut self, handles: &[Handle], invisible: bool) {
|
||||||
|
for &h in handles {
|
||||||
|
if let Some(e) = self.document.get_entity_mut(h) {
|
||||||
|
e.common_mut().invisible = invisible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuild the Isolate/Hide set (`hidden`) from the entities the document
|
||||||
|
/// currently marks invisible (DXF code 60). Call after loading a file or
|
||||||
|
/// restoring an undo/redo snapshot so the session set matches the persisted
|
||||||
|
/// per-entity visibility (and End Isolation stays available).
|
||||||
|
pub fn sync_hidden_from_invisible(&mut self) {
|
||||||
|
self.hidden = self
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter(|e| e.common().invisible)
|
||||||
|
.map(|e| e.common().handle)
|
||||||
|
.filter(|h| !h.is_null())
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
/// True if any currently selected entity is a Viewport.
|
/// True if any currently selected entity is a Viewport.
|
||||||
/// Used to enable the scale picker when a viewport is selected in paper space.
|
/// Used to enable the scale picker when a viewport is selected in paper space.
|
||||||
pub fn has_selected_viewport(&self) -> bool {
|
pub fn has_selected_viewport(&self) -> bool {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue