diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 6741cbb84..1b0431fb0 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -568,7 +568,10 @@ impl Scene { self.bump_geometry(); } - pub(super) fn synced_hatch_models(&self) -> Vec { + pub(super) fn synced_hatch_models( + &self, + frozen: Option<&rustc_hash::FxHashSet>, + ) -> Vec { let layout_block = self.current_layout_block_handle(); let layer_hidden = |layer: &str| { @@ -606,6 +609,11 @@ impl Scene { if c.invisible || layer_hidden(&c.layer) { return false; } + // Per-viewport layer freeze: a content viewport that freezes + // this layer hides its fills too, not just its wires. + if self.layer_frozen_in(&c.layer, frozen) { + return false; + } // Reject block-defn-only hatches (entities owned by a // BLOCK record that's neither model nor a paper layout // block) — they're tessellated separately via Insert @@ -686,7 +694,7 @@ impl Scene { // layout block and materialize its fills at world position. Shared with // the export path so a plot draws them identically. (tint_selected = // true applies the screen selection highlight.) - models.extend(self.exploded_insert_hatch_models(layout_block, hatch_bg, true)); + models.extend(self.exploded_insert_hatch_models(layout_block, hatch_bg, true, frozen)); // Wide LwPolyline / Polyline2D bands are no longer hatch fills at // model level: a flat band is drawn by expanding its centre-line wire @@ -717,6 +725,7 @@ impl Scene { layout_block: Handle, hatch_bg: [f32; 4], tint_selected: bool, + frozen: Option<&rustc_hash::FxHashSet>, ) -> Vec { let layer_hidden = |layer: &str| { self.document @@ -748,6 +757,11 @@ impl Scene { if ins.common.invisible || layer_hidden(&ins.common.layer) { continue; } + // Per-viewport freeze: an INSERT on a layer frozen in this content + // viewport hides the whole instance, its internal fills included. + if self.layer_frozen_in(&ins.common.layer, frozen) { + continue; + } if !self.belongs_to_visible_block( ins.common.handle, ins.common.owner_handle, @@ -880,7 +894,10 @@ impl Scene { /// Wipeout fill models — rendered in a separate pass AFTER wires so that /// wipeouts correctly mask everything below them in the draw order. - pub(crate) fn wipeout_models(&self) -> Vec { + pub(crate) fn wipeout_models( + &self, + frozen: Option<&rustc_hash::FxHashSet>, + ) -> Vec { let is_paper = self.current_layout != "Model"; let bg_color: [f32; 4] = if is_paper { self.paper_bg_color @@ -910,6 +927,11 @@ impl Scene { { continue; } + // Per-viewport freeze: a wipeout on a layer frozen in this content + // viewport is hidden along with the rest of that layer. + if self.layer_frozen_in(&entity.common().layer, frozen) { + continue; + } // Per-entity world_offset selection so paper-layout content // viewports still see model-block wipeouts at the right local // coordinates (same rationale as hatches). diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 8127d4fe5..caf931654 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -890,6 +890,17 @@ pub struct Scene { image_cache: RefCell>)>>, /// Cached mesh models, keyed by geometry_epoch. mesh_cache: RefCell>)>>, + /// Per-viewport (VP-frozen-layer) filtered fill / image / mesh sets, keyed by + /// the order-independent signature of the viewport's frozen-layer set. A + /// content viewport that freezes layers must hide their hatches, 2-D solids, + /// images/OLE and 3-D solids too — not just wires — so these hold the + /// filtered variants. Viewports sharing a frozen set share one entry (like + /// the resident wire set). Empty for a viewport with no frozen layers (it + /// reuses the unfiltered `*_arc` sets directly). + frozen_hatch_cache: RefCell>)>>, + frozen_wipeout_cache: RefCell>)>>, + frozen_image_cache: RefCell>)>>, + frozen_mesh_cache: RefCell>)>>, /// Cached block-INSERT hatches for hit-testing, keyed by geometry_epoch. /// Building this explodes every model-space INSERT, so without the cache a /// heavy block-instanced drawing re-explodes thousands of inserts on every @@ -1125,6 +1136,10 @@ impl Scene { wipeout_cache: RefCell::new(None), image_cache: RefCell::new(None), mesh_cache: RefCell::new(None), + frozen_hatch_cache: RefCell::new(HashMap::default()), + frozen_wipeout_cache: RefCell::new(HashMap::default()), + frozen_image_cache: RefCell::new(HashMap::default()), + frozen_mesh_cache: RefCell::new(HashMap::default()), insert_hatch_cache: RefCell::new(None), paper_sheet_cache: RefCell::new(None), paper_projected_cache: RefCell::new(HashMap::default()), @@ -3419,7 +3434,7 @@ impl Scene { return arc; } } - let arc = Arc::new(self.synced_hatch_models()); + let arc = Arc::new(self.synced_hatch_models(None)); *self.hatch_cache.borrow_mut() = Some((self.geometry_epoch, sel_sig, Arc::clone(&arc))); arc } @@ -3462,7 +3477,7 @@ impl Scene { return arc; } } - let arc = Arc::new(self.wipeout_models()); + let arc = Arc::new(self.wipeout_models(None)); *self.wipeout_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc))); arc } @@ -3491,21 +3506,33 @@ impl Scene { return arc; } } - let depth_map = self.draw_depth_map(); - let arc = Arc::new( - self.images - .iter() - .map(|(handle, model)| { - let mut m = model.clone(); - m.draw_depth = depth_map.get(&handle.value()).copied().unwrap_or(0.0); - m - }) - .collect(), - ); + let arc = Arc::new(self.image_models(None)); *self.image_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc))); arc } + /// Model image / OLE-frame models, optionally dropping those whose layer is + /// frozen in a content viewport (`frozen`). `None` reproduces the full set. + fn image_models(&self, frozen: Option<&HashSet>) -> Vec { + let depth_map = self.draw_depth_map(); + 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 mut m = model.clone(); + m.draw_depth = depth_map.get(&handle.value()).copied().unwrap_or(0.0); + Some(m) + }) + .collect() + } + /// Images owned by the active paper layout block only. The full-canvas /// sheet viewport uses this so model-block images don't bleed onto the /// paper sheet (mirrors `paper_canvas_hatches`). @@ -3561,12 +3588,29 @@ impl Scene { return arc; } } + let arc = Arc::new(self.mesh_models(None)); + *self.mesh_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc))); + arc + } + + /// Solid-mesh set (top-level + block-instanced), optionally dropping those + /// whose layer is frozen in a content viewport (`frozen`). `None` reproduces + /// the full set. + fn mesh_models(&self, frozen: Option<&HashSet>) -> Vec { // Top-level solids: drop those whose layer is off/frozen or that are - // flagged invisible / isolated-hidden, mirroring the 2D wire path. + // flagged invisible / isolated-hidden, mirroring the 2D wire path, plus + // any whose layer is frozen in the requesting viewport. let mut all: Vec = self .meshes .iter() - .filter(|(&h, _)| self.mesh_entity_visible(h)) + .filter(|(&h, _)| { + self.mesh_entity_visible(h) + && self + .document + .get_entity(h) + .map(|e| !self.layer_frozen_in(&e.common().layer, frozen)) + .unwrap_or(true) + }) .map(|(_, set)| set.clone()) .collect(); // Block-definition solids are instanced per INSERT of the ACTIVE space's @@ -3576,10 +3620,9 @@ impl Scene { all.extend(self.instanced_block_meshes( self.block_edit_block .unwrap_or_else(|| self.model_space_block_handle()), + frozen, )); - let arc = Arc::new(all); - *self.mesh_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc))); - arc + all } /// True when `layer` is turned off or frozen — entities on it never render. @@ -3591,6 +3634,108 @@ impl Scene { .unwrap_or(false) } + /// True when `layer`'s handle is in a content viewport's per-viewport + /// frozen-layer set (VP freeze). Mirrors the wire path's test in + /// [`Scene::resident_entity_visible`] so fills / images / meshes hide on the + /// same layers wires do. `None` / empty set → never frozen. + fn layer_frozen_in(&self, layer: &str, frozen: Option<&HashSet>) -> bool { + match frozen { + Some(fz) if !fz.is_empty() => self + .document + .layers + .get(layer) + .map(|l| fz.contains(&l.handle)) + .unwrap_or(false), + _ => false, + } + } + + /// Order-independent signature of a per-viewport frozen-layer set, so + /// viewports that freeze the same layers share one cached filtered fill / + /// image / mesh set. Matches the fold the resident wire cache uses. + fn frozen_layers_sig(frozen: &HashSet) -> u64 { + let mut sig: u64 = frozen.len() as u64; + for h in frozen.iter() { + sig ^= h.value().wrapping_mul(0x9E37_79B9_7F4A_7C15); + } + sig + } + + /// Hatch / 2-D-solid fills for a content viewport, with its frozen layers + /// removed. Cached per frozen-set signature (viewports sharing a frozen set + /// share the build). No frozen layers → the shared unfiltered set. + pub(super) fn hatch_models_for_viewport(&self, frozen: &HashSet) -> Arc> { + if frozen.is_empty() { + return self.hatch_models_arc(); + } + let sig = Self::frozen_layers_sig(frozen); + let sel = self.selected_set_sig(); + if let Some((e, s, arc)) = self.frozen_hatch_cache.borrow().get(&sig) { + if *e == self.geometry_epoch && *s == sel { + return Arc::clone(arc); + } + } + let arc = Arc::new(self.synced_hatch_models(Some(frozen))); + self.frozen_hatch_cache + .borrow_mut() + .insert(sig, (self.geometry_epoch, sel, Arc::clone(&arc))); + arc + } + + /// Wipeout fills for a content viewport, with its frozen layers removed. + pub(super) fn wipeout_models_for_viewport(&self, frozen: &HashSet) -> Arc> { + if frozen.is_empty() { + return self.wipeout_models_arc(); + } + let sig = Self::frozen_layers_sig(frozen); + if let Some((e, arc)) = self.frozen_wipeout_cache.borrow().get(&sig) { + if *e == self.geometry_epoch { + return Arc::clone(arc); + } + } + let arc = Arc::new(self.wipeout_models(Some(frozen))); + self.frozen_wipeout_cache + .borrow_mut() + .insert(sig, (self.geometry_epoch, Arc::clone(&arc))); + arc + } + + /// Image / OLE models for a content viewport, with its frozen layers removed. + pub(super) fn images_for_viewport(&self, frozen: &HashSet) -> Arc> { + if frozen.is_empty() { + return self.images_arc(); + } + let sig = Self::frozen_layers_sig(frozen); + if let Some((e, arc)) = self.frozen_image_cache.borrow().get(&sig) { + if *e == self.geometry_epoch { + return Arc::clone(arc); + } + } + let arc = Arc::new(self.image_models(Some(frozen))); + self.frozen_image_cache + .borrow_mut() + .insert(sig, (self.geometry_epoch, Arc::clone(&arc))); + arc + } + + /// Solid meshes for a content viewport, with its frozen layers removed. + pub(super) fn meshes_for_viewport(&self, frozen: &HashSet) -> Arc> { + if frozen.is_empty() { + return self.meshes_arc(); + } + let sig = Self::frozen_layers_sig(frozen); + if let Some((e, arc)) = self.frozen_mesh_cache.borrow().get(&sig) { + if *e == self.geometry_epoch { + return Arc::clone(arc); + } + } + let arc = Arc::new(self.mesh_models(Some(frozen))); + self.frozen_mesh_cache + .borrow_mut() + .insert(sig, (self.geometry_epoch, Arc::clone(&arc))); + arc + } + /// True when `handle`'s entity sits on a locked layer. Locked objects stay /// visible and snappable but cannot be selected or modified — callers in /// the pick / modify paths consult this to skip them. @@ -3628,7 +3773,11 @@ impl Scene { /// One transformed mesh per block-definition solid instance reached from an /// INSERT owned by `layout_block`. Nested INSERTs accumulate their /// transform. Empty when no block solids exist. (#123) - fn instanced_block_meshes(&self, layout_block: Handle) -> Vec { + fn instanced_block_meshes( + &self, + layout_block: Handle, + frozen: Option<&HashSet>, + ) -> Vec { if self.block_meshes.is_empty() { return Vec::new(); } @@ -3639,8 +3788,11 @@ impl Scene { } if let EntityType::Insert(ins) = e { // INSERT on an off/frozen (or invisible) layer hides the whole - // instance, block-internal solids included. - if !self.mesh_entity_visible(ins.common.handle) { + // instance, block-internal solids included — including a layer + // frozen only in the requesting content viewport. + if !self.mesh_entity_visible(ins.common.handle) + || self.layer_frozen_in(&ins.common.layer, frozen) + { continue; } // Colour inheritance sources for block-internal solids (#221). diff --git a/src/scene/paper.rs b/src/scene/paper.rs index 6edbd82e4..6c6e2a39b 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -238,7 +238,7 @@ impl Scene { } else { self.bg_color }; - let exploded = self.exploded_insert_hatch_models(layout_block, hatch_bg, false); + let exploded = self.exploded_insert_hatch_models(layout_block, hatch_bg, false, None); models.extend(exploded); Arc::new(models) } diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 7a359ba02..7d140ec09 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -1289,6 +1289,23 @@ impl Scene { // bleed past the viewport borders whenever model coords overlap the // paper area. Content viewports keep the full set (the model camera + // per-viewport scissor place / clip them correctly). + // Per-viewport frozen-layer set for a paper content viewport. Content + // viewports must hide FILLS / IMAGES / SOLIDS on their frozen layers too, + // not just wires (which `model_wires_for_viewport_arc` already filters). + // Empty for the sheet, model tiles and the implicit-model view — none of + // which carry a per-viewport freeze — so those reuse the shared sets. + let vp_frozen: rustc_hash::FxHashSet = if !inst.paper_sheet + && inst.tile_idx.is_none() + && inst.handle != acadrust::Handle::NULL + { + match self.document.get_entity(inst.handle) { + Some(EntityType::Viewport(vp)) => vp.frozen_layers.iter().cloned().collect(), + _ => rustc_hash::FxHashSet::default(), + } + } else { + rustc_hash::FxHashSet::default() + }; + let (hatches, wipeout_hatches) = if inst.paper_sheet { let mut v: Vec = Vec::new(); if let Some(sheet) = self.paper_sheet_fill() { @@ -1297,12 +1314,15 @@ impl Scene { v.extend(self.paper_canvas_hatches().iter().cloned()); (Arc::new(v), self.paper_canvas_wipeouts()) } else { - (self.hatch_models_arc(), self.wipeout_models_arc()) + ( + self.hatch_models_for_viewport(&vp_frozen), + self.wipeout_models_for_viewport(&vp_frozen), + ) }; let images = if inst.paper_sheet { self.paper_sheet_images() } else { - self.images_arc() + self.images_for_viewport(&vp_frozen) }; // The paper sheet shows the layout's own 2-D content (fills, borders, // annotation) — never the model's 3-D solids. Those are drawn inside @@ -1314,7 +1334,7 @@ impl Scene { let meshes = if inst.paper_sheet { Arc::new(Vec::new()) } else { - self.meshes_arc() + self.meshes_for_viewport(&vp_frozen) }; // SDF text quads (behind OCS_TEXT_SDF). The glyph quads ride on each