fix(blocks): unify block-backed content

Route pictures, table cells, leader content, symbols, and arrows through one dependency and transform graph.\n\nRefs #864
This commit is contained in:
Hakan Seven committed 2026-08-31 01:09:32 +03:00
1 parent 3a5591e036
commit 19629d1246
12 files changed
+902 -557

No files matched your search

+22 -70
View File
@@ -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<String> = {
let doc = &self.tabs[i].scene.document;
let by_handle: rustc_hash::FxHashMap<acadrust::Handle, String> = 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")
+55 -43
View File
@@ -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<EntityType> {
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<WireModel>,
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)
};
+10 -2
View File
@@ -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,
+12 -6
View File
@@ -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);
+119 -142
View File
@@ -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<LocalWire>,
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<LocalSub>,
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<BlockDefn>> {
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<String, usize> = 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<String> {
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<String> {
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<String> {
/// Collect block definitions needed by rendered representations.
fn collect_referenced_blocks(doc: &CadDocument, anno_scale: f32) -> Vec<String> {
let mut seen: HashSet<String> = HashSet::default();
let mut queue: Vec<String> = 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<String> {
seen.into_iter().collect()
}
fn referenced_block_tree(doc: &CadDocument, root: &str) -> Vec<String> {
fn referenced_block_tree(doc: &CadDocument, root: &str, anno_scale: f32) -> Vec<String> {
let mut seen: HashSet<String> = 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<Handle>,
depth_map: &HashMap<u64, [f32; 2]>,
) -> 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<Handle>,
depth_map: &HashMap<u64, [f32; 2]>,
) -> 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<Vec<WireModel>> {
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.
+30 -33
View File
@@ -73,6 +73,7 @@ fn block_entity_transform(
block_name: &str,
target: Handle,
visited: &mut Vec<String>,
annotation_scale: f32,
) -> Option<acadrust::types::Transform> {
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 {
+85 -58
View File
@@ -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<f64> {
/// 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<f32>,
bg_color: [f32; 4],
) -> Vec<WireModel> {
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
}
+36 -11
View File
@@ -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,
+6 -4
View File
@@ -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| {
+17 -15
View File
@@ -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(
+86 -70
View File
@@ -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<HashMap<Handle, Vec<HatchModel>>> {
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<String, HashSet<Handle>> = HashMap::default();
let mut parents: HashMap<String, HashSet<String>> = 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 {
+424 -103
View File
@@ -259,6 +259,7 @@ pub struct RenderSceneGraph<'a> {
document: &'a CadDocument,
frozen_layers: Option<&'a FxHashSet<Handle>>,
annotation_scale_handle: Option<Handle>,
annotation_scale: f32,
all_visible: bool,
depths: &'a FxHashMap<u64, [f32; 2]>,
viewport: Option<Handle>,
@@ -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<BlockUse> {
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<BlockUse>,
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<OwnedBlockInsert> {
let (insert, suppress_root_points, render_picture) = match entity {
annotation_scale: f32,
) -> Vec<BlockUse> {
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<BlockUse> {
entity_owned_block_uses(document, entity, annotation_scale)
}
pub fn entity_block_uses(
document: &CadDocument,
entity: &EntityType,
annotation_scale: f32,
) -> Vec<BlockUse> {
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<BlockUse> {
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<String, bool>,
) -> 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
}