From d5be78a187d7a8a9c9ed35ac0c1e0cb6a79d57b0 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 27 Jul 2026 23:36:02 +0300 Subject: [PATCH] fix(selection): separate transient visibility Keep object isolation out of persisted entity visibility and clear stale grips across block-editor transitions. Refs #512 Refs #517 --- src/app/commands/inquiry.rs | 20 ++++ src/app/commands/layers.rs | 34 +++++-- src/app/document.rs | 36 ++++++- src/app/history.rs | 67 +++++++++++- src/app/update/command.rs | 2 +- src/app/update/file.rs | 8 +- src/app/update/viewport.rs | 6 +- src/scene/entity.rs | 15 ++- src/scene/mod.rs | 198 +++++++++++++++++++++--------------- src/scene/paper.rs | 15 ++- 10 files changed, 295 insertions(+), 106 deletions(-) diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index 0902123d..cff87a40 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -444,12 +444,22 @@ impl OpenCADStudio { }); self.tabs[i].scene.deselect_all(); + // The first click of a double-click selects the model-space + // INSERT and populates the cached grip overlay. Clearing only + // Scene::selected leaves that INSERT's grips active inside the + // block editor, where dragging one moves the outer reference + // instead of block-local geometry (#517). + self.tabs[i].active_grip = None; + self.grip_hover = None; + self.grip_popup = None; + self.visibility_popup = None; self.tabs[i].scene.bump_geometry(); // Frame the camera on the block's own geometry (block-local, near // origin) — fit_all() goes through current_layout_block_handle so // it already scopes to the edited block. Without this the view // stays wherever model/paper space was. (#261) self.tabs[i].scene.fit_all(); + self.refresh_properties(); self.tabs[i].active_cmd = None; self.tabs[i].dirty = true; self.command_line.push_info(&format!( @@ -470,11 +480,16 @@ impl OpenCADStudio { // Edits are live on the block record — just leave the block space. self.tabs[i].scene.block_edit_block = None; self.tabs[i].scene.deselect_all(); + self.tabs[i].active_grip = None; + self.grip_hover = None; + self.grip_popup = None; + self.visibility_popup = None; self.tabs[i].scene.set_current_layout(session.return_layout.clone()); // Return the view to where it was when BEDIT began (#425). *self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone(); self.tabs[i].scene.camera_generation += 1; self.tabs[i].scene.rebuild_derived_caches(); + self.refresh_properties(); self.tabs[i].dirty = true; self.command_line.push_output(&format!( "BEDIT: Block \"{}\" saved. All references updated.", @@ -521,11 +536,16 @@ impl OpenCADStudio { } self.tabs[i].scene.block_edit_block = None; self.tabs[i].scene.deselect_all(); + self.tabs[i].active_grip = None; + self.grip_hover = None; + self.grip_popup = None; + self.visibility_popup = None; self.tabs[i].scene.set_current_layout(session.return_layout.clone()); // Return the view to where it was when BEDIT began (#425). *self.tabs[i].scene.camera.borrow_mut() = session.return_camera.clone(); self.tabs[i].scene.camera_generation += 1; self.tabs[i].scene.rebuild_derived_caches(); + self.refresh_properties(); self.tabs[i].dirty = true; self.command_line.push_output(&format!( "BEDIT: Block \"{}\" edit discarded.", diff --git a/src/app/commands/layers.rs b/src/app/commands/layers.rs index 185e18e5..d17746a0 100644 --- a/src/app/commands/layers.rs +++ b/src/app/commands/layers.rs @@ -491,9 +491,16 @@ impl OpenCADStudio { .push_error("ISOLATEOBJECTS: select the objects to isolate first."); } else { let n = self.tabs[i].scene.selected.len(); - self.push_undo_snapshot(i, "ISOLATEOBJECTS"); + let before = self.tabs[i].scene.object_isolation.clone(); + let selected_before = + self.tabs[i].scene.selected.iter().copied().collect(); self.tabs[i].scene.isolate_selected(); - self.tabs[i].dirty = true; + self.push_object_visibility_history( + i, + "ISOLATEOBJECTS", + before, + selected_before, + ); self.command_line.push_info(&format!( "Isolated {n} object(s). UNISOLATEOBJECTS to restore." )); @@ -507,9 +514,17 @@ impl OpenCADStudio { .push_error("HIDEOBJECTS: select the objects to hide first."); } else { let n = self.tabs[i].scene.selected.len(); - self.push_undo_snapshot(i, "HIDEOBJECTS"); + let before = self.tabs[i].scene.object_isolation.clone(); + let selected_before = + self.tabs[i].scene.selected.iter().copied().collect(); self.tabs[i].scene.hide_selected(); - self.tabs[i].dirty = true; + self.push_object_visibility_history( + i, + "HIDEOBJECTS", + before, + selected_before, + ); + self.refresh_properties(); self.command_line .push_info(&format!("Hid {n} object(s). UNISOLATEOBJECTS to restore.")); } @@ -518,9 +533,16 @@ impl OpenCADStudio { // UNISOLATEOBJECTS — bring back everything hidden by Isolate / Hide "UNISOLATEOBJECTS" => { if self.tabs[i].scene.is_isolation_active() { - self.push_undo_snapshot(i, "UNISOLATEOBJECTS"); + let before = self.tabs[i].scene.object_isolation.clone(); + let selected_before = + self.tabs[i].scene.selected.iter().copied().collect(); self.tabs[i].scene.end_isolation(); - self.tabs[i].dirty = true; + self.push_object_visibility_history( + i, + "UNISOLATEOBJECTS", + before, + selected_before, + ); self.command_line .push_info("Isolation ended — all objects shown."); } else { diff --git a/src/app/document.rs b/src/app/document.rs index 2c2997c1..62a90921 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -4,7 +4,7 @@ use crate::modules::draw::modify::block_edit::BlockEditSession; use crate::modules::draw::modify::refedit::RefEditSession; use crate::scene::pick::grip::GripEdit; use crate::scene::GripDef; -use crate::scene::Scene; +use crate::scene::{ObjectIsolationState, Scene}; use crate::snap::SnapResult; use crate::ui::{LayerPanel, PropertiesPanel}; use acadrust::tables::Ucs; @@ -518,12 +518,14 @@ impl DocumentTab { #[derive(Clone)] pub(super) enum HistorySnapshot { Delta(DeltaSnapshot), + ObjectVisibility(ObjectVisibilitySnapshot), } impl HistorySnapshot { pub(super) fn label(&self) -> &str { match self { HistorySnapshot::Delta(d) => &d.label, + HistorySnapshot::ObjectVisibility(v) => &v.label, } } @@ -544,10 +546,42 @@ impl HistorySnapshot { .saturating_add(d.selected_before.len().saturating_mul(16)) .saturating_add(d.selected_after.len().saturating_mul(16)) .saturating_add(d.label.len()), + HistorySnapshot::ObjectVisibility(v) => v + .before + .hidden + .len() + .saturating_add( + v.before + .keep + .as_ref() + .map_or(0, rustc_hash::FxHashSet::len), + ) + .saturating_add(v.after.hidden.len()) + .saturating_add( + v.after + .keep + .as_ref() + .map_or(0, rustc_hash::FxHashSet::len), + ) + .saturating_add(v.selected_before.len()) + .saturating_add(v.selected_after.len()) + .saturating_mul(16) + .saturating_add(v.label.len()), } } } +/// Symmetric undo/redo image for session-only object visibility. It contains +/// no document entity data and therefore can never make isolation serializable. +#[derive(Clone)] +pub(super) struct ObjectVisibilitySnapshot { + pub(super) before: ObjectIsolationState, + pub(super) after: ObjectIsolationState, + pub(super) selected_before: Vec, + pub(super) selected_after: Vec, + pub(super) label: String, +} + /// A transactional undo entry: for each touched handle, its before-image and /// after-image (`None` = the entity was absent on that side, i.e. an add or an /// erase). Symmetric — undo applies the before side, redo the after side — so diff --git a/src/app/history.rs b/src/app/history.rs index 3f22f09b..b2e687a3 100644 --- a/src/app/history.rs +++ b/src/app/history.rs @@ -1,10 +1,11 @@ use super::{ document::{ - DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, PendingHistorySnapshot, - StructureSnapshot, TableEntryDelta, + DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, ObjectVisibilitySnapshot, + PendingHistorySnapshot, StructureSnapshot, TableEntryDelta, }, OpenCADStudio, }; +use crate::scene::ObjectIsolationState; use acadrust::{EntityType, Handle}; use rustc_hash::{FxHashMap, FxHashSet as HashSet}; use std::sync::Arc; @@ -157,6 +158,32 @@ impl OpenCADStudio { self.trim_history(i); } + pub(super) fn push_object_visibility_history( + &mut self, + i: usize, + label: impl Into, + before: ObjectIsolationState, + selected_before: Vec, + ) { + self.finish_pending_history(i); + let after = self.tabs[i].scene.object_isolation.clone(); + let selected_after: Vec = + self.tabs[i].scene.selected.iter().copied().collect(); + if before == after && selected_before == selected_after { + return; + } + self.push_undo_entry( + i, + HistorySnapshot::ObjectVisibility(ObjectVisibilitySnapshot { + before, + after, + selected_before, + selected_after, + label: label.into(), + }), + ); + } + pub(super) fn push_single_entity_history( &mut self, i: usize, @@ -870,6 +897,27 @@ impl OpenCADStudio { changes } + fn apply_object_visibility_state( + &mut self, + i: usize, + snapshot: &ObjectVisibilitySnapshot, + undo: bool, + ) { + let (state, selected) = if undo { + (&snapshot.before, &snapshot.selected_before) + } else { + (&snapshot.after, &snapshot.selected_after) + }; + let scene = &mut self.tabs[i].scene; + scene.object_isolation = state.clone(); + scene.selected = selected + .iter() + .copied() + .filter(|handle| scene.document.get_entity(*handle).is_some()) + .collect(); + scene.bump_geometry_no_blocks(); + } + /// Perform the one expensive cache/UI synchronization required after a /// batch of history steps. Intermediate states are never rendered, so /// rebuilding them only multiplies latency. @@ -888,7 +936,6 @@ impl OpenCADStudio { // for the final state. Unlike the old path, decoded images stay // cached instead of being immediately cleared. scene.rebuild_derived_caches(); - scene.sync_hidden_from_invisible(); } else if !changes.is_empty() { use crate::scene::ChangeKind::{Added, Modified, Removed}; let mut net: FxHashMap = FxHashMap::default(); @@ -974,6 +1021,13 @@ impl OpenCADStudio { .redo_stack .push(HistorySnapshot::Delta(d)); } + HistorySnapshot::ObjectVisibility(v) => { + self.apply_object_visibility_state(i, &v, true); + self.tabs[i] + .history + .redo_stack + .push(HistorySnapshot::ObjectVisibility(v)); + } } } self.finish_history_apply(i, had_full, structure_changed, &changes); @@ -1010,6 +1064,13 @@ impl OpenCADStudio { .undo_stack .push(HistorySnapshot::Delta(d)); } + HistorySnapshot::ObjectVisibility(v) => { + self.apply_object_visibility_state(i, &v, false); + self.tabs[i] + .history + .undo_stack + .push(HistorySnapshot::ObjectVisibility(v)); + } } } self.finish_history_apply(i, had_full, structure_changed, &changes); diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 25ff6133..176214e1 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -549,7 +549,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { *e = orig; } } - self.tabs[i].scene.hidden.remove(&h); + self.tabs[i].scene.preview_hidden.remove(&h); self.tabs[i].scene.clear_preview_wire(); // Geometry restored to the backup — re-tessellate just it. self.tabs[i] diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 722e613f..29102e90 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -821,10 +821,10 @@ pub(super) fn on_open_file(&mut self) -> Task { .unwrap_or_else(|| "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(); + // Object isolation is session-only. A newly opened drawing must + // not inherit the previous tab's filter, and persisted entity + // visibility remains independent (not an isolation session). + self.tabs[i].scene.reset_transient_visibility(); crate::io::linetypes::populate_document(&mut self.tabs[i].scene.document); self.tabs[i].properties = PropertiesPanel::empty(); // Seed the current table / multileader style from the file's diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index e1c61a49..102d0faa 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -742,11 +742,11 @@ impl OpenCADStudio { // the whole model on every move. if self.grip_preview_handle != Some(grip.handle) { if let Some(prev) = self.grip_preview_handle.take() { - self.tabs[i].scene.hidden.remove(&prev); + self.tabs[i].scene.preview_hidden.remove(&prev); } // Back up the original geometry so Esc can cancel the drag. self.grip_original = self.tabs[i].scene.document.get_entity(grip.handle).cloned(); - self.tabs[i].scene.hidden.insert(grip.handle); + self.tabs[i].scene.preview_hidden.insert(grip.handle); // Hiding changes exactly one resident run. Publishing a full // delta here made the first grip move rebuild every wire. self.tabs[i] @@ -2016,7 +2016,7 @@ impl OpenCADStudio { } self.grip_text_verts = Vec::new(); self.grip_text_slide = false; - self.tabs[i].scene.hidden.remove(&h); + self.tabs[i].scene.preview_hidden.remove(&h); self.tabs[i].scene.clear_preview_wire(); // Only the dragged entity changed — re-tessellate just it. self.tabs[i] diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 2d33a827..56d5dfd3 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -812,7 +812,10 @@ impl Scene { return true; }; let c = entity.common(); - if c.invisible || layer_hidden(&c.layer) { + if c.invisible + || self.entity_temporarily_hidden(handle) + || layer_hidden(&c.layer) + { return false; } // Per-viewport layer freeze: a content viewport that freezes @@ -1059,7 +1062,10 @@ impl Scene { let EntityType::Insert(ins) = contextual.as_ref() else { continue; }; - if ins.common.invisible || layer_hidden(&ins.common.layer) { + if ins.common.invisible + || self.entity_temporarily_hidden(ins.common.handle) + || layer_hidden(&ins.common.layer) + { continue; } // Per-viewport freeze: an INSERT on a layer frozen in this content @@ -1291,7 +1297,9 @@ impl Scene { let EntityType::Wipeout(wo) = entity else { continue; }; - if entity.common().invisible { + if entity.common().invisible + || self.entity_temporarily_hidden(wo.common.handle) + { continue; } // Reject block-defn-only wipeouts (owned by a BLOCK record that is @@ -1361,6 +1369,7 @@ impl Scene { }; let c = &ins.common; if c.invisible + || self.entity_temporarily_hidden(c.handle) || self .document .layers diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 06780fb1..1b01c1a2 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -153,6 +153,29 @@ use std::rc::Rc; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; +/// Session-only object visibility used by ISOLATEOBJECTS / HIDEOBJECTS. +/// +/// This is deliberately separate from `EntityCommon::invisible`: that DXF +/// property belongs to the drawing (and dynamic-block visibility states), +/// whereas object isolation must never be serialized. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ObjectIsolationState { + /// Objects explicitly hidden with HIDEOBJECTS. + pub hidden: HashSet, + /// Objects retained by ISOLATEOBJECTS. `None` means no isolate filter. + pub keep: Option>, +} + +impl ObjectIsolationState { + fn hides(&self, handle: Handle) -> bool { + self.hidden.contains(&handle) + } + + fn is_active(&self) -> bool { + !self.hidden.is_empty() || self.keep.is_some() + } +} + /// Global counter so every Scene and every geometry mutation gets a /// process-wide unique epoch. This prevents two different tabs (Scenes) /// from ever sharing the same epoch value, which would cause the shared @@ -1359,10 +1382,13 @@ pub struct Scene { lighting_cache: RefCell>>, /// Currently selected entity handles. pub selected: HashSet, - /// Entity handles hidden by Isolate / Hide. Empty = nothing hidden. - /// `tessellate_block`'s visibility test skips these, so they neither - /// render nor hit-test until isolation ends. - pub hidden: HashSet, + /// Session-only ISOLATEOBJECTS / HIDEOBJECTS state. Never written to DWG/DXF. + pub object_isolation: ObjectIsolationState, + /// Entity handles temporarily removed from the base render while an + /// interactive preview (currently grip drag) draws their live replacement. + /// Separate from object isolation so a grip can never activate the + /// isolation status or be captured in its undo state. + pub preview_hidden: HashSet, /// During in-place block edit (REFEDIT), the handles of the entities being /// edited. Everything else is rendered faded toward the background so the /// edited geometry stands out while the surrounding drawing stays visible @@ -1751,7 +1777,8 @@ impl Scene { object_data_cache: crate::entities::object_data::ObjectDataCache::default(), lighting_cache: RefCell::new(None), selected: HashSet::default(), - hidden: HashSet::default(), + object_isolation: ObjectIsolationState::default(), + preview_hidden: HashSet::default(), refedit_keep: None, hover_highlight: None, transparency_display: true, @@ -3475,9 +3502,15 @@ impl Scene { content.len() } - /// True when any entities are hidden by Isolate / Hide. + /// True when ISOLATEOBJECTS or HIDEOBJECTS has an active session filter. pub fn is_isolation_active(&self) -> bool { - !self.hidden.is_empty() + self.object_isolation.is_active() + } + + /// True when a top-level entity must be omitted for a session-only object + /// visibility command or an interactive replacement preview. + fn entity_temporarily_hidden(&self, handle: Handle) -> bool { + self.object_isolation.hides(handle) || self.preview_hidden.contains(&handle) } /// Set (or clear) the previewed entity that renders with the selection @@ -3499,80 +3532,63 @@ impl Scene { } } - /// Hide every drawable entity except the current selection (Isolate). + /// Keep the current selection visible and temporarily filter every other + /// top-level object. Block-definition children are intentionally not added + /// here: an isolated INSERT remains a complete visible block instance. pub fn isolate_selected(&mut self) { if self.selected.is_empty() { return; } let keep = self.selected.clone(); + let active_block = self.interaction_block_handle(); let hide: Vec = self .document .entities() - .map(|e| e.common().handle) - .filter(|h| !h.is_null() && !keep.contains(h)) + .filter(|entity| { + let common = entity.common(); + !common.handle.is_null() + && !keep.contains(&common.handle) + && self.belongs_to_visible_block( + common.handle, + common.owner_handle, + active_block, + ) + }) + .map(|entity| entity.common().handle) .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.bump_geometry(); + self.object_isolation.hidden.extend(hide); + self.object_isolation.keep = Some(keep); + self.bump_geometry_no_blocks(); } - /// Hide the current selection (Hide Objects). + /// Temporarily hide the current selection without changing any entity + /// property in the document. pub fn hide_selected(&mut self) { if self.selected.is_empty() { return; } - let sel: Vec = self.selected.iter().copied().collect(); - for h in sel.iter().copied() { - self.hidden.insert(h); - } - self.set_invisible(&sel, true); + self.object_isolation + .hidden + .extend(self.selected.iter().copied()); self.selected.clear(); - // Only the hidden entities changed visibility — report just those so the - // resident set drops them instead of re-tessellating the whole drawing. - let changes: Vec<(Handle, ChangeKind)> = - sel.iter().map(|&h| (h, ChangeKind::Modified)).collect(); - self.bump_entities(&changes); + self.bump_geometry_no_blocks(); } - /// Clear isolation — bring every hidden entity back (End Isolation), - /// clearing the persisted invisible flag too so the reveal is saved. + /// Clear every session-only object visibility filter. pub fn end_isolation(&mut self) { - if self.hidden.is_empty() { + if !self.object_isolation.is_active() { return; } - let restore: Vec = self.hidden.iter().copied().collect(); - self.set_invisible(&restore, false); - self.hidden.clear(); - // Re-reveal just the previously hidden entities (bounded to that set). - let changes: Vec<(Handle, ChangeKind)> = - restore.iter().map(|&h| (h, ChangeKind::Modified)).collect(); - self.bump_entities(&changes); + self.object_isolation = ObjectIsolationState::default(); + self.bump_geometry_no_blocks(); } - /// 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(); + /// A newly opened/replaced document starts with no session visibility. + /// Persisted `EntityCommon::invisible` values stay untouched and continue + /// to serve file/dynamic-block visibility. + pub fn reset_transient_visibility(&mut self) { + self.object_isolation = ObjectIsolationState::default(); + self.preview_hidden.clear(); } /// True if any currently selected entity is a Viewport. @@ -4624,16 +4640,14 @@ impl Scene { self.images .iter() .filter_map(|(handle, model)| { - if frozen.is_some() { - let layer = self - .document - .get_entity(*handle) - .map(|e| e.common().layer.clone()); - if let Some(layer) = layer { - if self.layer_frozen_in(&layer, frozen) { - return None; - } - } + let entity = self.document.get_entity(*handle)?; + let common = entity.common(); + if common.invisible + || self.entity_temporarily_hidden(*handle) + || self.layer_hidden(&common.layer) + || self.layer_frozen_in(&common.layer, frozen) + { + return None; } let mut m = model.clone(); m.draw_depth = depth_map.get(&handle.value()).map_or(0.0, |d| d[0]); @@ -4655,6 +4669,8 @@ impl Scene { let entity = self.document.get_entity(handle)?; let c = entity.common(); if c.invisible + || self.entity_temporarily_hidden(handle) + || self.layer_hidden(&c.layer) || !self.belongs_to_visible_block(handle, c.owner_handle, layout_block) { return None; @@ -5012,20 +5028,29 @@ impl Scene { locked.then_some(name) } - /// Visibility test for a solid mesh entity, mirroring the 2D wire path: - /// honour the invisible flag, the isolate/hide set, and the layer's - /// off/frozen state. - fn mesh_entity_visible(&self, handle: Handle) -> bool { + /// File-backed visibility shared by top-level and block-definition meshes. + /// Object isolation is intentionally absent: a retained INSERT must retain + /// every visible child of its block definition. + fn mesh_definition_entity_visible(&self, handle: Handle) -> bool { let Some(c) = self.document.get_entity(handle).map(|e| e.common()) else { return false; }; if c.invisible { return false; } - if !self.hidden.is_empty() && self.hidden.contains(&handle) { + !self.layer_hidden(&c.layer) + } + + /// Visibility test for a top-level solid mesh entity, mirroring the direct + /// 2D wire path. + fn mesh_entity_visible(&self, handle: Handle) -> bool { + if !self.mesh_definition_entity_visible(handle) { return false; } - !self.layer_hidden(&c.layer) + if self.entity_temporarily_hidden(handle) { + return false; + } + true } fn mesh_visible_for_interaction(&self, handle: Handle) -> bool { @@ -5152,7 +5177,7 @@ impl Scene { let e = contextual.as_ref(); // A block-internal solid / nested INSERT on an off/frozen layer // (or flagged invisible) must not render, same as a top-level one. - if !self.mesh_entity_visible(h) { + if !self.mesh_definition_entity_visible(h) { continue; } if let EntityType::Insert(ins) = e { @@ -5371,7 +5396,7 @@ impl Scene { return false; }; if common.invisible - || (!self.hidden.is_empty() && self.hidden.contains(&handle)) + || self.entity_temporarily_hidden(handle) || self.layer_hidden(&common.layer) || self.interaction_layer_frozen(&common.layer) { @@ -5496,7 +5521,7 @@ impl Scene { continue; }; if ins.common.invisible - || (!self.hidden.is_empty() && self.hidden.contains(&ins.common.handle)) + || self.entity_temporarily_hidden(ins.common.handle) || layer_hidden(&ins.common.layer) { continue; @@ -6638,8 +6663,8 @@ impl Scene { if c.invisible { return false; } - // Isolate / Hide: skip entities the user has hidden. - if !self.hidden.is_empty() && self.hidden.contains(&c.handle) { + // Session-only Isolate / Hide and interactive replacement previews. + if self.entity_temporarily_hidden(c.handle) { return false; } // Block/BlockEnd are block-defn sentinels, not drawable geometry. @@ -7544,7 +7569,10 @@ impl Scene { } // 3D solids render as meshes, not wires, so fold their // XY AABBs in too — otherwise ZOOM EXTENTS ignores them. - for set in self.meshes.values() { + for (&handle, set) in &self.meshes { + if !self.mesh_entity_visible(handle) { + continue; + } let [ax, ay, bx, by] = set.world_aabb; let lo = glam::Vec3::new(ax, ay, 0.0); let hi = glam::Vec3::new(bx, by, 0.0); @@ -7570,7 +7598,10 @@ impl Scene { // wrong location on UTM-scale drawings. for entity in self.document.entities() { let c = entity.common(); - if c.owner_handle != model_block || c.invisible { + if c.owner_handle != model_block + || c.invisible + || self.entity_temporarily_hidden(c.handle) + { continue; } for wire in self.tessellate_one(entity) { @@ -7588,7 +7619,10 @@ impl Scene { } } // Same mesh inclusion for the tessellate fallback path. - for set in self.meshes.values() { + for (&handle, set) in &self.meshes { + if !self.mesh_entity_visible(handle) { + continue; + } let [ax, ay, bx, by] = set.world_aabb; let lo = glam::Vec3::new(ax, ay, 0.0); let hi = glam::Vec3::new(bx, by, 0.0); diff --git a/src/scene/paper.rs b/src/scene/paper.rs index c3accb6c..4b72b5f5 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -161,7 +161,11 @@ impl Scene { let Some(EntityType::Viewport(vp)) = self.document.get_entity(handle) else { continue; }; - if !vp.status.is_on { + if !vp.status.is_on + || vp.common.invisible + || self.entity_temporarily_hidden(handle) + || self.layer_hidden(&vp.common.layer) + { continue; } let h = vp.common.handle; @@ -317,7 +321,10 @@ impl Scene { crate::scene::annotative::entity_for_active_context(&self.document, source); let entity = contextual.as_ref(); let c = entity.common(); - if c.invisible || layer_hidden(&c.layer) { + if c.invisible + || self.entity_temporarily_hidden(handle) + || layer_hidden(&c.layer) + { continue; } if !self.belongs_to_visible_block(handle, c.owner_handle, layout_block) { @@ -382,7 +389,9 @@ impl Scene { let EntityType::Wipeout(wo) = entity else { continue; }; - if wo.common.invisible { + if wo.common.invisible + || self.entity_temporarily_hidden(wo.common.handle) + { continue; } if self