diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 23e62aa6a..c4a1c6216 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -792,10 +792,9 @@ impl Scene { pub(super) fn synced_hatch_models( &self, + target_block: Handle, frozen: Option<&rustc_hash::FxHashSet>, ) -> Vec { - let layout_block = self.current_layout_block_handle(); - let layer_hidden = |layer: &str| { self.document .layers @@ -811,14 +810,9 @@ impl Scene { // culling at draw time, which keeps the GPU upload set stable // across pan/zoom. // - // We INCLUDE hatches from blocks other than `current_layout`'s - // own block (specifically: paper-layout content viewports want - // model-block hatches). Every hatch's `world_origin` is already - // baked into the correct block coord-space at - // `populate_hatches_from_document` time (offset for model, 0 for - // paper), so projecting them through a camera built for the - // wrong block lands them outside the frustum and the per-vp - // scissor / LOD culls them out — no double-rendering. + // Every content viewport supplies the block it renders. Do not depend + // on camera/frustum culling to separate paper and model coordinates: + // overlapping coordinates otherwise make foreign fills visible. let depth_map = self.draw_depth_map(); let mut models: Vec = self .hatches @@ -850,13 +844,7 @@ impl Scene { // BLOCK record that's neither model nor a paper layout // block) — they're tessellated separately via Insert // explosion and only the laid-out copies should appear. - self.belongs_to_visible_block(handle, c.owner_handle, layout_block) - || (self.block_edit_block.is_none() - && self.belongs_to_visible_block( - handle, - c.owner_handle, - self.model_space_block_handle(), - )) + self.belongs_to_visible_block(handle, c.owner_handle, target_block) }) .flat_map(|(&handle, model)| { let contextual = self @@ -980,21 +968,14 @@ impl Scene { } else { self.bg_color }; - // Block-internal hatch fills: explode every visible INSERT of this - // layout block and materialize its fills at world position. Paper - // content viewports also need the model block's INSERT fills; without - // them the screen showed an empty/white block while plotting correctly - // materialized its solid hatch. (tint_selected = true applies the - // screen selection highlight.) - models.extend(self.exploded_insert_hatch_models(layout_block, hatch_bg, true, frozen)); - if self.current_layout != "Model" { - models.extend(self.exploded_insert_hatch_models( - self.model_space_block_handle(), - hatch_bg, - true, - frozen, - )); - } + // Block-internal hatch fills: descend INSERTs owned by the requested + // target only and materialize their fills at world position. + models.extend(self.exploded_insert_hatch_models( + target_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 @@ -1468,6 +1449,7 @@ impl Scene { /// wipeouts correctly mask everything below them in the draw order. pub(crate) fn wipeout_models( &self, + target_block: Handle, frozen: Option<&rustc_hash::FxHashSet>, ) -> Vec { let is_paper = self.current_layout != "Model"; @@ -1483,7 +1465,6 @@ impl Scene { // pipeline's `wipeout_skip_flags` (compute_wipeout_lod) does // the per-frame skip at draw time instead. let depth_map = self.draw_depth_map(); - let layout_block = self.current_layout_block_handle(); let mut models = Vec::new(); for entity in self.document.entities() { let EntityType::Wipeout(wo) = entity else { @@ -1501,7 +1482,7 @@ impl Scene { if !self.belongs_to_visible_block( wo.common.handle, wo.common.owner_handle, - layout_block, + target_block, ) { continue; } @@ -1574,7 +1555,7 @@ impl Scene { { continue; } - if !self.belongs_to_visible_block(c.handle, c.owner_handle, layout_block) { + if !self.belongs_to_visible_block(c.handle, c.owner_handle, target_block) { continue; } if frozen.is_none() diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 6594a0c7f..bf0a99927 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1515,10 +1515,10 @@ pub struct Scene { pub document: CadDocument, /// File-open-prepared index for non-graphical semantic object lookups. pub(crate) object_data_cache: crate::entities::object_data::ObjectDataCache, - /// Native AcDbLight/Sun inputs. Built once after document load, then - /// invalidated only when a light entity changes; ordinary geometry edits - /// never rescan a million-entity drawing just to rediscover its lights. - lighting_cache: RefCell>>, + /// Native AcDbLight/Sun inputs, separated by rendered block and viewport + /// frozen-layer signature. Invalidated only when a light entity changes; + /// ordinary geometry edits never rescan the drawing's lights. + lighting_cache: RefCell>>, /// Currently selected entity handles. pub selected: HashSet, /// Session-only ISOLATEOBJECTS / HIDEOBJECTS state. Never written to DWG/DXF. @@ -1633,21 +1633,22 @@ pub struct Scene { /// constants. `[depth, half]` retains a fixed child sub-range for block /// composition. Full sort/layout/block changes rebuild the labels. draw_depth_cache: RefCell>, - /// Cached hatch fill models, keyed by geometry_epoch. View culling + /// Cached hatch fill models, keyed by target block and geometry_epoch. View culling /// is handled at draw time via `hatch_skip_flags` in the pipeline, /// not at build time — that lets the GPU buffer stay stable across /// pan/zoom while still skipping out-of-view hatches. /// Keyed by `(geometry_epoch, selection_generation)` — selected hatches /// are tinted, so a select/deselect must rebuild even when the geometry /// is unchanged (issue #71). - hatch_cache: RefCell>)>>, - /// Cached wipeout fill models, keyed by geometry_epoch. Same + hatch_cache: RefCell>)>>, + /// Cached wipeout fill models, keyed by target block and geometry_epoch. Same /// reasoning as `hatch_cache`. - wipeout_cache: RefCell>)>>, - /// Cached image models, keyed by geometry_epoch. No camera key needed here. - image_cache: RefCell>)>>, - /// Cached mesh models, keyed by geometry_epoch. - mesh_cache: RefCell>)>>, + wipeout_cache: RefCell>)>>, + /// Cached image models, keyed by target block and geometry_epoch. No camera + /// key needed here. + image_cache: RefCell>)>>, + /// Cached mesh models, keyed by target block and geometry_epoch. + mesh_cache: RefCell>)>>, /// Picking mesh source for a non-model active space, keyed by geometry epoch /// and interaction block. Model/MSPACE reuse `mesh_cache` directly. interaction_mesh_cache: RefCell>)>>, @@ -1669,10 +1670,12 @@ pub struct Scene { /// 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>)>>, + 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 @@ -1915,7 +1918,7 @@ impl Scene { selection: Rc::new(RefCell::new(SelectionState::default())), document: CadDocument::new(), object_data_cache: crate::entities::object_data::ObjectDataCache::default(), - lighting_cache: RefCell::new(None), + lighting_cache: RefCell::new(HashMap::default()), selected: HashSet::default(), object_isolation: ObjectIsolationState::default(), preview_hidden: HashSet::default(), @@ -1941,7 +1944,7 @@ impl Scene { draw_depth_cache: RefCell::new(None), hatch_cache: RefCell::new(HashMap::default()), wipeout_cache: RefCell::new(HashMap::default()), - image_cache: RefCell::new(None), + image_cache: RefCell::new(HashMap::default()), mesh_cache: RefCell::new(HashMap::default()), interaction_mesh_cache: RefCell::new(None), mesh_pick_lookup_cache: RefCell::new(None), @@ -2382,7 +2385,7 @@ impl Scene { .as_deref() .is_some_and(|entity| matches!(entity, EntityType::Light(_))) }) { - *self.lighting_cache.borrow_mut() = None; + self.lighting_cache.borrow_mut().clear(); } let mut changes: Vec<(Handle, ChangeKind)> = Vec::with_capacity(entities.len()); for (h, before, after) in entities { @@ -2426,7 +2429,7 @@ impl Scene { // stale ownership. self.invalidate_dependency_index(); } - let cached_light_changed = self.lighting_cache.borrow().as_ref().is_some_and(|lights| { + let cached_light_changed = self.lighting_cache.borrow().values().any(|lights| { changes .iter() .any(|(handle, _)| lights.iter().any(|light| light.handle == *handle)) @@ -2448,7 +2451,7 @@ impl Scene { exists, ); } - *self.lighting_cache.borrow_mut() = None; + self.lighting_cache.borrow_mut().clear(); } let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed); self.geometry_epoch = epoch; @@ -2515,6 +2518,9 @@ impl Scene { pub fn bump_geometry(&mut self) { let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed); self.geometry_epoch = epoch; + // A full structural change may move lights between blocks or alter + // layer visibility without naming the affected handles. + self.lighting_cache.borrow_mut().clear(); // Default: also invalidate block definitions. Safe for every caller; // operations that know blocks are untouched use `bump_geometry_no_blocks`. self.block_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed); @@ -2545,6 +2551,7 @@ impl Scene { pub fn bump_geometry_no_blocks(&mut self) { let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed); self.geometry_epoch = epoch; + self.lighting_cache.borrow_mut().clear(); self.push_geometry_delta(epoch, Vec::new(), true); } @@ -5098,11 +5105,12 @@ impl Scene { // drawings. Key on a signature of `selected` instead, so hover (which // never changes `selected`) keeps the cache warm. let sel_sig = self.selected_hatch_sig(); - let space = self.current_layout.clone(); + let target_block = self.content_render_block_handle(); + let key = (target_block, self.current_layout.clone()); { let reuse = { let cache = self.hatch_cache.borrow(); - match cache.get(&space) { + match cache.get(&key) { // Selection tint is baked in, so the selected set must also // match; category = a changed direct fill or an INSERT // whose expanded block tree can contain hatch fills. @@ -5126,15 +5134,15 @@ impl Scene { } }; if let Some(arc) = reuse { - if let Some((e, _, _)) = self.hatch_cache.borrow_mut().get_mut(&space) { + if let Some((e, _, _)) = self.hatch_cache.borrow_mut().get_mut(&key) { *e = self.geometry_epoch; } return arc; } } - let arc = Arc::new(self.synced_hatch_models(None)); + let arc = Arc::new(self.synced_hatch_models(target_block, None)); self.hatch_cache.borrow_mut().insert( - space, + key, (self.geometry_epoch, sel_sig, Arc::clone(&arc)), ); arc @@ -5167,11 +5175,12 @@ impl Scene { } pub(super) fn wipeout_models_arc(&self) -> Arc> { - let space = self.current_layout.clone(); + let target_block = self.content_render_block_handle(); + let key = (target_block, self.current_layout.clone()); { let reuse = { let cache = self.wipeout_cache.borrow(); - match cache.get(&space) { + match cache.get(&key) { Some((cached_epoch, arc)) // wipeout_models scans the whole document for Wipeout // entities; relevance = the changed handle is a Wipeout. @@ -5192,27 +5201,28 @@ impl Scene { } }; if let Some(arc) = reuse { - if let Some((e, _)) = self.wipeout_cache.borrow_mut().get_mut(&space) { + if let Some((e, _)) = self.wipeout_cache.borrow_mut().get_mut(&key) { *e = self.geometry_epoch; } return arc; } } - let arc = Arc::new(self.wipeout_models(None)); + let arc = Arc::new(self.wipeout_models(target_block, None)); self.wipeout_cache .borrow_mut() - .insert(space, (self.geometry_epoch, Arc::clone(&arc))); + .insert(key, (self.geometry_epoch, Arc::clone(&arc))); arc } pub(super) fn images_arc(&self) -> Arc> { + let target_block = self.content_render_block_handle(); { let reuse = { let cache = self.image_cache.borrow(); - match *cache { - Some((cached_epoch, ref arc)) + match cache.get(&target_block) { + Some((cached_epoch, arc)) if self.category_cache_valid( - cached_epoch, + *cached_epoch, CACHE_CATEGORY_IMAGE, |h| self.images.contains_key(&h), ) => @@ -5225,20 +5235,26 @@ impl Scene { if let Some(arc) = reuse { // No image changed since — keep it warm, just advance the sync // epoch so the next replay window stays short. - if let Some((ref mut e, _)) = *self.image_cache.borrow_mut() { + if let Some((e, _)) = self.image_cache.borrow_mut().get_mut(&target_block) { *e = self.geometry_epoch; } return arc; } } - let arc = Arc::new(self.image_models(None)); - *self.image_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc))); + let arc = Arc::new(self.image_models(target_block, None)); + self.image_cache + .borrow_mut() + .insert(target_block, (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 { + /// Image / OLE-frame models owned by `target_block`, optionally dropping + /// those whose layer is frozen in a content viewport (`frozen`). + fn image_models( + &self, + target_block: Handle, + frozen: Option<&HashSet>, + ) -> Vec { let depth_map = self.draw_depth_map(); self.images .iter() @@ -5249,6 +5265,11 @@ impl Scene { || self.entity_temporarily_hidden(*handle) || self.layer_hidden(&common.layer) || self.layer_frozen_in(&common.layer, frozen) + || !self.belongs_to_visible_block( + *handle, + common.owner_handle, + target_block, + ) { return None; } @@ -5287,11 +5308,12 @@ impl Scene { } pub(super) fn meshes_arc(&self) -> Arc> { - let space = self.current_layout.clone(); + let target_block = self.content_render_block_handle(); + let key = (target_block, self.current_layout.clone()); { let reuse = { let cache = self.mesh_cache.borrow(); - match cache.get(&space) { + match cache.get(&key) { // Top-level solids seed self.meshes; the instanced_block part // is driven by INSERTs, so an INSERT edit (e.g. a move) must // also invalidate. Block-definition edits route through @@ -5315,19 +5337,28 @@ impl Scene { } }; if let Some(arc) = reuse { - if let Some((e, _)) = self.mesh_cache.borrow_mut().get_mut(&space) { + if let Some((e, _)) = self.mesh_cache.borrow_mut().get_mut(&key) { *e = self.geometry_epoch; } return arc; } } - let arc = Arc::new(self.mesh_models(None)); + let arc = Arc::new(self.mesh_models(target_block, None)); self.mesh_cache .borrow_mut() - .insert(space, (self.geometry_epoch, Arc::clone(&arc))); + .insert(key, (self.geometry_epoch, Arc::clone(&arc))); arc } + /// Block rendered by model-content viewports. Paper layout entities belong + /// only to the sheet viewport; floating paper viewports always show model + /// space. BEDIT is the sole exception and renders the edited block itself. + fn content_render_block_handle(&self) -> Handle { + self.block_edit_block + .filter(|block| !block.is_null()) + .unwrap_or_else(|| self.model_space_block_handle()) + } + fn interaction_block_handle(&self) -> Handle { if self.block_edit_block.is_none() && self.active_viewport.is_some() { self.model_space_block_handle() @@ -5403,19 +5434,7 @@ impl Scene { return Arc::clone(meshes); } } - let mut all: Vec = self - .meshes - .iter() - .filter(|(&handle, _)| { - self.mesh_entity_visible(handle) - && self.document.get_entity(handle).is_some_and(|entity| { - self.belongs_to_visible_block(handle, entity.common().owner_handle, block) - }) - }) - .map(|(_, set)| set.clone()) - .collect(); - all.extend(self.instanced_block_meshes(block, None)); - let meshes = Arc::new(all); + let meshes = Arc::new(self.mesh_models(block, None)); *self.interaction_mesh_cache.borrow_mut() = Some((self.geometry_epoch, key, Arc::clone(&meshes))); meshes @@ -5458,7 +5477,11 @@ impl Scene { /// 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 { + fn mesh_models( + &self, + target_block: Handle, + 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, plus // any whose layer is frozen in the requesting viewport. @@ -5470,8 +5493,15 @@ impl Scene { && self .document .get_entity(h) - .map(|e| !self.layer_frozen_in(&e.common().layer, frozen)) - .unwrap_or(true) + .map(|e| { + !self.layer_frozen_in(&e.common().layer, frozen) + && self.belongs_to_visible_block( + h, + e.common().owner_handle, + target_block, + ) + }) + .unwrap_or(false) }) .map(|(_, set)| set.clone()) .collect(); @@ -5479,13 +5509,7 @@ impl Scene { // block so a block placed at an INSERT scale renders at the right size // (#123) — model space normally, the edited block in a BEDIT editor so // model-space solids don't leak into it (#261). - all.extend( - self.instanced_block_meshes( - self.block_edit_block - .unwrap_or_else(|| self.model_space_block_handle()), - frozen, - ), - ); + all.extend(self.instanced_block_meshes(target_block, frozen)); all } @@ -5535,15 +5559,16 @@ impl Scene { if frozen.is_empty() { return self.hatch_models_arc(); } + let target_block = self.content_render_block_handle(); let sig = Self::frozen_layers_sig(frozen); - let key = (self.current_layout.clone(), sig); + let key = (target_block, self.current_layout.clone(), sig); let sel = self.selected_hatch_sig(); if let Some((e, s, arc)) = self.frozen_hatch_cache.borrow().get(&key) { if *e == self.geometry_epoch && *s == sel { return Arc::clone(arc); } } - let arc = Arc::new(self.synced_hatch_models(Some(frozen))); + let arc = Arc::new(self.synced_hatch_models(target_block, Some(frozen))); self.frozen_hatch_cache .borrow_mut() .insert(key, (self.geometry_epoch, sel, Arc::clone(&arc))); @@ -5558,14 +5583,15 @@ impl Scene { if frozen.is_empty() { return self.wipeout_models_arc(); } + let target_block = self.content_render_block_handle(); let sig = Self::frozen_layers_sig(frozen); - let key = (self.current_layout.clone(), sig); + let key = (target_block, self.current_layout.clone(), sig); if let Some((e, arc)) = self.frozen_wipeout_cache.borrow().get(&key) { if *e == self.geometry_epoch { return Arc::clone(arc); } } - let arc = Arc::new(self.wipeout_models(Some(frozen))); + let arc = Arc::new(self.wipeout_models(target_block, Some(frozen))); self.frozen_wipeout_cache .borrow_mut() .insert(key, (self.geometry_epoch, Arc::clone(&arc))); @@ -5577,16 +5603,18 @@ impl Scene { if frozen.is_empty() { return self.images_arc(); } + let target_block = self.content_render_block_handle(); let sig = Self::frozen_layers_sig(frozen); - if let Some((e, arc)) = self.frozen_image_cache.borrow().get(&sig) { + let key = (target_block, sig); + if let Some((e, arc)) = self.frozen_image_cache.borrow().get(&key) { if *e == self.geometry_epoch { return Arc::clone(arc); } } - let arc = Arc::new(self.image_models(Some(frozen))); + let arc = Arc::new(self.image_models(target_block, Some(frozen))); self.frozen_image_cache .borrow_mut() - .insert(sig, (self.geometry_epoch, Arc::clone(&arc))); + .insert(key, (self.geometry_epoch, Arc::clone(&arc))); arc } @@ -5595,14 +5623,15 @@ impl Scene { if frozen.is_empty() { return self.meshes_arc(); } + let target_block = self.content_render_block_handle(); let sig = Self::frozen_layers_sig(frozen); - let key = (self.current_layout.clone(), sig); + let key = (target_block, self.current_layout.clone(), sig); if let Some((e, arc)) = self.frozen_mesh_cache.borrow().get(&key) { if *e == self.geometry_epoch { return Arc::clone(arc); } } - let arc = Arc::new(self.mesh_models(Some(frozen))); + let arc = Arc::new(self.mesh_models(target_block, Some(frozen))); self.frozen_mesh_cache .borrow_mut() .insert(key, (self.geometry_epoch, Arc::clone(&arc))); @@ -8260,7 +8289,15 @@ impl Scene { // 3D solids render as meshes, not wires, so fold their // XY AABBs in too — otherwise ZOOM EXTENTS ignores them. for (&handle, set) in &self.meshes { - if !self.mesh_entity_visible(handle) { + if !self.mesh_entity_visible(handle) + || !self.document.get_entity(handle).is_some_and(|entity| { + self.belongs_to_visible_block( + handle, + entity.common().owner_handle, + model_block, + ) + }) + { continue; } let [ax, ay, bx, by] = set.world_aabb; @@ -8310,7 +8347,15 @@ impl Scene { } // Same mesh inclusion for the tessellate fallback path. for (&handle, set) in &self.meshes { - if !self.mesh_entity_visible(handle) { + if !self.mesh_entity_visible(handle) + || !self.document.get_entity(handle).is_some_and(|entity| { + self.belongs_to_visible_block( + handle, + entity.common().owner_handle, + model_block, + ) + }) + { continue; } let [ax, ay, bx, by] = set.world_aabb; diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 760087965..b66ca78be 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -1243,7 +1243,11 @@ fn crop_view_proj(view_proj: glam::Mat4, uo: f32, vo: f32, us: f32, vs: f32) -> // ── Render-style helpers (impl Scene) ──────────────────────────────────── impl Scene { - fn build_lighting_cache(&self) -> Vec { + fn build_lighting_cache( + &self, + target_block: Handle, + frozen: &rustc_hash::FxHashSet, + ) -> Vec { use acadrust::objects::{ClassObjectData, ObjectType}; fn normalized(value: [f64; 3], fallback: [f32; 3]) -> [f32; 3] { @@ -1385,20 +1389,26 @@ impl Scene { for &handle in crate::entities::object_data::light_entities( &self.object_data_cache, ) { - if lights.len() >= 4 { - break; - } if let Some(EntityType::Light(light)) = self.document.get_entity(handle) { + let common = &light.common; + if self.layer_frozen_in(&common.layer, Some(frozen)) + || !self.belongs_to_visible_block( + handle, + common.owner_handle, + target_block, + ) + { + continue; + } if let Some(light) = converted(self, light) { lights.push(light); } } } - if lights.len() < 4 { - let geo = crate::entities::object_data::geo_objects( - &self.object_data_cache, - ) + // SUN is document-global. Keep it after entity lights so the four-light + // shader limit prefers visible lights owned by this viewport's block. + let geo = crate::entities::object_data::geo_objects(&self.object_data_cache) .iter() .find_map(|handle| match self.document.objects.get(handle) { Some(ObjectType::GeoData(value)) @@ -1409,61 +1419,88 @@ impl Scene { && value.reference_point.y.abs() <= 90.0 => Some(value), _ => None, }); - for handle in crate::entities::object_data::sun_objects( - &self.object_data_cache, - ) { - let Some(ObjectType::ClassObject(value)) = - self.document.objects.get(handle) - else { - continue; - }; - let ClassObjectData::Sun(sun) = &value.data else { - continue; - }; - if !sun.is_on { - continue; - } - let Some(geo) = geo else { - break; - }; - let Some(direction) = solar_direction(sun, geo) else { - break; - }; - let rgba = tess_util::aci_to_rgba(&sun.color); - lights.push(SceneLight { - handle: value.handle, - color_layer: None, - light_type: 1.0, - position: [0.0; 3], - direction, - color: [rgba[0], rgba[1], rgba[2]], - intensity: sun.intensity.max(0.0) as f32, - hotspot_cos: 1.0, - falloff_cos: -1.0, - attenuation_type: 0.0, - attenuation_start: 0.0, - attenuation_end: 0.0, - }); - break; + for handle in crate::entities::object_data::sun_objects(&self.object_data_cache) { + let Some(ObjectType::ClassObject(value)) = self.document.objects.get(handle) else { + continue; + }; + let ClassObjectData::Sun(sun) = &value.data else { + continue; + }; + if !sun.is_on { + continue; } + let Some(geo) = geo else { + break; + }; + let Some(direction) = solar_direction(sun, geo) else { + break; + }; + let rgba = tess_util::aci_to_rgba(&sun.color); + lights.push(SceneLight { + handle: value.handle, + color_layer: None, + light_type: 1.0, + position: [0.0; 3], + direction, + color: [rgba[0], rgba[1], rgba[2]], + intensity: sun.intensity.max(0.0) as f32, + hotspot_cos: 1.0, + falloff_cos: -1.0, + attenuation_type: 0.0, + attenuation_start: 0.0, + attenuation_end: 0.0, + }); + break; } lights } - fn apply_document_lighting(&self, uniforms: &mut Uniforms) { - if self.lighting_cache.borrow().is_none() { - let lights = self.build_lighting_cache(); - *self.lighting_cache.borrow_mut() = Some(lights); + fn apply_document_lighting( + &self, + uniforms: &mut Uniforms, + target_block: Handle, + frozen: &rustc_hash::FxHashSet, + ) { + let key = (target_block, Self::frozen_layers_sig(frozen)); + if !self.lighting_cache.borrow().contains_key(&key) { + let lights = self.build_lighting_cache(target_block, frozen); + self.lighting_cache.borrow_mut().insert(key, lights); } let cache = self.lighting_cache.borrow(); - let lights = cache.as_deref().unwrap_or_default(); + let lights = cache.get(&key).map(Vec::as_slice).unwrap_or_default(); + let visible_lights: Vec<&SceneLight> = lights + .iter() + .filter(|light| match self.document.get_entity(light.handle) { + Some(EntityType::Light(entity)) => { + let common = &entity.common; + !common.invisible + && !self.entity_temporarily_hidden(light.handle) + && !self.layer_hidden(&common.layer) + && !self.layer_frozen_in(&common.layer, Some(frozen)) + && self.belongs_to_visible_block( + light.handle, + common.owner_handle, + target_block, + ) + } + Some(_) => false, + None => self.document.objects.get(&light.handle).is_some_and(|object| { + matches!( + object, + acadrust::objects::ObjectType::ClassObject(value) + if matches!(&value.data, acadrust::objects::ClassObjectData::Sun(_)) + ) + }), + }) + .take(4) + .collect(); let eye = [ uniforms.eye_high[0] as f64 + uniforms.eye_low[0] as f64, uniforms.eye_high[1] as f64 + uniforms.eye_low[1] as f64, uniforms.eye_high[2] as f64 + uniforms.eye_low[2] as f64, ]; - uniforms.lighting[0] = lights.len().min(4) as f32; - for (index, light) in lights.iter().take(4).enumerate() { + uniforms.lighting[0] = visible_lights.len() as f32; + for (index, light) in visible_lights.into_iter().enumerate() { let color = light .color_layer .as_deref() @@ -2124,6 +2161,26 @@ impl Scene { Arc::new(Vec::new()) }; + // Per-viewport frozen-layer set for a paper content viewport. Content + // viewports hide special fills, media, meshes and lights on VP-frozen + // layers too, matching the already-filtered resident wire set. + 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 lighting_block = if inst.paper_sheet { + self.current_layout_block_handle() + } else { + self.content_render_block_handle() + }; + // Build the camera at the *full* viewport's aspect so the ortho // frustum matches what the viewport entity stores, then post- // multiply by a clip-space "zoom into the visible sub-rect" that @@ -2159,7 +2216,7 @@ impl Scene { uniforms.viewport_size = [visible_w, visible_h]; uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 }; uniforms.transparency_enable = if self.transparency_display { 1.0 } else { 0.0 }; - self.apply_document_lighting(&mut uniforms); + self.apply_document_lighting(&mut uniforms, lighting_block, &vp_frozen); // `screen_rect` carries the *visible* sub-rectangle in normalized // canvas coords — that's what `Pipeline::prepare` uses to size @@ -2179,25 +2236,8 @@ impl Scene { // model-block hatches. Those belong inside the floating content // viewports; rendering them on the full-canvas sheet would let them // 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() - }; - + // paper area. Content viewport model builders are block-filtered too; + // the scissor only clips their already-correct Model Space set. let (hatches, wipeout_hatches, paper_images) = if inst.paper_sheet { let (hatches, wipeouts, images) = self.paper_sheet_render_models(); (hatches, wipeouts, Some(images))