diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index e738dfe41..381d1c186 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -261,28 +261,10 @@ impl OpenCADStudio { .filter(|s| !s.is_empty()) .collect(); - // Which block definitions are LIVE — reachable from a real - // layout container by following every block reference. A - // block nothing reachable inserts is dead and purges, even a - // whole mutually-referencing subgraph (e.g. the dependent - // blocks a detached xref leaves behind: they insert one - // another but hang off no layout, so a flat "is it named by - // any INSERT" scan keeps them alive forever). Roots: - // • *Model_Space / *Paper_Space(N), - // • any block whose layout handle resolves to a real Layout - // object — a DANGLING layout handle, as orphaned xref - // blocks carry, does NOT count, so is_layout() alone can't - // shield the dead subgraph it heads, - // • dimension-style arrowhead blocks (referenced by handle). - // A block's out-edges: its members' INSERT names, dimension - // *D blocks, table *T / cell blocks, and MultiLeader content. + // Live blocks are reachable from layouts and style roots + // through the shared graph. Dangling layouts do not count. let live_blocks: rustc_hash::FxHashSet = { let doc = &self.tabs[i].scene.document; - let by_handle: rustc_hash::FxHashMap = doc - .block_records - .iter() - .map(|br| (br.handle, br.name.clone())) - .collect(); let is_real_layout = |br: &acadrust::BlockRecord| -> bool { let up = br.name.to_ascii_uppercase(); up.starts_with("*MODEL_SPACE") @@ -298,44 +280,19 @@ impl OpenCADStudio { return out; }; for &h in &br.entity_handles { - match doc.get_entity(h) { - Some(acadrust::EntityType::Insert(ins)) - if !ins.block_name.is_empty() => - { - out.push(ins.block_name.clone()); - } - Some(acadrust::EntityType::Dimension(d)) - if !d.base().block_name.is_empty() => - { - out.push(d.base().block_name.clone()); - } - Some(acadrust::EntityType::Table(t)) => { - if let Some(bh) = t.block_record_handle { - if let Some(n) = by_handle.get(&bh) { - out.push(n.clone()); - } - } - for row in &t.rows { - for cell in &row.cells { - for c in &cell.contents { - if let Some(bh) = c.block_handle { - if let Some(n) = by_handle.get(&bh) { - out.push(n.clone()); - } - } - } - } - } - } - Some(acadrust::EntityType::MultiLeader(ml)) => { - if let Some(bh) = ml.block_content_handle { - if let Some(n) = by_handle.get(&bh) { - out.push(n.clone()); - } - } - } - _ => {} - } + let Some(entity) = doc.get_entity(h) else { + continue; + }; + out.extend( + crate::scene::render_graph::entity_block_uses( + doc, + entity, + 1.0, + ) + .into_iter() + .filter(|block_use| !block_use.block.is_null()) + .map(|block_use| block_use.insert.block_name), + ); } out }; @@ -346,15 +303,11 @@ impl OpenCADStudio { .filter(|&br| is_real_layout(br)) .map(|br| br.name.clone()) .collect(); - for ds in doc.dim_styles.iter() { - for h in [ds.dimblk, ds.dimblk1, ds.dimblk2, ds.dimldrblk] { - if !h.is_null() { - if let Some(n) = by_handle.get(&h) { - stack.push(n.clone()); - } - } - } - } + stack.extend( + crate::scene::render_graph::document_block_uses(doc) + .into_iter() + .map(|block_use| block_use.insert.block_name), + ); while let Some(n) = stack.pop() { if !live.insert(n.clone()) { continue; @@ -422,9 +375,8 @@ impl OpenCADStudio { .filter(|br| { let up = br.name.to_ascii_uppercase(); // Never PURGE an xref *definition* — that is a - // detach, not a purge. (Orphaned dependent blocks - // a past detach left behind are is_xref=false and - // still fall to the reachability test.) + // detach, not a purge. Orphaned dependencies from + // a past detach still use the reachability test. !br.flags.is_xref && !br.flags.is_xref_overlay && !up.starts_with("*MODEL_SPACE") diff --git a/src/entities/insert.rs b/src/entities/insert.rs index aa2c2339d..17fa1be69 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -185,6 +185,57 @@ impl crate::entities::traits::FallbackTess for Insert { ) } } +pub(crate) fn insert_attribute_entities( + document: &acadrust::CadDocument, + insert: &acadrust::entities::Insert, + annotation_scale: f32, +) -> Vec { + let visibility = document.header.attribute_visibility; + if visibility == 0 || insert.attributes.is_empty() { + return Vec::new(); + } + let block_scale = if insert + .common + .extended_data + .get_record("AcAnnotativeData") + .is_some() + { + annotation_scale as f64 + } else { + 1.0 + }; + let insertion = insert.insert_point; + let scale_about = |point: Vector3| { + Vector3::new( + insertion.x + (point.x - insertion.x) * block_scale, + insertion.y + (point.y - insertion.y) * block_scale, + insertion.z + (point.z - insertion.z) * block_scale, + ) + }; + insert + .attributes + .iter() + .filter(|attribute| { + let layer_visible = document + .layers + .get(&attribute.common.layer) + .is_none_or(|layer| !layer.flags.off && !layer.flags.frozen); + layer_visible + && (visibility == 2 + || (!attribute.common.invisible && !attribute.flags.invisible)) + }) + .map(|attribute| { + let mut attribute = attribute.clone(); + if (block_scale - 1.0).abs() > 1.0e-6 { + attribute.height *= block_scale; + attribute.insertion_point = scale_about(attribute.insertion_point); + attribute.alignment_point = scale_about(attribute.alignment_point); + } + EntityType::AttributeEntity(attribute) + }) + .collect() +} + pub(crate) fn append_insert_attribute_wires( wires: &mut Vec, document: &acadrust::CadDocument, @@ -202,47 +253,8 @@ pub(crate) fn append_insert_attribute_wires( pslt_factor: f32, anno_scale: f32, ) { - if ins.attributes.is_empty() { - return; - } - // ATTMODE (header.attribute_visibility): - // 0 = Off — every attribute hidden - // 1 = Normal — honour per-attribute `invisible` flag (default) - // 2 = On — every attribute forced visible, ignoring its flag - let attmode = document.header.attribute_visibility; - if attmode == 0 { - return; - } - // Attributes inherit the annotative block scale. Pre-scale them here so - // text tessellation stays at a neutral scale. - let annotative = ins - .common - .extended_data - .get_record("AcAnnotativeData") - .is_some(); - let block_scale = if annotative { anno_scale as f64 } else { 1.0 }; - let ip = ins.insert_point; - let scale_about = |q: acadrust::types::Vector3| { - acadrust::types::Vector3::new( - ip.x + (q.x - ip.x) * block_scale, - ip.y + (q.y - ip.y) * block_scale, - ip.z + (q.z - ip.z) * block_scale, - ) - }; - for attr in &ins.attributes { - let per_attr_hidden = attr.common.invisible || attr.flags.invisible; - if attmode == 1 && per_attr_hidden { - continue; - } - let attr_entity = EntityType::AttributeEntity({ - let mut a = attr.clone(); - if (block_scale - 1.0).abs() > 1e-6 { - a.height *= block_scale; - a.insertion_point = scale_about(a.insertion_point); - a.alignment_point = scale_about(a.alignment_point); - } - a - }); + for attr_entity in insert_attribute_entities(document, ins, anno_scale) { + let attr = attr_entity.common(); let (sub_color, sub_plen, sub_pat, sub_lw_px, sub_aci) = render::render_style_for_block_sub( document, &attr_entity, @@ -258,12 +270,12 @@ pub(crate) fn append_insert_attribute_wires( } else { sub_color }; - let attr_plottable = if render::is_effective_layer_zero(&attr.common.layer) { + let attr_plottable = if render::is_effective_layer_zero(&attr.layer) { ins_layer_plottable } else { document .layers - .get(&attr.common.layer) + .get(&attr.layer) .map(|layer| layer.is_plottable) .unwrap_or(true) }; diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 639e2eb7f..4474ea15c 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -2104,11 +2104,19 @@ impl MultiLeaderTess for MultiLeader { fill_tris_low: Vec::new(), }); - if let Some(insert) = block_content_insert(document, ml) { + let host = acadrust::EntityType::MultiLeader(ml.clone()); + for block_use in crate::scene::render_graph::entity_render_block_uses(document, &host, 1.0) + .into_iter() + .filter(|block_use| { + block_use.active + && block_use.role + == crate::scene::render_graph::BlockRole::MultiLeaderContent + }) + { wires.extend( crate::scene::convert::tess::expand_block_object( document, - &insert, + &block_use.insert, handle, selected, active_viewport, diff --git a/src/entities/table.rs b/src/entities/table.rs index 0a75045d0..3b5174840 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -963,16 +963,22 @@ pub(crate) fn block_cell_inserts( insert.set_x_scale(scale); insert.set_y_scale(scale); insert.set_z_scale(scale); - let transform = insert.get_transform(); let local_anchor = if has_block_bounds { (block_min + block_max) * 0.5 } else { - record.base_point + crate::scene::render_graph::block_base_point(document, &record.name) }; - let transformed_base = transform.apply(local_anchor); - let transformed_zero = transform.apply(Vector3::ZERO); + let transformed_anchor = crate::scene::render_graph::insert_transform( + document, + &insert, + ) + .apply(local_anchor); insert.insert_point = insert.insert_point - - (transformed_base - transformed_zero); + + Vector3::new( + position.x as f64 - transformed_anchor.x, + position.y as f64 - transformed_anchor.y, + position.z as f64 - transformed_anchor.z, + ); let attribute_definitions: Vec<_> = record .entity_handles .iter() @@ -1001,7 +1007,7 @@ pub(crate) fn block_cell_inserts( AttributeEntity::from_definition(definition, Some(attribute.value.clone())); acadrust::Entity::apply_transform( &mut entity, - &insert.get_transform(), + &crate::scene::render_graph::insert_transform(document, &insert), ); entity.common.handle = table.common.handle; insert.attributes.push(entity); diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 47deee40b..f38ba11c5 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -115,6 +115,9 @@ pub struct NestedRef { pub block_name: String, pub xform: Transform, pub style: crate::scene::render_graph::InsertStyleSpec, + /// ATTRIB geometry is stored in the parent definition's frame, but resolves + /// ByBlock and layer-0 properties through this nested insert's style. + pub attachments: Vec, pub instance_offsets: Vec<[f64; 3]>, pub plot_visible: bool, pub plot_l0: bool, @@ -139,12 +142,8 @@ pub enum LocalSub { #[derive(Clone, Debug, Default)] pub struct BlockDefn { pub subs: Vec, - pub base_point: Vector3, pub has_point_marker: bool, - /// Union of every sub's local AABB (including nested-INSERT contributions - /// resolved at expand time via their own defn's `aabb_local`). XY only — - /// the wire renderer is 2D-dominant. Expressed in this defn's *offset* - /// Absolute world-space XY (the double-single render path keeps it precise). + /// Union of direct and nested geometry in definition coordinates. pub aabb_local: [f32; 4], /// Raw entity count of the source block record (`entity_handles.len()`). /// Divisor for nested depth composition: a nested insert's children get a @@ -207,7 +206,12 @@ impl BlockCache { } pub fn defn(&self, block_name: &str) -> Option<&Arc> { - self.defns.get(block_name) + self.defns.get(block_name).or_else(|| { + self.defns + .iter() + .find(|(name, _)| name.eq_ignore_ascii_case(block_name)) + .map(|(_, definition)| definition) + }) } /// Build definitions referenced by block-backed entities. Space records stay @@ -225,7 +229,7 @@ impl BlockCache { ) -> Self { use crate::par::prelude::*; let mut cache = Self::new(); - let referenced = collect_referenced_blocks(doc); + let referenced = collect_referenced_blocks(doc, anno_scale); let mut reference_counts: HashMap = HashMap::default(); for entity in doc.entities() { if let EntityType::Insert(insert) = entity { @@ -280,7 +284,7 @@ impl BlockCache { ) -> Self { use crate::par::prelude::*; let mut cache = Self::new(); - let referenced = referenced_block_tree(doc, block_name); + let referenced = referenced_block_tree(doc, block_name, anno_scale); cache.defns = referenced .par_iter() .map(|name| { @@ -355,7 +359,11 @@ impl BlockCache { let found = defn.subs.iter().any(|sub| match sub { LocalSub::Wire(wire) => wire.point_marker.is_some(), LocalSub::Nested(insert) => { - self.defn_has_point_marker_recursive(&insert.block_name, visited) + insert + .attachments + .iter() + .any(|wire| wire.point_marker.is_some()) + || self.defn_has_point_marker_recursive(&insert.block_name, visited) } }); visited.pop(); @@ -385,7 +393,10 @@ impl BlockCache { LocalSub::Wire(lw) => lw.aabb_local, LocalSub::Nested(nref) => { let nested = self.defn_aabb_recursive(&nref.block_name, visited); - transform_aabb_xy(nested, &nref.xform) + nref.attachments.iter().fold( + transform_aabb_xy(nested, &nref.xform), + |aabb, wire| aabb_union(aabb, wire.aabb_local), + ) } }; acc = aabb_union(acc, aabb); @@ -395,36 +406,39 @@ impl BlockCache { } } -fn block_object_name(doc: &CadDocument, entity: &EntityType) -> Option { - if let EntityType::Insert(insert) = entity { - Some(insert.block_name.clone()) - } else { - crate::scene::render_graph::owned_block_insert(doc, entity) - .map(|owned| owned.insert.block_name) - } +fn block_object_names( + doc: &CadDocument, + entity: &EntityType, + anno_scale: f32, +) -> Vec { + crate::scene::render_graph::entity_render_block_uses(doc, entity, anno_scale) + .into_iter() + .filter(|block_use| !block_use.block.is_null()) + .map(|block_use| block_use.insert.block_name) + .collect() } -/// Collect every referenced block-backed object. -fn collect_referenced_blocks(doc: &CadDocument) -> Vec { +/// Collect block definitions needed by rendered representations. +fn collect_referenced_blocks(doc: &CadDocument, anno_scale: f32) -> Vec { let mut seen: HashSet = HashSet::default(); let mut queue: Vec = Vec::new(); for entity in doc.entities() { - if let Some(name) = block_object_name(doc, entity) { + for name in block_object_names(doc, entity, anno_scale) { if seen.insert(name.clone()) { queue.push(name); } } } while let Some(name) = queue.pop() { - let Some(br) = doc.block_records.get(&name) else { + let Some(br) = crate::scene::render_graph::block_record_by_name(doc, &name) else { continue; }; for &eh in &br.entity_handles { let Some(entity) = doc.get_entity(eh) else { continue; }; - if let Some(name) = block_object_name(doc, entity) { + for name in block_object_names(doc, entity, anno_scale) { if seen.insert(name.clone()) { queue.push(name); } @@ -434,19 +448,19 @@ fn collect_referenced_blocks(doc: &CadDocument) -> Vec { seen.into_iter().collect() } -fn referenced_block_tree(doc: &CadDocument, root: &str) -> Vec { +fn referenced_block_tree(doc: &CadDocument, root: &str, anno_scale: f32) -> Vec { let mut seen: HashSet = HashSet::default(); let mut queue = vec![root.to_string()]; seen.insert(root.to_string()); while let Some(name) = queue.pop() { - let Some(record) = doc.block_records.get(&name) else { + let Some(record) = crate::scene::render_graph::block_record_by_name(doc, &name) else { continue; }; for &handle in &record.entity_handles { let Some(entity) = doc.get_entity(handle) else { continue; }; - if let Some(name) = block_object_name(doc, entity) { + for name in block_object_names(doc, entity, anno_scale) { if seen.insert(name.clone()) { queue.push(name); } @@ -474,7 +488,7 @@ fn build_defn( viewport: Option, depth_map: &HashMap, ) -> BlockDefn { - let br = match doc.block_records.get(block_name) { + let br = match crate::scene::render_graph::block_record_by_name(doc, block_name) { Some(br) => br, None => return BlockDefn::default(), }; @@ -520,19 +534,31 @@ fn build_defn( ) { continue; } - if let Some(owned) = crate::scene::render_graph::owned_block_insert(doc, entity) { - if owned.render_picture { - let mut nested = build_nested_ref( - &owned.insert, - doc, - bg_color, - viewport, - depth_map, - ); - nested.suppress_root_points = owned.suppress_root_points; - subs.push(LocalSub::Nested(nested)); - continue; - } + let block_uses: Vec<_> = crate::scene::render_graph::entity_render_block_uses( + doc, + entity, + anno_scale, + ) + .into_iter() + .filter(|block_use| block_use.active) + .collect(); + let replaces_host = block_uses + .iter() + .any(|block_use| block_use.replaces_host_wire); + for block_use in &block_uses { + let mut nested = build_nested_ref( + &block_use.insert, + doc, + anno_scale, + bg_color, + viewport, + depth_map, + ); + nested.suppress_root_points = block_use.suppress_root_points; + subs.push(LocalSub::Nested(nested)); + } + if replaces_host { + continue; } match entity { EntityType::Block(_) | EntityType::BlockEnd(_) => continue, @@ -549,50 +575,6 @@ fn build_defn( { continue } - EntityType::Insert(nested_ins) => { - subs.push(LocalSub::Nested(build_nested_ref( - nested_ins, doc, bg_color, viewport, depth_map, - ))); - } - EntityType::Dimension(_) => { - for wire in tessellate_sub_local( - doc, - entity, - anno_scale, - annotation_scale_handle, - bg_color, - viewport, - depth_map, - ) { - subs.push(LocalSub::Wire(wire)); - } - } - EntityType::Table(table) => { - for wire in tessellate_sub_local( - doc, - entity, - anno_scale, - annotation_scale_handle, - bg_color, - viewport, - depth_map, - ) { - subs.push(LocalSub::Wire(wire)); - } - for insert in crate::entities::table::block_cell_inserts( - table, - doc, - anno_scale, - ) { - subs.push(LocalSub::Nested(build_nested_ref( - &insert, - doc, - bg_color, - viewport, - depth_map, - ))); - } - } _ => { // A wide polyline inside a block carries its `world_width` on // the LocalWire; `emit_wire` scales it by the insert transform @@ -614,7 +596,6 @@ fn build_defn( } BlockDefn { subs, - base_point: crate::scene::render_graph::block_base_point(doc, block_name), has_point_marker: false, aabb_local: [0.0; 4], child_count: br.entity_handles.len(), @@ -624,16 +605,19 @@ fn build_defn( fn build_nested_ref( nested_ins: &acadrust::entities::Insert, doc: &CadDocument, + anno_scale: f32, bg_color: [f32; 4], viewport: Option, depth_map: &HashMap, ) -> NestedRef { - let _ = bg_color; - // Bake the XCLIP boundary (parent-defn-local) so the nested insert keeps // its clip when the parent block is expanded — the spatial filter object // isn't reachable at expand time. - let xform = crate::scene::render_graph::insert_transform(doc, nested_ins); + let xform = crate::scene::render_graph::insert_transform_at_scale( + doc, + nested_ins, + anno_scale, + ); let clip_poly = crate::scene::pick::xclip::insert_spatial_filter(doc, nested_ins) .map(|filter| { crate::scene::pick::xclip::world_clip_polygon_for_transform(filter, &xform) @@ -646,11 +630,30 @@ fn build_nested_ref( .get(&nested_ins.common.layer) .map(|layer| layer.is_plottable) .unwrap_or(true); + let attachments = crate::entities::insert::insert_attribute_entities( + doc, + nested_ins, + anno_scale, + ) + .into_iter() + .flat_map(|attribute| { + tessellate_sub_local( + doc, + &attribute, + 1.0, + None, + bg_color, + viewport, + depth_map, + ) + }) + .collect(); NestedRef { block_name: nested_ins.block_name.clone(), xform, style: crate::scene::render_graph::InsertStyleSpec::new(doc, nested_ins, viewport), + attachments, instance_offsets: crate::scene::render_graph::array_offsets(nested_ins), plot_visible, plot_l0, @@ -941,6 +944,7 @@ pub fn aabb_disjoint_xy(a: [f32; 4], b: [f32; 4]) -> bool { /// Returns `None` if no defn is cached for `ins.block_name`. Returns /// `Some(empty)` if the defn exists but is empty. pub fn expand_insert( + doc: &CadDocument, cache: &BlockCache, ins: &acadrust::entities::Insert, ins_handle: Handle, @@ -975,26 +979,7 @@ pub fn expand_insert( suppress_root_points: bool, ) -> Option> { let defn = cache.defn(&ins.block_name)?; - let base = defn.base_point; - let mut xform = Transform::from_translation(Vector3::new(-base.x, -base.y, -base.z)) - .then(&ins.get_transform()); - // Annotative blocks (the flag lives on the block definition; the instance is - // marked with the AcAnnotativeData XDATA) scale as ONE uniform unit about - // their insertion point — internal geometry/text/attributes are carried by - // this transform, never scaled individually (which would double-scale). - if (anno_scale - 1.0).abs() > 1e-6 - && ins - .common - .extended_data - .get_record("AcAnnotativeData") - .is_some() - { - let p = ins.insert_point; - let scale_about_p = Transform::from_translation(Vector3::new(-p.x, -p.y, -p.z)) - .then(&Transform::from_scale(anno_scale as f64)) - .then(&Transform::from_translation(Vector3::new(p.x, p.y, p.z))); - xform = xform.then(&scale_about_p); - } + let xform = crate::scene::render_graph::insert_transform_at_scale(doc, ins, anno_scale); let name = ins_handle.value().to_string(); let prototype_key = if !ins.is_array() && cache.prototype_blocks.contains(&ins.block_name) @@ -1793,41 +1778,6 @@ fn expand_defn( emit_wire(lw, accum_xform, ctx, out, d_range); } LocalSub::Nested(nref) => { - if visited.iter().any(|n| n == &nref.block_name) { - // Cycle — skip. - continue; - } - let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else { - continue; - }; - // Nested-INSERT cull: union AABB of the nested defn, - // transformed by composed xform, vs view rect + pixel size. - // `nested_defn.aabb_local` lives in the nested defn's offset - // frame — re-add `nested_defn.local_offset` in f64 before - // composing with the parent transforms. - let composed = nref.xform.then(accum_xform); - let world = transform_aabb_xy(nested_defn.aabb_local, &composed); - let local = [ - world[0] as f32, - world[1] as f32, - world[2] as f32, - world[3] as f32, - ]; - if let Some(view) = ctx.view_aabb { - if aabb_disjoint_xy(local, view) { - continue; - } - } - if let Some(wpp) = ctx.world_per_pixel { - if aabb_pixel_size(local, wpp) < MIN_PIXEL_SIZE { - continue; - } - } - // Resolve the nested insert's own style against the outer ctx: - // ByBlock inherits the outer insert; a nested insert that is - // itself on layer "0" with ByLayer props inherits the outer - // layer-0 target (so its ByBlock leaves resolve to that layer, - // not layer 0). Mirrors the leaf resolution in emit_wire. let parent_style = crate::scene::render_graph::BlockStyle { insert: ( ctx.ins_color, @@ -1863,6 +1813,33 @@ fn expand_defn( is_xref: ctx.is_xref, bg_color: ctx.bg_color, }; + for attachment in &nref.attachments { + emit_wire(attachment, accum_xform, &inner_ctx, out, d_range); + } + if visited.iter().any(|n| n == &nref.block_name) { + continue; + } + let Some(nested_defn) = ctx.cache.defn(&nref.block_name) else { + continue; + }; + let composed = nref.xform.then(accum_xform); + let world = transform_aabb_xy(nested_defn.aabb_local, &composed); + let local = [ + world[0] as f32, + world[1] as f32, + world[2] as f32, + world[3] as f32, + ]; + if let Some(view) = ctx.view_aabb { + if aabb_disjoint_xy(local, view) { + continue; + } + } + if let Some(wpp) = ctx.world_per_pixel { + if aabb_pixel_size(local, wpp) < MIN_PIXEL_SIZE { + continue; + } + } visited.push(nref.block_name.clone()); // Children of this nested insert stack inside the slot its own // rank owns — same composition the scene graph applies. diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 07c9dab44..317148c9b 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -73,6 +73,7 @@ fn block_entity_transform( block_name: &str, target: Handle, visited: &mut Vec, + annotation_scale: f32, ) -> Option { if visited .iter() @@ -90,12 +91,28 @@ fn block_entity_transform( visited.push(record.name.clone()); let transform = record.entity_handles.iter().find_map(|handle| { - let EntityType::Insert(insert) = document.get_entity(*handle)? else { - return None; - }; - let inner = - block_entity_transform(document, &insert.block_name, target, visited)?; - Some(inner.then(&insert.get_transform())) + let entity = document.get_entity(*handle)?; + crate::scene::render_graph::entity_render_block_uses( + document, + entity, + annotation_scale, + ) + .into_iter() + .filter(|block_use| block_use.active) + .find_map(|block_use| { + let inner = block_entity_transform( + document, + &block_use.insert.block_name, + target, + visited, + annotation_scale, + )?; + Some(inner.then(&crate::scene::render_graph::insert_transform_at_scale( + document, + &block_use.insert, + annotation_scale, + ))) + }) }); visited.pop(); transform @@ -461,7 +478,7 @@ impl Scene { /// Centre on a text entity stored in a block definition, transformed /// through the concrete visible INSERT occurrence that FIND is visiting. pub fn center_camera_on_block_entity(&mut self, insert: Handle, entity: Handle) -> bool { - let (source, mut transform) = { + let (source, transform) = { let Some(EntityType::Insert(insert_entity)) = self.document.get_entity(insert) else { return false; }; @@ -474,40 +491,20 @@ impl Scene { &insert_entity.block_name, entity, &mut visited, + self.annotation_scale, ) else { return false; }; ( source.clone(), - inner.then(&insert_entity.get_transform()), + inner.then(&crate::scene::render_graph::insert_transform_at_scale( + &self.document, + insert_entity, + self.annotation_scale, + )), ) }; - if (self.annotation_scale - 1.0).abs() > 1e-6 { - let Some(EntityType::Insert(insert_entity)) = self.document.get_entity(insert) else { - return false; - }; - if insert_entity - .common - .extended_data - .get_record("AcAnnotativeData") - .is_some() - { - let point = insert_entity.insert_point; - let scale_about = - acadrust::types::Transform::from_translation(acadrust::types::Vector3::new( - -point.x, -point.y, -point.z, - )) - .then(&acadrust::types::Transform::from_scale( - self.annotation_scale as f64, - )) - .then(&acadrust::types::Transform::from_translation( - acadrust::types::Vector3::new(point.x, point.y, point.z), - )); - transform = transform.then(&scale_about); - } - } - let fallback_z = self.active_camera_target().z; let wires = self.tessellate_one(&source); let Some(local_center) = rendered_wire_center(&wires, fallback_z) else { diff --git a/src/scene/convert/dgn_linestyle.rs b/src/scene/convert/dgn_linestyle.rs index b44116a3a..70ba4c6b5 100644 --- a/src/scene/convert/dgn_linestyle.rs +++ b/src/scene/convert/dgn_linestyle.rs @@ -1,6 +1,6 @@ //! DGN line-style rendering (first pass). //! -//! Linetypes converted from MicroStation DGN store their real pattern as DGN +//! Imported DGN linetypes store their real pattern as DGN //! line-style objects (`AcDbLS*`), not standard `LTYPE` dashes — the standard //! table entry is empty, so acadrust exposes the structure in //! [`CadDocument::dgn_ls_definitions`] / `dgn_ls_components` instead. See @@ -12,7 +12,7 @@ //! lengths are carried through to the pipe walls. use acadrust::objects::DgnLsComponentType; -use acadrust::types::{Handle, Transform, Vector3}; +use acadrust::types::{Handle, Vector3}; use acadrust::{CadDocument, EntityType}; use std::collections::HashSet; @@ -133,29 +133,52 @@ pub fn wall_dashes(doc: &CadDocument, lt_name: &str) -> Vec { /// radius, which is also the offset of the two pipe walls (they sit tangent to /// the end circles) — so it doubles as the rail offset for the double line. pub fn symbol_radius(doc: &CadDocument, block: Handle, scale: f64) -> f64 { - let Some(br) = doc.block_records.iter().find(|b| b.handle == block) else { + let mut r = 0.0_f64; + let depths = rustc_hash::FxHashMap::default(); + let graph = crate::scene::render_graph::RenderSceneGraph::new(doc, None, None, true, &depths); + let Some(block_use) = crate::scene::render_graph::block_use_from_handle( + doc, + block, + crate::scene::render_graph::BlockRole::LineStyleSymbol, + Vector3::ZERO, + ) else { return 0.0; }; - let bx = br.base_point.x; - let by = br.base_point.y; - let d = |x: f64, y: f64| ((x - bx).powi(2) + (y - by).powi(2)).sqrt(); - let mut r = 0.0_f64; - for eh in &br.entity_handles { - let Some(e) = doc.get_entity(*eh) else { - continue; - }; - let ext = match e { - EntityType::Ellipse(el) => { - let a = (el.major_axis.x.powi(2) + el.major_axis.y.powi(2)).sqrt(); - d(el.center.x, el.center.y) + a - } - EntityType::Circle(c) => d(c.center.x, c.center.y) + c.radius, - EntityType::Arc(a) => d(a.center.x, a.center.y) + a.radius, - EntityType::Line(l) => d(l.start.x, l.start.y).max(d(l.end.x, l.end.y)), - _ => 0.0, - }; - r = r.max(ext); - } + graph.walk_insert( + &block_use.insert, + block, + |_, _| true, + |entity, context| { + let mut placed = entity.clone(); + placed.apply_transform(&context.transform); + let distance = |x: f64, y: f64| x.hypot(y); + let extent = match &placed { + EntityType::Ellipse(ellipse) => { + distance(ellipse.center.x, ellipse.center.y) + + distance(ellipse.major_axis.x, ellipse.major_axis.y) + } + EntityType::Circle(circle) => { + distance(circle.center.x, circle.center.y) + circle.radius + } + EntityType::Arc(arc) => distance(arc.center.x, arc.center.y) + arc.radius, + EntityType::Line(line) => distance(line.start.x, line.start.y) + .max(distance(line.end.x, line.end.y)), + _ => crate::scene::convert::tess::entity_world_aabb_f64(&placed) + .map(|[min_x, min_y, max_x, max_y]| { + [ + distance(min_x, min_y), + distance(max_x, min_y), + distance(max_x, max_y), + distance(min_x, max_y), + ] + .into_iter() + .fold(0.0_f64, f64::max) + }) + .unwrap_or(0.0), + }; + r = r.max(extent); + }, + ); let s = if scale.abs() > 1e-9 { scale } else { 1.0 }; r / s } @@ -286,46 +309,50 @@ pub fn place_block_wires( world_per_pixel: Option, bg_color: [f32; 4], ) -> Vec { - let Some(br) = doc.block_records.iter().find(|b| b.handle == block) else { - return Vec::new(); - }; - // The symbol block's native geometry is drawn at 1 / scale_divisor (the - // divisor is read from the symbol component's leaf data). Scale about the - // origin, then translate the (scaled) base point to the placement point. let s = if scale_divisor.abs() > 1e-9 { 1.0 / scale_divisor } else { 1.0 }; - let scale = Transform::from_scale(s); - let offset = Vector3::new( - at[0] - br.base_point.x * s, - at[1] - br.base_point.y * s, - at[2] - br.base_point.z * s, - ); + let Some(mut block_use) = crate::scene::render_graph::block_use_from_handle( + doc, + block, + crate::scene::render_graph::BlockRole::LineStyleSymbol, + Vector3::new(at[0], at[1], at[2]), + ) else { + return Vec::new(); + }; + let insert = &mut block_use.insert; + insert.set_x_scale(s); + insert.set_y_scale(s); + insert.set_z_scale(s); + let depths = rustc_hash::FxHashMap::default(); + let graph = crate::scene::render_graph::RenderSceneGraph::new(doc, None, None, true, &depths) + .with_annotation_scale(anno_scale); let mut out = Vec::new(); - for eh in &br.entity_handles { - let Some(ent) = doc.get_entity(*eh) else { - continue; - }; - let mut clone = ent.clone(); - clone.as_entity_mut().apply_transform(&scale); - clone.as_entity_mut().translate(offset); - out.extend(super::tessellate::tessellate( - doc, - *eh, - &clone, - false, - color, - 0.0, - [0.0; 8], - line_weight_px, - anno_scale, - None, - world_per_pixel, - bg_color, - false, - )); - } + graph.walk_insert( + insert, + block, + |_, _| true, + |entity, context| { + let mut placed = entity.clone(); + placed.apply_transform(&context.transform); + out.extend(super::tessellate::tessellate( + doc, + entity.common().handle, + &placed, + false, + color, + 0.0, + [0.0; 8], + line_weight_px, + anno_scale, + None, + world_per_pixel, + bg_color, + false, + )); + }, + ); out } diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 0298fd522..f6a78a726 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -122,6 +122,7 @@ pub(crate) fn expand_block_object( .map(|record| record.flags.is_xref || record.flags.is_xref_overlay) .unwrap_or(false); let mut wires = cache::block_cache::expand_insert( + document, cache, insert, owner, @@ -146,7 +147,11 @@ pub(crate) fn expand_block_object( if options.apply_xclip { if let Some(filter) = pick::xclip::insert_spatial_filter(document, insert) { - let transform = crate::scene::render_graph::insert_transform(document, insert); + let transform = crate::scene::render_graph::insert_transform_at_scale( + document, + insert, + anno_scale, + ); let polygon = pick::xclip::world_clip_polygon_for_transform(filter, &transform); pick::xclip::clip_wires(&mut wires, &polygon); for wire in &mut wires { @@ -952,12 +957,17 @@ fn tessellate_entity_inner( // Render non-annotative dimensions from their stored picture block. // Rebuild geometry only when no usable block exists. if matches!(e, EntityType::Dimension(_)) { - if let Some(owned) = crate::scene::render_graph::owned_block_insert(document, e) - .filter(|owned| owned.render_picture) + if let Some(block_use) = crate::scene::render_graph::entity_render_block_uses( + document, + e, + anno_scale, + ) + .into_iter() + .find(|block_use| block_use.active) { let mut wires = expand_block_object( document, - &owned.insert, + &block_use.insert, h, sel, active_viewport, @@ -969,7 +979,7 @@ fn tessellate_entity_inner( world_per_pixel, pslt_factor, BlockObjectOptions { - suppress_root_points: owned.suppress_root_points, + suppress_root_points: block_use.suppress_root_points, ..BlockObjectOptions::default() }, ) @@ -1054,10 +1064,19 @@ fn tessellate_entity_inner( // When the block exists we render it directly. Same pattern as // Dimension's `block_name`. if let EntityType::Table(table) = e { - if let Some(owned) = crate::scene::render_graph::owned_block_insert(document, e) { + if let Some(block_use) = crate::scene::render_graph::entity_render_block_uses( + document, + e, + anno_scale, + ) + .into_iter() + .find(|block_use| { + block_use.active + && block_use.role == crate::scene::render_graph::BlockRole::TablePicture + }) { let mut wires = expand_block_object( document, - &owned.insert, + &block_use.insert, h, sel, active_viewport, @@ -1091,9 +1110,15 @@ fn tessellate_entity_inner( line_weight_px, table_anno, ); - for insert in - crate::entities::table::block_cell_inserts(table, document, table_anno) - { + for block_use in crate::scene::render_graph::entity_render_block_uses( + document, + e, + table_anno, + ) + .into_iter() + .filter(|block_use| { + block_use.role == crate::scene::render_graph::BlockRole::TableCell + }) { wires.extend(tessellate_entity( document, selected, @@ -1101,7 +1126,7 @@ fn tessellate_entity_inner( bg_color, 1.0, None, - &EntityType::Insert(insert), + &EntityType::Insert(block_use.insert), block_cache, view_aabb, world_per_pixel, diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 98c28613f..aa131ce88 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -2091,15 +2091,17 @@ fn custom_arrow_from_block( true, &depths, ); - let insert = acadrust::entities::Insert::new( - record.name.clone(), + let block_use = crate::scene::render_graph::block_use_from_handle( + doc, + record.handle, + crate::scene::render_graph::BlockRole::ArrowHead, acadrust::types::Vector3::ZERO, - ); + )?; let mut lines = Vec::new(); let mut fill = Vec::new(); let mut deferred_hatch = false; graph.walk_insert( - &insert, + &block_use.insert, record.handle, |_, _| true, |entity, context| { diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 4234bf8b6..908a59100 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1289,6 +1289,7 @@ impl Scene { all_visible, depth_map.as_ref(), ) + .with_annotation_scale(self.annotation_scale) .with_viewport(viewport); let mut hatch_block_memo = std::collections::HashMap::new(); let mut models = Vec::new(); @@ -1315,24 +1316,24 @@ impl Scene { return matches!(entity, EntityType::Insert(_)) && targets.contains(&common.handle); } - match entity { - EntityType::Insert(insert) => { + let block_uses: Vec<_> = crate::scene::render_graph::entity_render_block_uses( + &self.document, + entity, + 1.0, + ) + .into_iter() + .filter(|block_use| block_use.active) + .collect(); + if block_uses.is_empty() { + true + } else { + block_uses.into_iter().any(|block_use| { crate::scene::render_graph::block_contains_hatch( &self.document, - &insert.block_name, + &block_use.insert.block_name, &mut hatch_block_memo, ) - } - EntityType::Dimension(dimension) => { - let name = dimension.base().block_name.trim(); - !name.is_empty() - && crate::scene::render_graph::block_contains_hatch( - &self.document, - name, - &mut hatch_block_memo, - ) - } - _ => true, + }) } }, |entity, context| { @@ -1464,7 +1465,8 @@ impl Scene { annotation_scale_handle, all_visible, depth_map.as_ref(), - ); + ) + .with_annotation_scale(self.annotation_scale); let mut models = Vec::new(); let mut wipeout_sources = rustc_hash::FxHashMap::default(); graph.walk_root( diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 9596cc5fa..afa0e3be8 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -6082,6 +6082,7 @@ impl Scene { all_visible, depth_map.as_ref(), ) + .with_annotation_scale(self.annotation_scale) .with_viewport(viewport); let mut models = Vec::new(); let mut image_sources = rustc_hash::FxHashMap::default(); @@ -6739,6 +6740,7 @@ impl Scene { all_visible, depth_map.as_ref(), ) + .with_annotation_scale(self.annotation_scale) .with_viewport(viewport); let mut out = Vec::new(); graph.walk_root( @@ -7027,9 +7029,7 @@ impl Scene { self.belongs_to_visible_block(handle, common.owner_handle, self.interaction_block_handle()) } - /// Per-Insert hatch models in the current layout, keyed by the Insert - /// handle so a click on a block-internal hatch can select the parent - /// Insert; block-owned leaves resolve to their owning instance. + /// Instanced hatch models keyed by their block-backed host handle. pub fn insert_hatches_for_click(&self) -> Arc>> { let interaction_block = self.interaction_block_handle(); @@ -7042,10 +7042,15 @@ impl Scene { epoch, CACHE_CATEGORY_INSERT_HATCH, |handle| { - matches!( - self.document.get_entity(handle), - Some(EntityType::Insert(_)) - ) + self.document.get_entity(handle).is_some_and(|entity| { + render_graph::entity_render_block_uses( + &self.document, + entity, + self.annotation_scale, + ) + .into_iter() + .any(|block_use| block_use.active) + }) }, ) { @@ -7082,22 +7087,22 @@ impl Scene { annotation_scale_handle, all_visible, depth_map.as_ref(), - ); + ) + .with_annotation_scale(self.annotation_scale); for entity in self.document.entities() { let contextual = crate::scene::annotative::entity_for_annotation_context( &self.document, entity, annotation_scale_handle, ); - let EntityType::Insert(ins) = contextual.as_ref() else { - continue; - }; - if ins.common.invisible - || self.entity_temporarily_hidden(ins.common.handle) - || layer_hidden(&ins.common.layer) + let host = contextual.as_ref(); + let common = host.common(); + if common.invisible + || self.entity_temporarily_hidden(common.handle) + || layer_hidden(&common.layer) || crate::scene::annotative::annotative_offscale_for( &self.document, - &ins.common, + common, annotation_scale_handle, all_visible, ) @@ -7105,58 +7110,67 @@ impl Scene { continue; } if !self.belongs_to_visible_block( - ins.common.handle, - ins.common.owner_handle, + common.handle, + common.owner_handle, interaction_block, ) { continue; } - if !render_graph::block_contains_hatch( + for block_use in render_graph::entity_render_block_uses( &self.document, - &ins.block_name, - &mut hatch_memo, - ) { - continue; - } - graph.walk_insert( - ins, - ins.common.handle, - |_, _| true, - |entity, context| { - let EntityType::Hatch(source) = entity else { - return; - }; - let mut placed = EntityType::Hatch(source.clone()); - placed.apply_transform(&context.transform); - let EntityType::Hatch(hatch) = placed else { - return; - }; - let color = crate::scene::view::render::adapt_to_bg( - context.style_for(&self.document, entity).0, - self.current_bg(), - ); - let Some(mut model) = Self::hatch_model_from_dxf(&hatch, color) - else { - return; - }; - for clip in &context.clips { - let clip: Vec<[f32; 2]> = clip - .iter() - .map(|point| [point[0] as f32, point[1] as f32]) - .collect(); - let clipped = pick::xclip::clip_hatch_boundary( - &model.boundary, - model.world_origin, - &clip, - ); - if clipped.is_empty() { + host, + self.annotation_scale, + ) + .into_iter() + .filter(|block_use| block_use.active) + { + if !render_graph::block_contains_hatch( + &self.document, + &block_use.insert.block_name, + &mut hatch_memo, + ) { + continue; + } + graph.walk_insert( + &block_use.insert, + common.handle, + |_, _| true, + |entity, context| { + let EntityType::Hatch(source) = entity else { return; + }; + let mut placed = EntityType::Hatch(source.clone()); + placed.apply_transform(&context.transform); + let EntityType::Hatch(hatch) = placed else { + return; + }; + let color = crate::scene::view::render::adapt_to_bg( + context.style_for(&self.document, entity).0, + self.current_bg(), + ); + let Some(mut model) = Self::hatch_model_from_dxf(&hatch, color) + else { + return; + }; + for clip in &context.clips { + let clip: Vec<[f32; 2]> = clip + .iter() + .map(|point| [point[0] as f32, point[1] as f32]) + .collect(); + let clipped = pick::xclip::clip_hatch_boundary( + &model.boundary, + model.world_origin, + &clip, + ); + if clipped.is_empty() { + return; + } + model.boundary = Arc::new(clipped); } - model.boundary = Arc::new(clipped); - } - out.entry(ins.common.handle).or_default().push(model); - }, - ); + out.entry(common.handle).or_default().push(model); + }, + ); + } } let arc = Arc::new(out); *self.insert_hatch_cache.borrow_mut() = @@ -8872,11 +8886,7 @@ impl Scene { let mut roots: HashMap> = HashMap::default(); let mut parents: HashMap> = HashMap::default(); for entity in self.document.entities() { - let EntityType::Insert(insert) = entity else { - continue; - }; - let target = normalize_name(&insert.block_name); - let common = &insert.common; + let common = entity.common(); let owner = if common.owner_handle.is_null() { membership .get(&common.handle) @@ -8885,13 +8895,19 @@ impl Scene { } else { common.owner_handle }; - if owner.is_null() || layout_blocks.contains(&owner) { - roots.entry(target).or_default().insert(common.handle); - } else if let Some(parent) = block_names.get(&owner) { - parents.entry(target).or_default().insert(parent.clone()); + for block_use in render_graph::entity_block_uses(&self.document, entity, 1.0) { + let target = normalize_name(&block_use.insert.block_name); + if target.is_empty() { + continue; + } + if owner.is_null() || layout_blocks.contains(&owner) { + roots.entry(target).or_default().insert(common.handle); + } else if let Some(parent) = block_names.get(&owner) { + parents.entry(target).or_default().insert(parent.clone()); + } } } - // Propagate top-level INSERT users through nested block references. + // Propagate root users through nested block references. // Fixed-point form is cycle-safe and block graphs are normally shallow. let mut changed = true; while changed { diff --git a/src/scene/render_graph.rs b/src/scene/render_graph.rs index 4bbbb7371..dd737aaef 100644 --- a/src/scene/render_graph.rs +++ b/src/scene/render_graph.rs @@ -259,6 +259,7 @@ pub struct RenderSceneGraph<'a> { document: &'a CadDocument, frozen_layers: Option<&'a FxHashSet>, annotation_scale_handle: Option, + annotation_scale: f32, all_visible: bool, depths: &'a FxHashMap, viewport: Option, @@ -276,6 +277,7 @@ impl<'a> RenderSceneGraph<'a> { document, frozen_layers, annotation_scale_handle, + annotation_scale: 1.0, all_visible, depths, viewport: None, @@ -287,6 +289,11 @@ impl<'a> RenderSceneGraph<'a> { self } + pub fn with_annotation_scale(mut self, annotation_scale: f32) -> Self { + self.annotation_scale = annotation_scale.max(1.0e-6); + self + } + /// Walk direct root entities and every referenced block subtree. `visible` /// can add session-only rules such as isolate/preview hiding; returning /// false for an Insert removes its whole subtree. @@ -397,7 +404,12 @@ impl<'a> RenderSceneGraph<'a> { }; for offset in array_offsets(insert) { - let local = insert_instance_transform(self.document, insert, offset); + let local = insert_instance_transform( + self.document, + insert, + offset, + self.annotation_scale, + ); let transform = local.then(&parent.transform); let mut context = parent.clone(); context.transform = transform; @@ -449,7 +461,7 @@ impl<'a> RenderSceneGraph<'a> { if context.nesting_depth > 32 { return; } - let Some(record) = self.document.block_records.get(block_name) else { + let Some(record) = block_record_by_name(self.document, block_name) else { return; }; for &handle in &record.entity_handles { @@ -499,16 +511,20 @@ impl<'a> RenderSceneGraph<'a> { V: FnMut(&EntityType, &RenderContext) -> bool, F: FnMut(&EntityType, &RenderContext), { - let Some(owned) = owned_block_insert(self.document, entity) else { - return; - }; - if stack - .iter() - .any(|name| name.eq_ignore_ascii_case(&owned.insert.block_name)) + for block_use in entity_render_block_uses(self.document, entity, self.annotation_scale) + .into_iter() + .filter(|block_use| block_use.active) { - return; + if stack + .iter() + .any(|name| name.eq_ignore_ascii_case(&block_use.insert.block_name)) + { + continue; + } + stack.push(block_use.insert.block_name.clone()); + self.walk_insert_instances(&block_use.insert, context, visible, leaf); + stack.pop(); } - self.walk_insert_instances(&owned.insert, context, visible, leaf); } fn document_visible(&self, entity: &EntityType) -> bool { @@ -540,11 +556,16 @@ impl<'a> RenderSceneGraph<'a> { pub fn block_base_point(document: &CadDocument, block_name: &str) -> Vector3 { document .block_records - .get(block_name) - .and_then(|record| document.get_entity(record.block_entity_handle)) - .and_then(|entity| match entity { - EntityType::Block(block) => Some(block.base_point), - _ => None, + .iter() + .find(|record| record.name.eq_ignore_ascii_case(block_name)) + .map(|record| { + document + .get_entity(record.block_entity_handle) + .and_then(|entity| match entity { + EntityType::Block(block) => Some(block.base_point), + _ => None, + }) + .unwrap_or(record.base_point) }) .unwrap_or(Vector3::ZERO) } @@ -555,64 +576,388 @@ pub fn insert_transform(document: &CadDocument, insert: &Insert) -> Transform { .then(&insert.get_transform()) } -pub struct OwnedBlockInsert { - pub insert: Insert, - pub suppress_root_points: bool, - pub render_picture: bool, +pub fn insert_transform_at_scale( + document: &CadDocument, + insert: &Insert, + annotation_scale: f32, +) -> Transform { + let mut transform = insert_transform(document, insert); + if (annotation_scale - 1.0).abs() > 1.0e-6 + && insert + .common + .extended_data + .get_record("AcAnnotativeData") + .is_some() + { + let point = insert.insert_point; + let scale = Transform::from_translation(Vector3::new(-point.x, -point.y, -point.z)) + .then(&Transform::from_scale(annotation_scale as f64)) + .then(&Transform::from_translation(Vector3::new( + point.x, point.y, point.z, + ))); + transform = transform.then(&scale); + } + transform } -pub fn owned_block_insert( +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlockRole { + Insert, + DimensionPicture, + TablePicture, + TableCell, + MultiLeaderContent, + LineStyleSymbol, + ArrowHead, +} + +#[derive(Clone)] +pub struct BlockUse { + pub block: Handle, + pub insert: Insert, + pub role: BlockRole, + /// Whether this edge belongs to the active render representation. Inactive + /// edges remain dependencies so cache invalidation and purge stay complete. + pub active: bool, + pub replaces_host_wire: bool, + pub suppress_root_points: bool, +} + +pub(crate) fn block_record_by_name<'a>( + document: &'a CadDocument, + name: &str, +) -> Option<&'a acadrust::tables::BlockRecord> { + document.block_records.get(name).or_else(|| { + document + .block_records + .iter() + .find(|record| record.name.eq_ignore_ascii_case(name)) + }) +} + +fn block_use( + document: &CadDocument, + mut insert: Insert, + role: BlockRole, + active: bool, + replaces_host_wire: bool, + suppress_root_points: bool, +) -> BlockUse { + let block = + block_record_by_name(document, &insert.block_name).map_or(Handle::NULL, |record| { + insert.block_name.clone_from(&record.name); + record.handle + }); + BlockUse { + block, + insert, + role, + active, + replaces_host_wire, + suppress_root_points, + } +} + +pub fn block_use_from_handle( + document: &CadDocument, + block: Handle, + role: BlockRole, + insertion: Vector3, +) -> Option { + let record = document + .block_records + .iter() + .find(|record| record.handle == block)?; + Some(block_use( + document, + Insert::new(record.name.clone(), insertion), + role, + true, + false, + false, + )) +} + +fn push_dependency_use( + uses: &mut Vec, + document: &CadDocument, + block: Handle, + role: BlockRole, +) { + if block.is_null() + || uses + .iter() + .any(|block_use| block_use.block == block && block_use.role == role) + { + return; + } + if let Some(mut block_use) = + block_use_from_handle(document, block, role, Vector3::ZERO) + { + block_use.active = false; + uses.push(block_use); + } +} + +/// Every direct block edge owned by an entity. One host may have multiple +/// edges, such as block-valued table cells. Representation flags let renderers +/// choose the active picture while dependency consumers keep the full graph. +fn entity_owned_block_uses( document: &CadDocument, entity: &EntityType, -) -> Option { - let (insert, suppress_root_points, render_picture) = match entity { + annotation_scale: f32, +) -> Vec { + match entity { + EntityType::Insert(insert) => vec![block_use( + document, + insert.clone(), + BlockRole::Insert, + true, + true, + false, + )], EntityType::Dimension(dimension) => { let name = dimension.base().block_name.trim(); - let record = document - .block_records - .iter() - .find(|record| record.name.eq_ignore_ascii_case(name))?; - if record.entity_handles.is_empty() { - return None; + if let Some(record) = block_record_by_name(document, name) + .filter(|record| !record.entity_handles.is_empty()) + { + let mut insert = Insert::new(record.name.clone(), Vector3::ZERO); + insert.common = dimension.base().common.clone(); + let picture = !crate::scene::annotative::is_annotative(document, entity); + vec![block_use( + document, + insert, + BlockRole::DimensionPicture, + picture, + picture, + true, + )] + } else { + Vec::new() } - let mut insert = Insert::new(record.name.clone(), Vector3::ZERO); - insert.common = dimension.base().common.clone(); - ( - insert, - true, - !crate::scene::annotative::is_annotative(document, entity), - ) } EntityType::Table(table) => { - let record = table.block_record_handle.and_then(|handle| { + let picture_record = table.block_record_handle.and_then(|handle| { document .block_records .iter() .find(|record| record.handle == handle) - })?; - if record.entity_handles.is_empty() { - return None; + .filter(|record| !record.entity_handles.is_empty()) + }); + let mut uses = Vec::new(); + if let Some(record) = picture_record { + let mut insert = Insert::new(record.name.clone(), table.insertion_point); + insert.rotation = table + .horizontal_direction + .y + .atan2(table.horizontal_direction.x); + insert.common = table.common.clone(); + uses.push(block_use( + document, + insert, + BlockRole::TablePicture, + true, + true, + false, + )); } - let mut insert = Insert::new(record.name.clone(), table.insertion_point); - insert.rotation = table - .horizontal_direction - .y - .atan2(table.horizontal_direction.x); - insert.common = table.common.clone(); - (insert, false, true) + let cells_active = picture_record.is_none(); + uses.extend( + crate::entities::table::block_cell_inserts(table, document, annotation_scale) + .into_iter() + .map(|mut insert| { + insert + .common + .extended_data + .remove_record("AcAnnotativeData"); + block_use( + document, + insert, + BlockRole::TableCell, + cells_active, + false, + false, + ) + }), + ); + uses } - EntityType::MultiLeader(multileader) => ( - crate::entities::multileader::block_content_insert(document, multileader)?, - false, - true, - ), - _ => return None, - }; - Some(OwnedBlockInsert { - insert, - suppress_root_points, - render_picture, - }) + EntityType::MultiLeader(multileader) => { + crate::entities::multileader::block_content_insert(document, multileader) + .map(|mut insert| { + insert + .common + .extended_data + .remove_record("AcAnnotativeData"); + vec![block_use( + document, + insert, + BlockRole::MultiLeaderContent, + true, + false, + false, + )] + }) + .unwrap_or_default() + } + _ => Vec::new(), + } +} + +pub fn entity_render_block_uses( + document: &CadDocument, + entity: &EntityType, + annotation_scale: f32, +) -> Vec { + entity_owned_block_uses(document, entity, annotation_scale) +} + +pub fn entity_block_uses( + document: &CadDocument, + entity: &EntityType, + annotation_scale: f32, +) -> Vec { + let mut uses = entity_owned_block_uses(document, entity, annotation_scale); + + let line_type = crate::scene::view::render::linetype_name_for(document, entity); + for symbol in crate::scene::convert::dgn_linestyle::symbol_blocks(document, line_type) { + push_dependency_use( + &mut uses, + document, + symbol.block, + BlockRole::LineStyleSymbol, + ); + } + + match entity { + EntityType::Dimension(dimension) => { + let style_name = dimension.base().style_name.trim(); + if let Some(style) = document.dim_styles.iter().find(|style| { + style.name.eq_ignore_ascii_case(style_name) + || (style_name.is_empty() && style.name.eq_ignore_ascii_case("Standard")) + }) { + let style = crate::entities::dimension::resolved_dimension_style( + style, + dimension, + document, + ); + for handle in [style.dimblk, style.dimblk1, style.dimblk2, style.dimldrblk] { + push_dependency_use( + &mut uses, + document, + handle, + BlockRole::ArrowHead, + ); + } + for name in [style.dimblk_name, style.dimblk1_name, style.dimblk2_name] { + if let Some(record) = block_record_by_name(document, &name) { + push_dependency_use( + &mut uses, + document, + record.handle, + BlockRole::ArrowHead, + ); + } + } + } + } + EntityType::Leader(leader) => { + let style = document.dim_styles.iter().find(|style| { + style.name.eq_ignore_ascii_case(&leader.dimension_style) + || (leader.dimension_style.trim().is_empty() + && style.name.eq_ignore_ascii_case("Standard")) + }); + let handle = crate::entities::dim_override::handle( + &leader.common.extended_data, + crate::entities::dim_override::DIMLDRBLK, + ) + .or_else(|| style.map(|style| style.dimldrblk)); + if let Some(handle) = handle { + push_dependency_use( + &mut uses, + document, + handle, + BlockRole::ArrowHead, + ); + } + } + EntityType::MultiLeader(multileader) => { + if let Some(handle) = multileader.arrowhead_handle { + push_dependency_use( + &mut uses, + document, + handle, + BlockRole::ArrowHead, + ); + } + for line in multileader + .context + .leader_roots + .iter() + .flat_map(|root| &root.lines) + { + if let Some(handle) = line.arrowhead_handle { + push_dependency_use( + &mut uses, + document, + handle, + BlockRole::ArrowHead, + ); + } + } + } + _ => {} + } + uses +} + +/// Blocks referenced by document-level styles rather than a concrete entity. +pub fn document_block_uses(document: &CadDocument) -> Vec { + let mut uses = Vec::new(); + for style in document.dim_styles.iter() { + for handle in [style.dimblk, style.dimblk1, style.dimblk2, style.dimldrblk] { + push_dependency_use(&mut uses, document, handle, BlockRole::ArrowHead); + } + for name in [&style.dimblk_name, &style.dimblk1_name, &style.dimblk2_name] { + if let Some(record) = block_record_by_name(document, name) { + push_dependency_use( + &mut uses, + document, + record.handle, + BlockRole::ArrowHead, + ); + } + } + } + for object in document.objects.values() { + let acadrust::objects::ObjectType::MultiLeaderStyle(style) = object else { + continue; + }; + if let Some(handle) = style.arrowhead_handle { + push_dependency_use(&mut uses, document, handle, BlockRole::ArrowHead); + } + if let Some(handle) = style.block_content_handle { + push_dependency_use( + &mut uses, + document, + handle, + BlockRole::MultiLeaderContent, + ); + } + } + for line_type in document.line_types.iter() { + for symbol in + crate::scene::convert::dgn_linestyle::symbol_blocks(document, &line_type.name) + { + push_dependency_use( + &mut uses, + document, + symbol.block, + BlockRole::LineStyleSymbol, + ); + } + } + uses } pub fn array_offsets(insert: &Insert) -> Vec<[f64; 3]> { @@ -636,8 +981,9 @@ pub fn insert_instance_transform( document: &CadDocument, insert: &Insert, offset: [f64; 3], + annotation_scale: f32, ) -> Transform { - let transform = insert_transform(document, insert); + let transform = insert_transform_at_scale(document, insert, annotation_scale); if offset == [0.0; 3] { transform } else { @@ -651,51 +997,26 @@ pub fn block_contains_hatch( block_name: &str, memo: &mut std::collections::HashMap, ) -> bool { - if let Some(&contains) = memo.get(block_name) { + let key = block_name.to_ascii_lowercase(); + if let Some(&contains) = memo.get(&key) { return contains; } - memo.insert(block_name.to_string(), false); - let contains = document - .block_records - .get(block_name) - .is_some_and(|record| { - record - .entity_handles - .iter() - .any(|&handle| match document.get_entity(handle) { - Some(EntityType::Hatch(_)) => true, - Some(EntityType::Insert(insert)) => { - block_contains_hatch(document, &insert.block_name, memo) - } - Some(EntityType::Dimension(dimension)) => { - let name = dimension.base().block_name.trim(); - !name.is_empty() && block_contains_hatch(document, name, memo) - } - Some(EntityType::Table(table)) => table - .block_record_handle - .and_then(|handle| { - document - .block_records - .iter() - .find(|record| record.handle == handle) - }) - .is_some_and(|record| { - block_contains_hatch(document, &record.name, memo) - }), - Some(EntityType::MultiLeader(multileader)) => multileader - .block_content_handle - .and_then(|handle| { - document - .block_records - .iter() - .find(|record| record.handle == handle) - }) - .is_some_and(|record| { - block_contains_hatch(document, &record.name, memo) - }), - _ => false, - }) - }); - memo.insert(block_name.to_string(), contains); + memo.insert(key.clone(), false); + let contains = block_record_by_name(document, block_name).is_some_and(|record| { + record + .entity_handles + .iter() + .any(|&handle| match document.get_entity(handle) { + Some(EntityType::Hatch(_)) => true, + Some(entity) => entity_render_block_uses(document, entity, 1.0) + .into_iter() + .filter(|block_use| block_use.active) + .any(|block_use| { + block_contains_hatch(document, &block_use.insert.block_name, memo) + }), + _ => false, + }) + }); + memo.insert(key, contains); contains }