fix(wipeout): mask block-internal wipeouts once inserted

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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-23 23:18:48 +03:00
commit de5c35d44a
2 changed files with 138 additions and 1 deletions

2
Cargo.lock generated
View file

@ -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",

View file

@ -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<Handle>>,
bg_color: [f32; 4],
depth_map: &HashMap<u64, [f32; 2]>,
models: &mut Vec<HatchModel>,
) {
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