From de5c35d44aff7c2bbb70853fa2124aa9ad1bdcef Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 23 Jul 2026 23:18:48 +0300 Subject: [PATCH] fix(wipeout): mask block-internal wipeouts once inserted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A wipeout defined inside a block never masked the geometry once the block was inserted — wipeout_models only scanned top-level entities, so the wipeout was invisible unless you entered block-edit (where it is top-level). It also drew a stray mask at its raw block-local coordinates because the top-level scan lacked the block-defn reject the hatch path uses. Descend visible inserts and place each block-internal wipeout in world space, applying the accumulated insert transform via apply / apply_rotation. Add the belongs_to_visible_block filter so block-defn wipeouts only appear through the descent, in world space. Bumps acadrust for the matching transform_wipeout fix: its u/v basis was double-scaled (apply_rotation then another scale-factor multiply), so a scaled insert grew the mask; the descent sidesteps that path but the explode/copy commands need the crate fix. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 2 +- src/scene/entity.rs | 137 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 116c7e7ad..9e8e233fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,7 +74,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#022290a3a2a548c54c8fdf580ef5e44de36b4520" +source = "git+https://github.com/OpenAEC-Foundation/acadifc?branch=main#824270e9c0463b8fb926424c9ac61eda5c09d752" dependencies = [ "ahash 0.8.12", "anyhow", diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 5c589fd0c..07fc148e9 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1101,6 +1101,7 @@ 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 { @@ -1109,6 +1110,17 @@ impl Scene { if entity.common().invisible { continue; } + // Reject block-defn-only wipeouts (owned by a BLOCK record that is + // neither model nor a paper layout block): those are placed via + // insert descent below, in world space. Without this they also draw + // once at their raw block-local coordinates. Mirrors the hatch path. + if !self.belongs_to_visible_block( + wo.common.handle, + wo.common.owner_handle, + layout_block, + ) { + continue; + } if self .document .layers @@ -1150,9 +1162,134 @@ impl Scene { }); } } + // Wipeouts nested inside block inserts: the loop above only sees + // top-level wipeouts, so a wipeout defined inside a block never masked + // once the block was inserted (it did in block-edit, where it is + // top-level). Descend visible inserts and place each block-internal + // wipeout in world space. The insert transform is applied here (apply / + // apply_rotation) rather than through Insert::explode — the latter + // double-scales the u/v basis. + for entity in self.document.entities() { + let EntityType::Insert(ins) = entity else { + continue; + }; + let c = &ins.common; + if c.invisible + || self + .document + .layers + .get(&c.layer) + .map(|l| l.flags.off || l.flags.frozen) + .unwrap_or(false) + || self.layer_frozen_in(&c.layer, frozen) + { + continue; + } + if !self.belongs_to_visible_block(c.handle, c.owner_handle, layout_block) { + continue; + } + if frozen.is_none() + && crate::scene::annotative::annotative_offscale(&self.document, c) + { + continue; + } + self.collect_block_wipeouts( + &ins.get_transform(), + &ins.block_name, + 0, + frozen, + bg_color, + &depth_map, + &mut models, + ); + } models } + /// Recursively collect wipeout masks defined inside a block, transformed to + /// world space by the accumulated insert transform. See `wipeout_models`. + #[allow(clippy::too_many_arguments)] + fn collect_block_wipeouts( + &self, + xform: &acadrust::types::Transform, + block_name: &str, + depth: usize, + frozen: Option<&rustc_hash::FxHashSet>, + bg_color: [f32; 4], + depth_map: &HashMap, + models: &mut Vec, + ) { + if depth > 32 { + return; + } + let Some(br) = self + .document + .block_records + .iter() + .find(|b| b.name.eq_ignore_ascii_case(block_name)) + else { + return; + }; + for &eh in &br.entity_handles { + let Some(e) = self.document.get_entity(eh) else { + continue; + }; + let c = e.common(); + if c.invisible + || self + .document + .layers + .get(&c.layer) + .map(|l| l.flags.off || l.flags.frozen) + .unwrap_or(false) + || self.layer_frozen_in(&c.layer, frozen) + { + continue; + } + match e { + EntityType::Wipeout(wo) => { + let mut w = wo.clone(); + w.insertion_point = xform.apply(wo.insertion_point); + w.u_vector = xform.apply_rotation(wo.u_vector); + w.v_vector = xform.apply_rotation(wo.v_vector); + let (fill_origin, boundary) = Self::wipeout_boundary_2d(&w); + if boundary.len() >= 3 { + let mut fill_color = bg_color; + if self.selected.contains(&c.handle) { + fill_color = [0.15, 0.55, 1.00, 0.35]; + } + models.push(HatchModel { + boundary: Arc::new(boundary), + boundary_wcs: None, + pattern: model::hatch_model::HatchPattern::Solid, + name: "WIPEOUT_FILL".into(), + color: fill_color, + angle_offset: 0.0, + scale: 1.0, + world_origin: fill_origin, + draw_depth: depth_map.get(&c.handle.value()).map_or(0.0, |d| d[0]), + }); + } + } + EntityType::Insert(ni) => { + // `compose` applies the nested insert's transform first, + // then the accumulated parent transform. + let child = xform.compose(&ni.get_transform()); + self.collect_block_wipeouts( + &child, + &ni.block_name, + depth + 1, + frozen, + bg_color, + depth_map, + models, + ); + } + _ => {} + } + } + } + /// Compute the 2D (XY) boundary polygon for a Wipeout entity. /// Wipeout fill boundary as small f32 offsets from the returned `world_origin` /// (the insertion point, kept in f64). Building it in absolute WCS f32