From abb4f4bf568ca2ab66dca2e29d91db746d522ae5 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 30 Aug 2026 23:30:14 +0300 Subject: [PATCH] fix(blocks): unify nested object transforms Route block-backed entities through one local geometry and accumulated transform path. Refs #864 --- src/entities/multileader.rs | 191 ++++++------ src/scene/cache/block_cache.rs | 183 ++++++------ src/scene/convert/tess.rs | 512 +++++++++++++++------------------ src/scene/render_graph.rs | 172 +++++------ 4 files changed, 471 insertions(+), 587 deletions(-) diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 49b4a8127..639e2eb7f 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1700,6 +1700,59 @@ impl crate::entities::traits::Transformable for MultiLeader { } } +pub(crate) fn block_content_insert( + document: &acadrust::CadDocument, + ml: &MultiLeader, +) -> Option { + if ml.content_type != LeaderContentType::Block || !ml.context.has_block_contents { + return None; + } + let record = ml.block_content_handle.and_then(|handle| { + document + .block_records + .iter() + .find(|record| record.handle == handle) + })?; + let mut insertion = ml.context.block_content_location; + if ml.block_connection_type == BlockContentConnectionType::BlockExtents { + let mut bounds = [f32::MAX, f32::MAX, f32::MIN, f32::MIN]; + for handle in &record.entity_handles { + if let Some(entity) = document.get_entity(*handle) { + let aabb = crate::scene::convert::tess::entity_aabb(entity); + if aabb != crate::scene::model::wire_model::WireModel::UNBOUNDED_AABB { + bounds[0] = bounds[0].min(aabb[0]); + bounds[1] = bounds[1].min(aabb[1]); + bounds[2] = bounds[2].max(aabb[2]); + bounds[3] = bounds[3].max(aabb[3]); + } + } + } + if bounds[0] <= bounds[2] && bounds[1] <= bounds[3] { + let to_block = Transform::rotation(-ml.block_rotation); + let from_left = ml.context.leader_roots.first().map_or(true, |root| { + to_block.apply_vector([root.direction.x, root.direction.y])[0] >= 0.0 + }); + let anchor_x = if from_left { bounds[0] } else { bounds[2] } as f64; + let anchor_y = ((bounds[1] + bounds[3]) * 0.5) as f64; + let local_x = (anchor_x - record.base_point.x) * ml.block_scale.x; + let local_y = (anchor_y - record.base_point.y) * ml.block_scale.y; + let rotated = + Transform::rotation(ml.block_rotation).apply_vector([local_x, local_y]); + insertion.x -= rotated[0]; + insertion.y -= rotated[1]; + } + } + let mut insert = acadrust::entities::Insert::new(record.name.clone(), insertion); + insert.set_x_scale(ml.block_scale.x); + insert.set_y_scale(ml.block_scale.y); + insert.set_z_scale(ml.block_scale.z); + insert.rotation = ml.block_rotation; + insert.normal = ml.context.block_content_normal; + insert.common = ml.common.clone(); + insert.common.color = ml.block_content_color.clone(); + Some(insert) +} + /// Per-entity tessellation entry for `MultiLeader`. Returns multiple /// `WireModel`s (leader/dogleg + arrow fill, optional block content, /// text strokes, frame, background fill) so each piece can carry its @@ -1713,6 +1766,10 @@ pub trait MultiLeaderTess { entity_color: [f32; 4], line_weight_px: f32, anno_scale: f32, + active_viewport: Option, + annotation_scale_handle: Option, + block_cache: Option<&crate::scene::cache::block_cache::BlockCache>, + view_aabb: Option<[f32; 4]>, world_per_pixel: Option, bg_color: [f32; 4], ) -> Vec; @@ -1727,11 +1784,15 @@ impl MultiLeaderTess for MultiLeader { entity_color: [f32; 4], line_weight_px: f32, anno_scale: f32, - _world_per_pixel: Option, + active_viewport: Option, + annotation_scale_handle: Option, + block_cache: Option<&crate::scene::cache::block_cache::BlockCache>, + view_aabb: Option<[f32; 4]>, + world_per_pixel: Option, bg_color: [f32; 4], ) -> Vec { use crate::scene::convert::tessellate::{ - append_arrow, arrow_from_block, color_or_inherit, tessellate, ArrowKind, DimGeom, + append_arrow, arrow_from_block, color_or_inherit, ArrowKind, DimGeom, }; use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel}; use glam::Vec3; @@ -2043,117 +2104,25 @@ impl MultiLeaderTess for MultiLeader { fill_tris_low: Vec::new(), }); - // ── Block content ─────────────────────────────────────────────────────── - // When content_type == Block, the MultiLeader displays a block reference - // at block_content_location with the recorded rotation/scale. A - // synthetic Insert supplies that context to the shared scene graph; - // every child then uses its normal tessellator. - if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents { - let block_record = match ml.block_content_handle { - Some(h) if !h.is_null() => document - .block_records - .iter() - .find(|br| br.handle == h) - .cloned(), - _ => None, - }; - if let Some(block_record) = block_record { - let block_name = block_record.name.clone(); - let block_color = if selected { - line_color - } else { - color_or_inherit(&ml.block_content_color, entity_color) - }; - let mut insertion = ml.context.block_content_location; - if ml.block_connection_type == BlockContentConnectionType::BlockExtents { - let mut bounds = [f32::MAX, f32::MAX, f32::MIN, f32::MIN]; - for entity_handle in &block_record.entity_handles { - if let Some(entity) = document.get_entity(*entity_handle) { - let aabb = crate::scene::convert::tess::entity_aabb(entity); - if aabb != WireModel::UNBOUNDED_AABB { - bounds[0] = bounds[0].min(aabb[0]); - bounds[1] = bounds[1].min(aabb[1]); - bounds[2] = bounds[2].max(aabb[2]); - bounds[3] = bounds[3].max(aabb[3]); - } - } - } - if bounds[0] <= bounds[2] && bounds[1] <= bounds[3] { - let to_block = Transform::rotation(-ml.block_rotation); - let from_left = ml - .context - .leader_roots - .first() - .map_or(true, |root| { - to_block.apply_vector([ - root.direction.x, - root.direction.y, - ])[0] - >= 0.0 - }); - let anchor_x = if from_left { bounds[0] } else { bounds[2] } as f64; - let anchor_y = ((bounds[1] + bounds[3]) * 0.5) as f64; - let local_x = - (anchor_x - block_record.base_point.x) * ml.block_scale.x; - let local_y = - (anchor_y - block_record.base_point.y) * ml.block_scale.y; - let rotated = Transform::rotation(ml.block_rotation) - .apply_vector([local_x, local_y]); - insertion.x -= rotated[0]; - insertion.y -= rotated[1]; - } - } - let mut synth_ins = acadrust::entities::Insert::new(block_name, insertion); - synth_ins.set_x_scale(ml.block_scale.x); - synth_ins.set_y_scale(ml.block_scale.y); - synth_ins.set_z_scale(ml.block_scale.z); - synth_ins.rotation = ml.block_rotation; - synth_ins.common.layer = ml.common.layer.clone(); - let depths = rustc_hash::FxHashMap::default(); - let graph = crate::scene::render_graph::RenderSceneGraph::new( + if let Some(insert) = block_content_insert(document, ml) { + wires.extend( + crate::scene::convert::tess::expand_block_object( document, - None, - None, - true, - &depths, - ); - graph.walk_insert( - &synth_ins, + &insert, handle, - |_, _| true, - |entity, context| { - let mut placed = entity.clone(); - placed.apply_transform(&context.transform); - let mut sub_wires = tessellate( - document, - handle, - &placed, - selected, - block_color, - leader_pat_len, - leader_pat, - leader_lw_px, - 1.0, - None, - None, - bg_color, - false, - ); - for w in &mut sub_wires { - w.name = name.clone(); - } - wires.extend(sub_wires); - }, - ); - // Block attributes attached to the multileader — render each as - // its own attribute entity at WCS location like INSERT does. - for ba in &ml.block_attributes { - let _ = ba; // BlockAttribute carries only the value override - // string; we'd need the AttributeDefinition handle - // to materialise it as ATTRIB geometry. Skipped - // until that wiring exists. - } - } + selected, + active_viewport, + bg_color, + 1.0, + annotation_scale_handle, + block_cache, + view_aabb, + world_per_pixel, + 1.0, + crate::scene::convert::tess::BlockObjectOptions::default(), + ) + .wires, + ); } // ── Text strokes / frame / background fill ────────────────────────────── diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 73f3dbca5..47deee40b 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -42,6 +42,7 @@ pub struct LocalWire { /// Low-bit residual paired with `points` so block-instance wires keep /// sub-f32 precision once the renderer translates them to world space. pub points_low: Vec<[f32; 3]>, + pub is_point: bool, pub point_marker: Option, /// SDF glyph quads for block-internal text, in block-local coordinates. /// Non-empty only when SDF text is on and this sub is a TEXT/MTEXT. The @@ -126,6 +127,7 @@ pub struct NestedRef { /// The nested INSERT's own signed draw-order rank in (-1,1) within the /// parent block — narrows the depth sub-range its children may occupy. pub local_rank: f32, + pub suppress_root_points: bool, } #[derive(Clone, Debug)] @@ -172,6 +174,7 @@ struct ExpansionPrototypeKey { insert_style: Vec, selected: bool, is_xref: bool, + suppress_root_points: bool, } #[derive(Debug)] @@ -207,11 +210,8 @@ impl BlockCache { self.defns.get(block_name) } - /// Build (flat) defns only for block records actually referenced by - /// Inserts in the document — transitively, so nested-insert targets are - /// included too. The Model_Space / Paper_Space block_records are skipped - /// because their entities are emitted as top-level wires, not via the - /// cache. + /// Build definitions referenced by block-backed entities. Space records stay + /// roots so their direct entities preserve individual selection handles. pub fn build( doc: &CadDocument, anno_scale: f32, @@ -395,16 +395,24 @@ impl BlockCache { } } -/// Walk all entities + all block_record contents collecting every distinct -/// `block_name` that appears in an Insert (transitively). +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) + } +} + +/// Collect every referenced block-backed object. fn collect_referenced_blocks(doc: &CadDocument) -> Vec { let mut seen: HashSet = HashSet::default(); let mut queue: Vec = Vec::new(); for entity in doc.entities() { - if let EntityType::Insert(ins) = entity { - if seen.insert(ins.block_name.clone()) { - queue.push(ins.block_name.clone()); + if let Some(name) = block_object_name(doc, entity) { + if seen.insert(name.clone()) { + queue.push(name); } } } @@ -416,9 +424,9 @@ fn collect_referenced_blocks(doc: &CadDocument) -> Vec { let Some(entity) = doc.get_entity(eh) else { continue; }; - if let EntityType::Insert(ins) = entity { - if seen.insert(ins.block_name.clone()) { - queue.push(ins.block_name.clone()); + if let Some(name) = block_object_name(doc, entity) { + if seen.insert(name.clone()) { + queue.push(name); } } } @@ -435,11 +443,13 @@ fn referenced_block_tree(doc: &CadDocument, root: &str) -> Vec { continue; }; for &handle in &record.entity_handles { - let Some(EntityType::Insert(insert)) = doc.get_entity(handle) else { + let Some(entity) = doc.get_entity(handle) else { continue; }; - if seen.insert(insert.block_name.clone()) { - queue.push(insert.block_name.clone()); + if let Some(name) = block_object_name(doc, entity) { + if seen.insert(name.clone()) { + queue.push(name); + } } } } @@ -510,6 +520,20 @@ 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; + } + } match entity { EntityType::Block(_) | EntityType::BlockEnd(_) => continue, // A non-constant ATTDEF is only a template — the insert supplies an @@ -543,91 +567,30 @@ fn build_defn( subs.push(LocalSub::Wire(wire)); } } - // A nested table's stored graphics use the same scene-graph - // traversal as top-level table content. The resulting leaf wires - // remain local to this cached definition; the parent instance - // transform is composed later by the cache expansion. EntityType::Table(table) => { - let baked = table.block_record_handle.and_then(|handle| { - doc.block_records - .iter() - .find(|record| record.handle == handle) - }); - if let Some(record) = baked.filter(|record| { - !record.entity_handles.is_empty() - }) { - let table_plot_l0 = crate::scene::view::render::is_effective_layer_zero( - &table.common.layer, - ); - let table_plot_visible = doc - .layers - .get(&table.common.layer) - .map(|layer| layer.is_plottable) - .unwrap_or(true); - let mut insert = acadrust::entities::Insert::new( - record.name.clone(), - table.insertion_point, - ); - insert.rotation = table - .horizontal_direction - .y - .atan2(table.horizontal_direction.x); - insert.common = table.common.clone(); - let graph = crate::scene::render_graph::RenderSceneGraph::new( - doc, - None, - annotation_scale_handle, - all_visible, - depth_map, - ) - .with_viewport(viewport); - graph.walk_insert( + 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, - table.common.handle, - |_, _| true, - |leaf, context| { - let mut placed = leaf.clone(); - placed.apply_transform(&context.transform); - for mut wire in tessellate_sub_local( - doc, - &placed, - anno_scale, - annotation_scale_handle, - bg_color, - viewport, - depth_map, - ) { - wire.plot_visible &= table_plot_l0 || table_plot_visible; - wire.plot_l0 |= table_plot_l0; - subs.push(LocalSub::Wire(wire)); - } - }, - ); - } else { - 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, - ))); - } + ))); } } _ => { @@ -695,6 +658,7 @@ fn build_nested_ref( local_rank: depth_map .get(&nested_ins.common.handle.value()) .map_or(0.0, |d| d[0]), + suppress_root_points: false, } } @@ -850,6 +814,7 @@ fn tessellate_sub_local( result.push(LocalWire { points: wire.points, points_low: wire.points_low, + is_point: matches!(sub, EntityType::Point(_)), point_marker: wire.point_marker, text_verts: wire.text_verts, key_vertices: wire.key_vertices, @@ -1005,6 +970,9 @@ pub fn expand_insert( // Current annotation scale. An annotative block scales as one uniform unit // about its insertion point; a non-annotative block is unaffected. anno_scale: f32, + // Dimension picture blocks store definition POINTs at their root. They are + // metadata, not visible geometry; nested POINTs remain regular block data. + suppress_root_points: bool, ) -> Option> { let defn = cache.defn(&ins.block_name)?; let base = defn.base_point; @@ -1045,6 +1013,7 @@ pub fn expand_insert( is_xref, bg_color, anno_scale, + suppress_root_points, )) } else { None @@ -1148,6 +1117,7 @@ pub fn expand_insert( &mut first_batches, &mut visited, 0, + suppress_root_points, (0.0, 1.0), ); let mut first = first_batches.finalize(&name, selected, bg_color); @@ -1185,7 +1155,16 @@ pub fn expand_insert( )); translation.then(&xform) }; - expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0, (0.0, 1.0)); + expand_defn( + defn, + &base_xform, + &ctx, + &mut batches, + &mut visited, + 0, + suppress_root_points, + (0.0, 1.0), + ); } let mut wires = batches.finalize(&name, selected, bg_color); if let Some(guard) = prototype_guard.as_mut() { @@ -1230,6 +1209,7 @@ fn expansion_prototype_key( is_xref: bool, bg_color: [f32; 4], anno_scale: f32, + suppress_root_points: bool, ) -> ExpansionPrototypeKey { let matrix = &transform.matrix.m; let linear = [ @@ -1262,6 +1242,7 @@ fn expansion_prototype_key( insert_style, selected, is_xref, + suppress_root_points, } } @@ -1384,6 +1365,7 @@ fn nested_prototype_key( transform: &Transform, ctx: &ExpandCtx<'_>, depth_scale: f32, + suppress_root_points: bool, ) -> NestedPrototypeKey { let matrix = &transform.matrix.m; let linear = [ @@ -1411,6 +1393,7 @@ fn nested_prototype_key( selected: ctx.selected, is_xref: ctx.is_xref, depth_scale: depth_scale.to_bits(), + suppress_root_points, } } @@ -1527,6 +1510,7 @@ struct NestedPrototypeKey { selected: bool, is_xref: bool, depth_scale: u32, + suppress_root_points: bool, } #[derive(Clone, Debug)] @@ -1766,6 +1750,7 @@ fn expand_defn( out: &mut Batches, visited: &mut Vec, depth: usize, + suppress_root_points: bool, // Block-local depth sub-range `(base, scale)` accumulated through nested // inserts: a child at rank r lands at `base + r * scale`, all within // (-1,1) of the top-level insert. Seeded `(0.0, 1.0)` by `expand_insert`. @@ -1778,6 +1763,9 @@ fn expand_defn( for sub in &defn.subs { match sub { LocalSub::Wire(lw) => { + if suppress_root_points && lw.is_point { + continue; + } // `lw.aabb_local` is in the defn's offset frame; re-add // `defn_lo` (in f64) before composing with `accum_xform` // so culling uses correct world-space corners. @@ -1926,6 +1914,7 @@ fn expand_defn( &mut sub, visited, depth + 1, + nref.suppress_root_points, nested_range, ); let mut wires = sub.finalize("", ctx.selected, ctx.bg_color); @@ -1958,6 +1947,7 @@ fn expand_defn( &composed, &inner_ctx, nested_range.1, + nref.suppress_root_points, ); if let Some(cached) = out.nested_prototypes.get(&key).cloned() { let delta = [ @@ -1986,6 +1976,7 @@ fn expand_defn( &mut sub, visited, depth + 1, + nref.suppress_root_points, nested_range, ); let mut wires = sub.finalize("", ctx.selected, ctx.bg_color); diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 02d0dcd54..0298fd522 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -17,6 +17,172 @@ fn fade_if_locked( } } +#[derive(Clone, Copy)] +struct BlockObjectStyle { + color: [f32; 4], + aci: u8, + pattern_length: f32, + pattern: [f32; 8], + line_weight_px: f32, + layer: view::render::InheritStyle, + layer_aci: u8, + layer_plottable: bool, +} + +pub(crate) struct ExpandedBlockObject { + pub(crate) wires: Vec, + style: BlockObjectStyle, + is_xref: bool, +} + +#[derive(Clone, Copy, Default)] +pub(crate) struct BlockObjectOptions { + pub(crate) suppress_root_points: bool, + pub(crate) apply_xclip: bool, +} + +fn block_object_style( + document: &acadrust::CadDocument, + insert: &acadrust::entities::Insert, + active_viewport: Option, + bg_color: [f32; 4], +) -> BlockObjectStyle { + let entity = EntityType::Insert(insert.clone()); + let (color, pattern_length, pattern, line_weight_px, aci) = + view::render::render_style_for_viewport(document, &entity, active_viewport); + let mut layer = view::render::layer_render_style_viewport( + document, + &insert.common.layer, + active_viewport, + ); + layer.color = view::render::adapt_to_bg(layer.color, bg_color); + let layer_aci = document + .layers + .get(&insert.common.layer) + .and_then(|layer| match &layer.color { + acadrust::types::Color::Index(index) => Some(*index), + _ => None, + }) + .unwrap_or(0); + let layer_plottable = document + .layers + .get(&insert.common.layer) + .map(|layer| layer.is_plottable) + .unwrap_or(true); + BlockObjectStyle { + color: view::render::adapt_to_bg(color, bg_color), + aci, + pattern_length, + pattern, + line_weight_px, + layer, + layer_aci, + layer_plottable, + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn expand_block_object( + document: &acadrust::CadDocument, + insert: &acadrust::entities::Insert, + owner: Handle, + selected: bool, + active_viewport: Option, + bg_color: [f32; 4], + anno_scale: f32, + annotation_scale_handle: Option, + block_cache: Option<&cache::block_cache::BlockCache>, + view_aabb: Option<[f32; 4]>, + world_per_pixel: Option, + pslt_factor: f32, + options: BlockObjectOptions, +) -> ExpandedBlockObject { + let style = block_object_style(document, insert, active_viewport, bg_color); + let fallback_depths = rustc_hash::FxHashMap::default(); + let fallback_cache; + let cache = match block_cache { + Some(cache) if cache.defn(&insert.block_name).is_some() => cache, + _ => { + fallback_cache = cache::block_cache::BlockCache::build_for_block( + document, + &insert.block_name, + anno_scale, + annotation_scale_handle, + true, + bg_color, + active_viewport, + &fallback_depths, + ); + &fallback_cache + } + }; + let is_xref = document + .block_records + .get(&insert.block_name) + .map(|record| record.flags.is_xref || record.flags.is_xref_overlay) + .unwrap_or(false); + let mut wires = cache::block_cache::expand_insert( + cache, + insert, + owner, + style.color, + style.aci, + style.pattern_length, + style.pattern, + style.line_weight_px, + style.layer, + style.layer_aci, + style.layer_plottable, + selected, + pslt_factor, + view_aabb, + world_per_pixel, + is_xref, + bg_color, + anno_scale, + options.suppress_root_points, + ) + .unwrap_or_default(); + + 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 polygon = pick::xclip::world_clip_polygon_for_transform(filter, &transform); + pick::xclip::clip_wires(&mut wires, &polygon); + for wire in &mut wires { + if let Some(mut instance) = wire.render_instance { + instance.source_id = cache.clip_source_id( + instance.source_id, + &polygon, + instance.translation, + ); + wire.render_instance = Some(instance); + } + } + let frame_mode = + crate::scene::frame::mode(document, crate::scene::frame::FrameKind::Xclip); + if polygon.len() >= 3 { + let mut frame = pick::xclip::frame_wire( + &polygon, + owner.value().to_string(), + style.color, + selected, + style.line_weight_px, + ); + frame.display_visible = frame_mode != 0; + frame.plot_visible = frame_mode == 1; + wires.push(frame); + } + } + } + + ExpandedBlockObject { + wires, + style, + is_xref, + } +} + /// The section mark's viewing direction (paper-space, unit), derived entirely /// from the decoded model-documentation view graph: /// @@ -785,99 +951,32 @@ fn tessellate_entity_inner( // Render non-annotative dimensions from their stored picture block. // Rebuild geometry only when no usable block exists. - if let EntityType::Dimension(dim) = e { - let baked = Some(dim.base().block_name.trim()) - .filter(|name| !name.is_empty()) - .filter(|_| !crate::scene::annotative::is_annotative(document, e)) - .and_then(|name| { - document - .block_records - .iter() - .find(|record| record.name.eq_ignore_ascii_case(name)) - }) - .filter(|record| !record.entity_handles.is_empty()); - if let Some(record) = baked { - // The block's contents are already in world space and its base - // point is the origin, so the instance that carries them is the - // identity — it exists only to reuse the graph's ByBlock / layer-0 - // colour resolution and its handling of anything nested inside. - let mut insert = acadrust::entities::Insert::new( - record.name.clone(), - acadrust::types::Vector3::ZERO, - ); - insert.common = dim.base().common.clone(); - - let depths = rustc_hash::FxHashMap::default(); - let graph = crate::scene::render_graph::RenderSceneGraph::new( + if matches!(e, EntityType::Dimension(_)) { + if let Some(owned) = crate::scene::render_graph::owned_block_insert(document, e) + .filter(|owned| owned.render_picture) + { + let mut wires = expand_block_object( document, - None, - annotation_scale_handle, - true, - &depths, - ); - let mut wires = Vec::new(); - graph.walk_insert( - &insert, + &owned.insert, h, - |sub, context| { - // Direct POINTs are dimension definition markers. - context.insert_path.len() != 1 || !matches!(sub, EntityType::Point(_)) + sel, + active_viewport, + bg_color, + anno_scale, + annotation_scale_handle, + block_cache, + view_aabb, + world_per_pixel, + pslt_factor, + BlockObjectOptions { + suppress_root_points: owned.suppress_root_points, + ..BlockObjectOptions::default() }, - |sub, context| { - let has_book_color = view::render::has_resolved_book_color(document, sub); - let color_byblock = - !has_book_color && sub.common().color == acadrust::types::Color::ByBlock; - let color_layer0 = !has_book_color - && view::render::is_effective_layer_zero(&sub.common().layer) - && sub.common().color == acadrust::types::Color::ByLayer; - let contrast_bg = convert::tessellate::text_contrast_background(sub, bg_color); - let sub_color = view::render::adapt_to_bg( - view::render::render_style_for_viewport( - document, - sub, - active_viewport, - ) - .0, - contrast_bg, - ); - let style = context.style_for(document, sub); - let resolved_color = view::render::adapt_to_bg(style.0, contrast_bg); - let mut placed = sub.clone(); - placed.apply_transform(&context.transform); - let sub_wires = tessellate_entity( - document, - selected, - active_viewport, - bg_color, - anno_scale, - annotation_scale_handle, - &placed, - block_cache, - view_aabb, - world_per_pixel, - paper_space, - ); - for mut wire in sub_wires { - // Every wire answers to the dimension, so a pick selects - // the dimension rather than one of its pieces. - wire.name = h.value().to_string(); - if sel { - wire.selected = true; - wire.color = WireModel::SELECTED; - } else if (color_byblock || color_layer0) && wire.color == sub_color { - wire.color = resolved_color; - wire.aci = style.4; - } - wires.push(wire); - } - }, - ); + ) + .wires; if !wires.is_empty() { let aabb = entity_aabb(e); for wire in &mut wires { - // As below: only stroke / fill wires take the whole-dimension - // box. An SDF-text wire keeps its own tight glyph box so the - // text pick area hugs the text. if !wire.points.is_empty() || !wire.fill_tris.is_empty() { set_wire_aabb(wire, aabb); } @@ -927,6 +1026,10 @@ fn tessellate_entity_inner( entity_color, line_weight_px, anno_scale, + active_viewport, + annotation_scale_handle, + block_cache, + view_aabb, world_per_pixel, bg_color, ); @@ -951,92 +1054,31 @@ 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(record) = table.block_record_handle.and_then(|handle| { - document - .block_records - .iter() - .find(|record| record.handle == handle) - }) { - if !record.entity_handles.is_empty() { - let mut insert = - acadrust::entities::Insert::new( - record.name.clone(), - table.insertion_point, - ); - insert.rotation = table - .horizontal_direction - .y - .atan2(table.horizontal_direction.x); - insert.common = table.common.clone(); - - let depths = rustc_hash::FxHashMap::default(); - let graph = crate::scene::render_graph::RenderSceneGraph::new( - document, - None, - annotation_scale_handle, - true, - &depths, - ); - let mut wires = Vec::new(); - graph.walk_insert( - &insert, - h, - |_, _| true, - |sub, context| { - let has_book_color = - view::render::has_resolved_book_color(document, sub); - let color_byblock = !has_book_color - && sub.common().color - == acadrust::types::Color::ByBlock; - let color_layer0 = !has_book_color - && view::render::is_effective_layer_zero( - &sub.common().layer, - ) - && sub.common().color - == acadrust::types::Color::ByLayer; - let style = context.style_for(document, sub); - let mut placed = sub.clone(); - placed.apply_transform(&context.transform); - let sub_wires = tessellate_entity( - document, - selected, - active_viewport, - bg_color, - anno_scale, - annotation_scale_handle, - &placed, - block_cache, - view_aabb, - world_per_pixel, - paper_space, - ); - for mut wire in sub_wires { - wire.name = h.value().to_string(); - if color_byblock { - wire.color = if sel { - WireModel::SELECTED - } else { - view::render::adapt_to_bg(style.0, bg_color) - }; - wire.aci = style.4; - } else if color_layer0 && !sel { - wire.color = - view::render::adapt_to_bg(style.0, bg_color); - wire.aci = style.4; - } - wires.push(wire); - } - }, - ); - if !wires.is_empty() { - let aabb = entity_aabb(e); - for wire in &mut wires { - if !wire.points.is_empty() || !wire.fill_tris.is_empty() { - set_wire_aabb(wire, aabb); - } + if let Some(owned) = crate::scene::render_graph::owned_block_insert(document, e) { + let mut wires = expand_block_object( + document, + &owned.insert, + h, + sel, + active_viewport, + bg_color, + anno_scale, + annotation_scale_handle, + block_cache, + view_aabb, + world_per_pixel, + pslt_factor, + BlockObjectOptions::default(), + ) + .wires; + if !wires.is_empty() { + let aabb = entity_aabb(e); + for wire in &mut wires { + if !wire.points.is_empty() || !wire.fill_tris.is_empty() { + set_wire_aabb(wire, aabb); } - return wires; } + return wires; } } @@ -1079,34 +1121,6 @@ fn tessellate_entity_inner( } if let EntityType::Insert(ins) = e { - // Resolve the INSERT's own style so ByBlock sub-entities can inherit it. - let (ins_color, ins_pat_len, ins_pat, ins_lw_px, ins_aci) = - view::render::render_style_for_viewport(document, e, active_viewport); - let ins_color = view::render::adapt_to_bg(ins_color, bg_color); - // Resolve the INSERT's *layer* style — the layer-0 inheritance target - // for sub-entities on layer "0" with ByLayer properties (#221). - let ins_layer = { - let mut s = view::render::layer_render_style_viewport( - document, - &ins.common.layer, - active_viewport, - ); - s.color = view::render::adapt_to_bg(s.color, bg_color); - s - }; - let ins_layer_aci = document - .layers - .get(&ins.common.layer) - .and_then(|layer| match &layer.color { - acadrust::types::Color::Index(index) => Some(*index), - _ => None, - }) - .unwrap_or(0); - let ins_layer_plottable = document - .layers - .get(&ins.common.layer) - .map(|layer| layer.is_plottable) - .unwrap_or(true); let ip = glam::Vec3::new( (ins.insert_point.x) as f32, (ins.insert_point.y) as f32, @@ -1146,83 +1160,27 @@ fn tessellate_entity_inner( fill_tris_low: Vec::new(), }; - let fallback_depths = rustc_hash::FxHashMap::default(); - let fallback_cache; - let cache = match block_cache { - Some(cache) if cache.defn(&ins.block_name).is_some() => cache, - _ => { - fallback_cache = cache::block_cache::BlockCache::build_for_block( - document, - &ins.block_name, - anno_scale, - annotation_scale_handle, - true, - bg_color, - active_viewport, - &fallback_depths, - ); - &fallback_cache - } - }; - let is_xref = document - .block_records - .get(&ins.block_name) - .map(|record| record.flags.is_xref || record.flags.is_xref_overlay) - .unwrap_or(false); - let mut wires = cache::block_cache::expand_insert( - cache, + let expanded = expand_block_object( + document, ins, h, - ins_color, - ins_aci, - ins_pat_len, - ins_pat, - ins_lw_px, - ins_layer, - ins_layer_aci, - ins_layer_plottable, sel, - pslt_factor, - view_aabb, - world_per_pixel, - is_xref, + active_viewport, bg_color, anno_scale, - ) - .unwrap_or_default(); - - if let Some(filter) = pick::xclip::insert_spatial_filter(document, ins) { - let transform = crate::scene::render_graph::insert_transform(document, ins); - let polygon = - pick::xclip::world_clip_polygon_for_transform(filter, &transform); - pick::xclip::clip_wires(&mut wires, &polygon); - for wire in &mut wires { - if let Some(mut instance) = wire.render_instance { - instance.source_id = cache.clip_source_id( - instance.source_id, - &polygon, - instance.translation, - ); - wire.render_instance = Some(instance); - } - } - let frame_mode = crate::scene::frame::mode( - document, - crate::scene::frame::FrameKind::Xclip, - ); - if polygon.len() >= 3 { - let mut frame = pick::xclip::frame_wire( - &polygon, - h.value().to_string(), - ins_color, - sel, - ins_lw_px, - ); - frame.display_visible = frame_mode != 0; - frame.plot_visible = frame_mode == 1; - wires.push(frame); - } - } + annotation_scale_handle, + block_cache, + view_aabb, + world_per_pixel, + pslt_factor, + BlockObjectOptions { + apply_xclip: true, + ..BlockObjectOptions::default() + }, + ); + let style = expanded.style; + let is_xref = expanded.is_xref; + let mut wires = expanded.wires; crate::entities::insert::append_insert_attribute_wires( &mut wires, @@ -1230,12 +1188,12 @@ fn tessellate_entity_inner( ins, h, sel, - ins_color, - ins_pat_len, - ins_pat, - ins_lw_px, - ins_layer, - ins_layer_plottable, + style.color, + style.pattern_length, + style.pattern, + style.line_weight_px, + style.layer, + style.layer_plottable, bg_color, is_xref, pslt_factor, diff --git a/src/scene/render_graph.rs b/src/scene/render_graph.rs index 235b37c80..4bbbb7371 100644 --- a/src/scene/render_graph.rs +++ b/src/scene/render_graph.rs @@ -157,35 +157,6 @@ impl BlockStyle { InsertStyleSpec::new(document, insert, viewport).resolve(parent) } - pub fn for_owned( - document: &CadDocument, - entity: &EntityType, - parent: Option, - viewport: Option, - ) -> Self { - let on_layer0 = is_effective_layer_zero(&entity.common().layer); - let insert = parent - .map(|style| style.resolve(document, entity, viewport)) - .unwrap_or_else(|| render_style_for_viewport(document, entity, viewport)); - Self { - insert, - layer0: if on_layer0 { - parent - .map(|style| style.layer0) - .unwrap_or_else(|| layer_render_style_viewport(document, &entity.common().layer, viewport)) - } else { - layer_render_style_viewport(document, &entity.common().layer, viewport) - }, - layer0_aci: if on_layer0 { - parent - .map(|style| style.layer0_aci) - .unwrap_or_else(|| layer_aci(document, &entity.common().layer)) - } else { - layer_aci(document, &entity.common().layer) - }, - } - } - pub fn resolve( self, document: &CadDocument, @@ -528,81 +499,16 @@ impl<'a> RenderSceneGraph<'a> { V: FnMut(&EntityType, &RenderContext) -> bool, F: FnMut(&EntityType, &RenderContext), { - match entity { - EntityType::Dimension(dimension) => { - let block_name = dimension.base().block_name.trim(); - if block_name.is_empty() - || stack - .iter() - .any(|name| name.eq_ignore_ascii_case(block_name)) - { - return; - } - let placement = - Transform::from_translation(dimension.base().insertion_point) - .then(&context.transform); - let mut owned = context.clone(); - owned.transform = placement; - if owned.root_handle.is_null() { - owned.root_handle = entity.common().handle; - } - owned.block_style = Some(BlockStyle::for_owned( - self.document, - entity, - context.block_style, - self.viewport, - )); - owned.nesting_depth += 1; - stack.push(block_name.to_string()); - self.walk_block(block_name, &owned, visible, leaf, stack); - stack.pop(); - } - EntityType::Table(table) => { - let Some(record) = table.block_record_handle.and_then(|handle| { - self.document - .block_records - .iter() - .find(|record| record.handle == handle) - }) else { - return; - }; - 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(); - self.walk_insert_instances(&insert, context, visible, leaf); - } - EntityType::MultiLeader(multileader) - if matches!( - multileader.content_type, - acadrust::entities::LeaderContentType::Block - ) && multileader.context.has_block_contents => - { - let Some(record) = multileader.block_content_handle.and_then(|handle| { - self.document - .block_records - .iter() - .find(|record| record.handle == handle) - }) else { - return; - }; - let mut insert = Insert::new( - record.name.clone(), - multileader.context.block_content_location, - ); - insert.common = multileader.common.clone(); - insert.common.color = multileader.block_content_color.clone(); - insert.set_x_scale(multileader.block_scale.x); - insert.set_y_scale(multileader.block_scale.y); - insert.set_z_scale(multileader.block_scale.z); - insert.rotation = multileader.block_rotation; - insert.normal = multileader.context.block_content_normal; - self.walk_insert_instances(&insert, context, visible, leaf); - } - _ => {} + 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)) + { + return; } + self.walk_insert_instances(&owned.insert, context, visible, leaf); } fn document_visible(&self, entity: &EntityType) -> bool { @@ -649,6 +555,66 @@ 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 owned_block_insert( + document: &CadDocument, + entity: &EntityType, +) -> Option { + let (insert, suppress_root_points, render_picture) = match entity { + 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; + } + 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| { + document + .block_records + .iter() + .find(|record| record.handle == handle) + })?; + if record.entity_handles.is_empty() { + return None; + } + 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) + } + EntityType::MultiLeader(multileader) => ( + crate::entities::multileader::block_content_insert(document, multileader)?, + false, + true, + ), + _ => return None, + }; + Some(OwnedBlockInsert { + insert, + suppress_root_points, + render_picture, + }) +} + pub fn array_offsets(insert: &Insert) -> Vec<[f64; 3]> { if !insert.is_minsert() { return vec![[0.0; 3]];