forked from 1b-agent/opencadstudio-upstream
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:
1 parent
fd33810057
commit
b884e33f38
4 files changed
+222
-28
No files matched your search
+25
-3
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user