fix(viewport): apply per-viewport layer freeze to fills, images and meshes

A content viewport that froze layers still drew those layers' hatches,
2-D solids, images, OLE frames and 3-D solids — only its wires honoured
`Viewport.frozen_layers`. A viewport meant to be empty (all content
layers frozen) leaked the whole model instead.

viewport_data_for fed content viewports the full model hatch/wipeout/
image/mesh sets with no per-viewport frozen filter. Thread the viewport's
frozen-layer set through the builders (synced_hatch_models,
exploded_insert_hatch_models, wipeout_models, image_models, mesh_models,
instanced_block_meshes) via Scene::layer_frozen_in — the same
layer-name→handle→membership test the wire path uses — and expose
*_for_viewport accessors that cache the filtered set per order-independent
frozen-set signature (viewports sharing a frozen set share the build; an
empty set returns the shared unfiltered arc, so model tiles and
non-freezing viewports are byte-identical).

Each viewport renders in its own pipeline slot with the hatch upload
gated on geometry_epoch, so a per-viewport filtered set uploads once per
epoch rather than every frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-20 19:01:30 +03:00
1 parent fd33810057
commit b884e33f38
4 files changed
+222 -28

No files matched your search

+25 -3
View File
@@ -568,7 +568,10 @@ impl Scene {
self.bump_geometry();
}
pub(super) fn synced_hatch_models(&self) -> Vec<HatchModel> {
pub(super) fn synced_hatch_models(
&self,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
) -> Vec<HatchModel> {
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<Handle>>,
) -> Vec<HatchModel> {
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<HatchModel> {
pub(crate) fn wipeout_models(
&self,
frozen: Option<&rustc_hash::FxHashSet<Handle>>,
) -> Vec<HatchModel> {
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).
+173 -21
View File
@@ -890,6 +890,17 @@ pub struct Scene {
image_cache: RefCell<Option<(u64, Arc<Vec<ImageModel>>)>>,
/// Cached mesh models, keyed by geometry_epoch.
mesh_cache: RefCell<Option<(u64, Arc<Vec<MeshLodSet>>)>>,
/// 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<HashMap<u64, (u64, u64, Arc<Vec<HatchModel>>)>>,
frozen_wipeout_cache: RefCell<HashMap<u64, (u64, Arc<Vec<HatchModel>>)>>,
frozen_image_cache: RefCell<HashMap<u64, (u64, Arc<Vec<ImageModel>>)>>,
frozen_mesh_cache: RefCell<HashMap<u64, (u64, Arc<Vec<MeshLodSet>>)>>,
/// 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<Handle>>) -> Vec<ImageModel> {
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<Handle>>) -> Vec<MeshLodSet> {
// 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<MeshLodSet> = 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<Handle>>) -> 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<Handle>) -> 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<Handle>) -> Arc<Vec<HatchModel>> {
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<Handle>) -> Arc<Vec<HatchModel>> {
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<Handle>) -> Arc<Vec<ImageModel>> {
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<Handle>) -> Arc<Vec<MeshLodSet>> {
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<MeshLodSet> {
fn instanced_block_meshes(
&self,
layout_block: Handle,
frozen: Option<&HashSet<Handle>>,
) -> Vec<MeshLodSet> {
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).
+1 -1
View File
@@ -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)
}
+23 -3
View File
@@ -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<Handle> = 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<HatchModel> = 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