diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 46c9740f7..9837ae5ee 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -362,6 +362,110 @@ fn build_defn( nested_ins, doc, bg_color, depth_map, ))); } + // A dimension nested in a block bakes its geometry (extension / + // dim lines, arrows, text) into a per-instance `*D` block, exactly + // like a top-level dimension. The top-level path expands that block + // in `tessellate_entity`; the block-expand path calls the plain + // `tessellate::tessellate`, which does NOT, so nested dimensions + // drew nothing. Expand the `*D` block's entities here as block-local + // subs so they transform with the parent insert. (Empty block_name + // — a non-baked dimension — falls through to the default arm.) + EntityType::Dimension(dim) if !dim.base().block_name.trim().is_empty() => { + let dblk = doc + .block_records + .iter() + .find(|br| br.name.eq_ignore_ascii_case(&dim.base().block_name)); + if let Some(dblk) = dblk { + // The `*D` block content is baked in the coordinate space it + // occupied when the dimension was created — for a dimension + // inside a block that is often the ORIGINAL WCS, not the + // block-local space. The dimension's insertion_point (DXF + // 12) is the offset that maps the baked content into the + // dimension's own (block-local) space; it is zero for + // dimensions baked in place, so this is a no-op there. + let ins = dim.base().insertion_point; + for &deh in &dblk.entity_handles { + let Some(dsub) = doc.get_entity(deh) else { + continue; + }; + // Definition points are baked as POINTs on Defpoints — + // grip markers, never drawn (matches the top-level path). + if matches!(dsub, EntityType::Point(_)) { + continue; + } + if dsub.common().invisible || layer_hidden(doc, &dsub.common().layer) { + continue; + } + let mut placed = dsub.clone(); + placed.as_entity_mut().translate(ins); + // An arrowhead is baked as a nested INSERT of an arrow + // block — route it through the nested-ref machinery so + // the arrow block expands (tessellate_sub_local doesn't + // expand inserts). + if let EntityType::Insert(arrow) = &placed { + subs.push(LocalSub::Nested(build_nested_ref( + arrow, doc, bg_color, depth_map, + ))); + } else { + for lw in tessellate_sub_local( + doc, &placed, anno_scale, bg_color, depth_map, + ) { + subs.push(LocalSub::Wire(lw)); + } + } + } + } + } + // A table nested in a block bakes its geometry (gridlines, cell + // text, fills) into a `*T` block, laid out in block-LOCAL space with + // the table carrying the world placement in insertion_point + + // horizontal_direction (like an INSERT). The top-level path expands + // and places it in `tessellate_entity`; the block-expand path skips + // that, so a nested table drew nothing. Place each `*T` sub by the + // table's rotation+insertion here (defn-local), then tessellate — the + // parent insert transform composes on top at expand time. + EntityType::Table(tab) => { + let tblk = tab + .block_record_handle + .and_then(|h| doc.block_records.iter().find(|br| br.handle == h)); + if let Some(tblk) = tblk { + let ins = tab.insertion_point; + let angle = tab.horizontal_direction.y.atan2(tab.horizontal_direction.x); + for &teh in &tblk.entity_handles { + let Some(tsub) = doc.get_entity(teh) else { + continue; + }; + if tsub.common().invisible || layer_hidden(doc, &tsub.common().layer) { + continue; + } + let mut placed = tsub.clone(); + { + let ent = placed.as_entity_mut(); + if angle.abs() > 1e-9 { + ent.apply_rotation( + acadrust::types::Vector3::new(0.0, 0.0, 1.0), + angle, + ); + } + ent.translate(ins); + } + // A baked table can nest inserts too (block cells); + // route those through the nested-ref path like the + // dimension arrows above. + if let EntityType::Insert(cell_ins) = &placed { + subs.push(LocalSub::Nested(build_nested_ref( + cell_ins, doc, bg_color, depth_map, + ))); + } else { + for lw in + tessellate_sub_local(doc, &placed, anno_scale, bg_color, depth_map) + { + subs.push(LocalSub::Wire(lw)); + } + } + } + } + } _ => { // A wide polyline inside a block carries its `world_width` on // the LocalWire; `emit_wire` scales it by the insert transform diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 74efec0f1..6835cfe67 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -847,6 +847,52 @@ impl Scene { // at world position, like a block-internal hatch. (#222) let mut wide_block_memo: std::collections::HashMap = std::collections::HashMap::new(); + // `explode_from_document` returns a Dimension child verbatim (it carries + // no fill of its own), so a filled arrowhead that lives as a nested + // Insert(hatch) inside the dimension's baked `*D` block never reaches + // this fill explosion. Expand each dimension child's `*D` content into + // the owning block's frame — translate by the dimension insertion_point + // (WCS → block-local) then apply the insert transform (block-local → + // world) — so its arrow inserts / hatches ride the normal walk. Only + // fill-bearing children (Insert / Hatch) are lifted; the dimension's + // lines and text still render through the wire path. + fn explode_including_dims(ins: &DxfInsert, doc: &CadDocument) -> Vec { + let mut out = ins.explode_from_document(doc); + let Some(br) = doc.block_records.get(&ins.block_name) else { + return out; + }; + let xform = ins.get_transform(); + for &beh in &br.entity_handles { + let Some(EntityType::Dimension(dim)) = doc.get_entity(beh) else { + continue; + }; + let bn = dim.base().block_name.clone(); + if bn.trim().is_empty() { + continue; + } + let ins_pt = dim.base().insertion_point; + let Some(dblk) = doc + .block_records + .iter() + .find(|r| r.name.eq_ignore_ascii_case(&bn)) + else { + continue; + }; + for &deh in &dblk.entity_handles { + let Some(dsub) = doc.get_entity(deh) else { + continue; + }; + if !matches!(dsub, EntityType::Insert(_) | EntityType::Hatch(_)) { + continue; + } + let mut placed = dsub.clone(); + placed.as_entity_mut().translate(ins_pt); + placed.apply_transform(&xform); + out.push(placed); + } + } + out + } for entity in self.document.entities() { let EntityType::Insert(ins) = entity else { continue; @@ -902,8 +948,7 @@ impl Scene { [f32; 4], crate::scene::view::render::InheritStyle, (f32, f32), - )> = ins - .explode_from_document(&self.document) + )> = explode_including_dims(ins, &self.document) .into_iter() .map(|e| (normalize(e), 0usize, ins_color, l0, (base_depth, half_gap))) .collect(); @@ -947,7 +992,7 @@ impl Scene { .map_or(0.0, |d| d[0]) * d_half; let nd_half = d_half / (block_count(&nins.block_name) as f32 + 1.0); - for e in nins.explode_from_document(&self.document) { + for e in explode_including_dims(&nins, &self.document) { stack.push(( normalize(e), depth + 1, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index e41f43451..1bcb73a49 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -4092,6 +4092,14 @@ impl Scene { br.entity_handles.iter().any(|&h| match self.document.get_entity(h) { Some(EntityType::Hatch(_)) => true, Some(EntityType::Insert(ins)) => self.block_has_hatch(&ins.block_name, memo), + // A dimension bakes its geometry into a per-instance `*D` + // block; a custom filled arrowhead lives there as a nested + // Insert(hatch). Recurse so the fill explosion knows to + // descend the dimension (see `explode_including_dims`). + Some(EntityType::Dimension(dim)) => { + !dim.base().block_name.trim().is_empty() + && self.block_has_hatch(&dim.base().block_name, memo) + } _ => false, }) })