diff --git a/src/entities/insert.rs b/src/entities/insert.rs index 17fa1be69..37f7dcd7f 100644 --- a/src/entities/insert.rs +++ b/src/entities/insert.rs @@ -189,16 +189,19 @@ pub(crate) fn insert_attribute_entities( document: &acadrust::CadDocument, insert: &acadrust::entities::Insert, annotation_scale: f32, + scale_policy: crate::scene::BlockScalePolicy, ) -> Vec { let visibility = document.header.attribute_visibility; if visibility == 0 || insert.attributes.is_empty() { return Vec::new(); } - let block_scale = if insert - .common - .extended_data - .get_record("AcAnnotativeData") - .is_some() + let block_scale = if scale_policy + == crate::scene::BlockScalePolicy::FromInsert + && insert + .common + .extended_data + .get_record("AcAnnotativeData") + .is_some() { annotation_scale as f64 } else { @@ -253,59 +256,75 @@ pub(crate) fn append_insert_attribute_wires( pslt_factor: f32, anno_scale: f32, ) { - 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( + let attributes = insert_attribute_entities( + document, + ins, + anno_scale, + crate::scene::BlockScalePolicy::FromInsert, + ); + for offset in crate::scene::render_graph::array_offsets(ins) { + let delta = crate::scene::render_graph::insert_instance_translation_delta( document, - &attr_entity, - ins_color, - ins_pat_len, - ins_pat, - ins_lw_px, - ins_layer, + ins, + offset, + anno_scale, + crate::scene::BlockScalePolicy::FromInsert, ); - let sub_color = render::adapt_to_bg(sub_color, bg_color); - let sub_color = if is_xref && !sel { - block_cache::fade_toward_bg(sub_color, bg_color) - } else { - sub_color - }; - let attr_plottable = if render::is_effective_layer_zero(&attr.layer) { - ins_layer_plottable - } else { - document - .layers - .get(&attr.layer) - .map(|layer| layer.is_plottable) - .unwrap_or(true) - }; - let attr_plottable = ins_layer_plottable && attr_plottable; - let sub_aabb = crate::scene::entity_aabb(&attr_entity); - let mut attr_wires = tessellate::tessellate( - document, - insert_handle, - &attr_entity, - sel, - sub_color, - sub_plen * pslt_factor, - sub_pat.map(|v| v * pslt_factor), - sub_lw_px, - // Neutral: the attribute is already scaled as part of the block unit - // above, so the text path must not scale it a second time. - 1.0, - None, - None, - bg_color, - false, - ); - // Use the INSERT's handle so selection / picking groups attribute - // text with the parent insert instead of treating it as a stray text. - for w in &mut attr_wires { - w.name = insert_handle.value().to_string(); - w.aci = sub_aci; - w.aabb = sub_aabb; - w.plot_visible &= attr_plottable; + for source in &attributes { + let mut attr_entity = source.clone(); + if delta != Vector3::ZERO { + attr_entity.apply_transform(&Transform::from_translation(delta)); + } + 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, + ins_color, + ins_pat_len, + ins_pat, + ins_lw_px, + ins_layer, + ); + let sub_color = render::adapt_to_bg(sub_color, bg_color); + let sub_color = if is_xref && !sel { + block_cache::fade_toward_bg(sub_color, bg_color) + } else { + sub_color + }; + let attr_plottable = if render::is_effective_layer_zero(&attr.layer) { + ins_layer_plottable + } else { + document + .layers + .get(&attr.layer) + .map(|layer| layer.is_plottable) + .unwrap_or(true) + }; + let attr_plottable = ins_layer_plottable && attr_plottable; + let sub_aabb = crate::scene::entity_aabb(&attr_entity); + let mut attr_wires = tessellate::tessellate( + document, + insert_handle, + &attr_entity, + sel, + sub_color, + sub_plen * pslt_factor, + sub_pat.map(|v| v * pslt_factor), + sub_lw_px, + 1.0, + None, + None, + bg_color, + false, + ); + for w in &mut attr_wires { + w.name = insert_handle.value().to_string(); + w.aci = sub_aci; + w.aabb = sub_aabb; + w.plot_visible &= attr_plottable; + } + wires.extend(attr_wires); } - wires.extend(attr_wires); } } diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index c3737e28d..c1c4bcc65 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -2129,7 +2129,10 @@ impl MultiLeaderTess for MultiLeader { view_aabb, world_per_pixel, 1.0, - crate::scene::convert::tess::BlockObjectOptions::default(), + crate::scene::convert::tess::BlockObjectOptions { + scale_policy: block_use.scale_policy, + ..crate::scene::convert::tess::BlockObjectOptions::default() + }, ) .wires, ); diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index aaf12f4da..5e04a9613 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -140,12 +140,16 @@ pub enum LocalSub { Nested(NestedRef), } +#[derive(Clone, Copy, Debug, Default)] +pub struct BlockMetrics { + pub aabb_local: [f32; 4], + pub has_point_marker: bool, +} + #[derive(Clone, Debug, Default)] pub struct BlockDefn { pub subs: Vec, - pub has_point_marker: bool, - /// Union of direct and nested geometry in definition coordinates. - pub aabb_local: [f32; 4], + pub metrics: BlockMetrics, /// Raw entity count of the source block record (`entity_handles.len()`). /// Divisor for nested depth composition: a nested insert's children get a /// sub-range of `parent_scale / (child_count + 1)`, shared with the scene @@ -247,8 +251,8 @@ impl BlockCache { // by-name references (`LocalSub::Nested`), never expanded here, so a // block's build never depends on another block's defn. That makes the // builds embarrassingly parallel over the read-only `doc` — no - // topological ordering required. `compute_block_aabbs` stays a serial - // post-pass (it resolves nested references and is comparatively cheap). + // topological ordering required. Metrics are resolved after all + // definitions exist. cache.defns = referenced .par_iter() .map(|name| { @@ -267,7 +271,7 @@ impl BlockCache { ) }) .collect(); - cache.compute_block_aabbs(&referenced); + cache.compute_block_metrics(&referenced); cache } @@ -304,106 +308,73 @@ impl BlockCache { ) }) .collect(); - cache.compute_block_aabbs(&referenced); + cache.compute_block_metrics(&referenced); cache } - /// Compute and store the `aabb_local` for every cached defn. Direct wires - /// contribute their own aabb_local; nested INSERT references look up the - /// nested defn (already cached) and transform its aabb_local by the - /// nested Insert's transform before unioning. - /// - /// Run as a post-pass so it doesn't matter which order build_defn was - /// called in. Cycle guard: a self-referential block keeps an empty AABB - /// (will fail every frustum test → not emitted, which is correct). - fn compute_block_aabbs(&mut self, names: &[String]) { + /// Resolve recursive bounds and point-marker presence. + fn compute_block_metrics(&mut self, names: &[String]) { use crate::par::prelude::*; - // Phase 1 (parallel, read-only): each defn's union AABB is resolved - // by `defn_aabb_recursive`, which only *reads* `self.defns` (the map - // is fully built by now). There's no memoization, so a defn shared by - // many parents is re-walked per parent — real work on block-heavy - // drawings — and the per-name walks are independent, so they fan out. + // Definitions are complete, so each recursive read can run independently. let this: &Self = self; - let resolved: Vec<(&String, [f32; 4], bool)> = names + let resolved: Vec<(&String, BlockMetrics)> = names .par_iter() .map(|name| { let mut visited: Vec = Vec::new(); - let aabb = this.defn_aabb_recursive(name, &mut visited); - visited.clear(); - let has_marker = this.defn_has_point_marker_recursive(name, &mut visited); - (name, aabb, has_marker) + (name, this.defn_metrics_recursive(name, &mut visited)) }) .collect(); - // Phase 2 (serial): store the AABB back into each defn. - for (name, aabb, has_marker) in resolved { + for (name, metrics) in resolved { if let Some(defn_arc) = self.defns.get_mut(name) { let mut defn = (**defn_arc).clone(); - defn.aabb_local = aabb; - defn.has_point_marker = has_marker; + defn.metrics = metrics; *defn_arc = Arc::new(defn); } } } - fn defn_has_point_marker_recursive( + fn defn_metrics_recursive( &self, block_name: &str, visited: &mut Vec, - ) -> bool { + ) -> BlockMetrics { if visited.iter().any(|name| name == block_name) { - return false; + return BlockMetrics::default(); } let Some(defn) = self.defns.get(block_name) else { - return false; + return BlockMetrics::default(); }; visited.push(block_name.to_string()); - let found = defn.subs.iter().any(|sub| match sub { - LocalSub::Wire(wire) => wire.point_marker.is_some(), - LocalSub::Nested(insert) => { - insert - .attachments - .iter() - .any(|wire| wire.point_marker.is_some()) - || self.defn_has_point_marker_recursive(&insert.block_name, visited) - } - }); - visited.pop(); - found - } - - /// Returns the union AABB for `block_name`'s defn, expressed in **that - /// defn's offset frame** (so its caller can store it in - /// `BlockDefn.aabb_local` without a coordinate-frame mismatch). - /// - /// LocalWire contributions are already in the parent defn's offset - /// frame. Nested-INSERT contributions live in the *child* defn's offset - /// frame, so we re-add `child.local_offset` (f64), apply the nested - /// Insert's transform to get parent-native coordinates, then subtract - /// `parent.local_offset` to land back in the parent's offset frame. - fn defn_aabb_recursive(&self, block_name: &str, visited: &mut Vec) -> [f32; 4] { - if visited.iter().any(|n| n == block_name) { - return [0.0, 0.0, 0.0, 0.0]; - } - let Some(defn) = self.defns.get(block_name) else { - return [0.0, 0.0, 0.0, 0.0]; - }; - visited.push(block_name.to_string()); - let mut acc = [0.0_f32, 0.0, 0.0, 0.0]; + let mut metrics = BlockMetrics::default(); for sub in &defn.subs { - let aabb = match sub { - LocalSub::Wire(lw) => lw.aabb_local, - LocalSub::Nested(nref) => { - let nested = self.defn_aabb_recursive(&nref.block_name, visited); - nref.attachments.iter().fold( - transform_aabb_xy(nested, &nref.xform), - |aabb, wire| aabb_union(aabb, wire.aabb_local), - ) + match sub { + LocalSub::Wire(wire) => { + metrics.aabb_local = aabb_union(metrics.aabb_local, wire.aabb_local); + metrics.has_point_marker |= wire.point_marker.is_some(); } - }; - acc = aabb_union(acc, aabb); + LocalSub::Nested(nref) => { + let nested = self.defn_metrics_recursive(&nref.block_name, visited); + metrics.has_point_marker |= nested.has_point_marker; + for offset in &nref.instance_offsets { + let transform = nested_instance_transform(nref, *offset); + metrics.aabb_local = aabb_union( + metrics.aabb_local, + transform_aabb_xy(nested.aabb_local, &transform), + ); + let attachment_transform = nested_attachment_transform(nref, *offset); + for wire in &nref.attachments { + metrics.aabb_local = aabb_union( + metrics.aabb_local, + transform_aabb_xy(wire.aabb_local, &attachment_transform), + ); + metrics.has_point_marker |= wire.point_marker.is_some(); + } + } + } + } } visited.pop(); - acc + metrics } } @@ -479,6 +450,21 @@ fn layer_hidden(doc: &CadDocument, layer: &str) -> bool { .unwrap_or(false) } +fn nested_instance_transform(nref: &NestedRef, offset: [f64; 3]) -> Transform { + if offset == [0.0; 3] { + nref.xform.clone() + } else { + Transform::from_translation(Vector3::new(offset[0], offset[1], offset[2])) + .then(&nref.xform) + } +} + +fn nested_attachment_transform(nref: &NestedRef, offset: [f64; 3]) -> Transform { + let base = nref.xform.apply(Vector3::ZERO); + let instance = nested_instance_transform(nref, offset).apply(Vector3::ZERO); + Transform::from_translation(instance - base) +} + fn build_defn( doc: &CadDocument, block_name: &str, @@ -549,6 +535,7 @@ fn build_defn( for block_use in &block_uses { let mut nested = build_nested_ref( &block_use.insert, + block_use.scale_policy, doc, anno_scale, bg_color, @@ -597,14 +584,14 @@ fn build_defn( } BlockDefn { subs, - has_point_marker: false, - aabb_local: [0.0; 4], + metrics: BlockMetrics::default(), child_count: br.entity_handles.len(), } } fn build_nested_ref( nested_ins: &acadrust::entities::Insert, + scale_policy: crate::scene::BlockScalePolicy, doc: &CadDocument, anno_scale: f32, bg_color: [f32; 4], @@ -614,10 +601,11 @@ fn build_nested_ref( // 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_at_scale( + let xform = crate::scene::render_graph::insert_transform_with_policy( doc, nested_ins, anno_scale, + scale_policy, ); let clip_poly = crate::scene::pick::xclip::insert_spatial_filter(doc, nested_ins) .map(|filter| { @@ -635,6 +623,7 @@ fn build_nested_ref( doc, nested_ins, anno_scale, + scale_policy, ) .into_iter() .flat_map(|attribute| { @@ -977,12 +966,18 @@ 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, + scale_policy: crate::scene::BlockScalePolicy, // 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 xform = crate::scene::render_graph::insert_transform_at_scale(doc, ins, anno_scale); + let xform = crate::scene::render_graph::insert_transform_with_policy( + doc, + ins, + anno_scale, + scale_policy, + ); let name = ins_handle.value().to_string(); let prototype_key = if !ins.is_array() && cache.prototype_blocks.contains(&ins.block_name) @@ -1042,10 +1037,22 @@ pub fn expand_insert( let mut batches = Batches::default(); let mut visited: Vec = Vec::with_capacity(8); - // `defn.aabb_local` is in the defn's offset frame — re-add - // `defn.local_offset` (f64) before transforming so the world AABB is - // accurate for distant content. - let insert_world = transform_aabb_xy(defn.aabb_local, &xform); + let offsets = crate::scene::render_graph::array_offsets(ins); + let cell_xform = |offset: &[f64; 3]| { + crate::scene::render_graph::insert_instance_transform( + doc, + ins, + *offset, + anno_scale, + scale_policy, + ) + }; + let insert_world = offsets.iter().fold([0.0_f32; 4], |aabb, offset| { + aabb_union( + aabb, + transform_aabb_xy(defn.metrics.aabb_local, &cell_xform(offset)), + ) + }); let insert_local = [ insert_world[0] as f32, insert_world[1] as f32, @@ -1062,7 +1069,7 @@ pub fn expand_insert( // Whole-Insert pixel-size LOD: if the entire Insert footprint projects // to sub-pixel size, skip it entirely. if let Some(wpp) = world_per_pixel { - if !defn.has_point_marker && aabb_pixel_size(insert_local, wpp) < MIN_PIXEL_SIZE { + if !defn.metrics.has_point_marker && aabb_pixel_size(insert_local, wpp) < MIN_PIXEL_SIZE { return Some(vec![]); } } @@ -1085,16 +1092,7 @@ pub fn expand_insert( is_xref, bg_color, }; - let offsets = crate::scene::render_graph::array_offsets(ins); if offsets.len() > 1 { - let cell_xform = |offset: &[f64; 3]| { - if offset == &[0.0; 3] { - xform.clone() - } else { - Transform::from_translation(Vector3::new(offset[0], offset[1], offset[2])) - .then(&xform) - } - }; let first_xform = cell_xform(&offsets[0]); let first_translation = transform_translation(&first_xform); let mut first_batches = Batches::default(); @@ -1135,14 +1133,7 @@ pub fn expand_insert( } for offset in &offsets { - let base_xform = if offset == &[0.0; 3] { - xform.clone() - } else { - let translation = Transform::from_translation(Vector3::new( - offset[0], offset[1], offset[2], - )); - translation.then(&xform) - }; + let base_xform = cell_xform(offset); expand_defn( defn, &base_xform, @@ -1816,8 +1807,18 @@ 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); + for offset in &nref.instance_offsets { + let attachment_transform = nested_attachment_transform(nref, *offset) + .then(accum_xform); + for attachment in &nref.attachments { + emit_wire( + attachment, + &attachment_transform, + &inner_ctx, + out, + d_range, + ); + } } if visited.iter().any(|n| n == &nref.block_name) { continue; @@ -1825,8 +1826,17 @@ fn expand_defn( 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 world = nref.instance_offsets.iter().fold( + [0.0_f32; 4], + |aabb, offset| { + let composed = nested_instance_transform(nref, *offset) + .then(accum_xform); + aabb_union( + aabb, + transform_aabb_xy(nested_defn.metrics.aabb_local, &composed), + ) + }, + ); let local = [ world[0] as f32, world[1] as f32, @@ -1851,15 +1861,7 @@ fn expand_defn( d_range.1 / (nested_defn.child_count.max(1) as f32 + 1.0), ); let composed_for = |offset: &[f64; 3]| { - if offset == &[0.0; 3] { - nref.xform.then(accum_xform) - } else { - Transform::from_translation(Vector3::new( - offset[0], offset[1], offset[2], - )) - .then(&nref.xform) - .then(accum_xform) - } + nested_instance_transform(nref, *offset).then(accum_xform) }; if let Some(cp) = &nref.clip_poly { let base_composed = nref.xform.then(accum_xform); diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 317148c9b..30fbc7259 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -107,10 +107,11 @@ fn block_entity_transform( visited, annotation_scale, )?; - Some(inner.then(&crate::scene::render_graph::insert_transform_at_scale( + Some(inner.then(&crate::scene::render_graph::insert_transform_with_policy( document, &block_use.insert, annotation_scale, + block_use.scale_policy, ))) }) }); diff --git a/src/scene/convert/dgn_linestyle.rs b/src/scene/convert/dgn_linestyle.rs index 70ba4c6b5..6b7b4c041 100644 --- a/src/scene/convert/dgn_linestyle.rs +++ b/src/scene/convert/dgn_linestyle.rs @@ -144,8 +144,8 @@ pub fn symbol_radius(doc: &CadDocument, block: Handle, scale: f64) -> f64 { ) else { return 0.0; }; - graph.walk_insert( - &block_use.insert, + graph.walk_block_use( + &block_use, block, |_, _| true, |entity, context| { @@ -322,16 +322,15 @@ pub fn place_block_wires( ) 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); + block_use.insert.set_x_scale(s); + block_use.insert.set_y_scale(s); + block_use.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(); - graph.walk_insert( - insert, + graph.walk_block_use( + &block_use, block, |_, _| true, |entity, context| { diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index f6a78a726..b877827dd 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -39,6 +39,7 @@ pub(crate) struct ExpandedBlockObject { pub(crate) struct BlockObjectOptions { pub(crate) suppress_root_points: bool, pub(crate) apply_xclip: bool, + pub(crate) scale_policy: crate::scene::BlockScalePolicy, } fn block_object_style( @@ -141,16 +142,18 @@ pub(crate) fn expand_block_object( is_xref, bg_color, anno_scale, + options.scale_policy, 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_at_scale( + let transform = crate::scene::render_graph::insert_transform_with_policy( document, insert, anno_scale, + options.scale_policy, ); let polygon = pick::xclip::world_clip_polygon_for_transform(filter, &transform); pick::xclip::clip_wires(&mut wires, &polygon); @@ -568,7 +571,7 @@ fn tessellate_entity_inner( // True only when tessellating content shown inside a paper-space viewport. // Retained through recursive INSERT expansion; PSLTSCALE itself is applied // by the viewport's GPU uniform so it never changes resident wire content. - paper_space: bool, + _paper_space: bool, ) -> Vec { let contextual = crate::scene::annotative::entity_for_annotation_context( document, @@ -980,6 +983,7 @@ fn tessellate_entity_inner( pslt_factor, BlockObjectOptions { suppress_root_points: block_use.suppress_root_points, + scale_policy: block_use.scale_policy, ..BlockObjectOptions::default() }, ) @@ -1087,7 +1091,10 @@ fn tessellate_entity_inner( view_aabb, world_per_pixel, pslt_factor, - BlockObjectOptions::default(), + BlockObjectOptions { + scale_policy: block_use.scale_policy, + ..BlockObjectOptions::default() + }, ) .wires; if !wires.is_empty() { @@ -1119,19 +1126,27 @@ fn tessellate_entity_inner( .filter(|block_use| { block_use.role == crate::scene::render_graph::BlockRole::TableCell }) { - wires.extend(tessellate_entity( - document, - selected, - active_viewport, - bg_color, - 1.0, - None, - &EntityType::Insert(block_use.insert), - block_cache, - view_aabb, - world_per_pixel, - paper_space, - )); + wires.extend( + expand_block_object( + document, + &block_use.insert, + h, + sel, + active_viewport, + bg_color, + table_anno, + None, + block_cache, + view_aabb, + world_per_pixel, + pslt_factor, + BlockObjectOptions { + scale_policy: block_use.scale_policy, + ..BlockObjectOptions::default() + }, + ) + .wires, + ); } if !wires.is_empty() { let aabb = entity_aabb(e); diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index aa131ce88..6bf477734 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -2100,8 +2100,8 @@ fn custom_arrow_from_block( let mut lines = Vec::new(); let mut fill = Vec::new(); let mut deferred_hatch = false; - graph.walk_insert( - &block_use.insert, + graph.walk_block_use( + &block_use, record.handle, |_, _| true, |entity, context| { diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 908a59100..85f25954e 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1183,7 +1183,7 @@ impl Scene { fn layer_plottable_in_context( &self, entity: &EntityType, - context: &crate::scene::render_graph::RenderContext, + context: &crate::scene::render_graph::InstanceContext, ) -> bool { let common = entity.common(); let layer = if crate::scene::view::render::is_effective_layer_zero(&common.layer) { diff --git a/src/scene/mod.rs b/src/scene/mod.rs index e54209b74..13f5bf6c2 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -17,6 +17,13 @@ pub(crate) mod render_graph; pub mod text; pub mod view; +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] +pub enum BlockScalePolicy { + #[default] + FromInsert, + Applied, +} + // Topic submodules split out of this root (each contributes `impl Scene` // blocks and/or free functions). Pure text-move from the original mod.rs. mod boundary; @@ -66,6 +73,13 @@ struct DependencyTargets { touches_block_definition: bool, } +#[derive(Clone, Copy)] +enum DependencyKind { + Layer, + TextStyle, + DimStyle, +} + #[derive(Default)] struct SceneDependencyIndex { layers: HashMap, @@ -75,6 +89,7 @@ struct SceneDependencyIndex { points: DependencyTargets, text_geometry: DependencyTargets, annotation_geometry: DependencyTargets, + signatures: HashMap, } fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64; 4]> { @@ -164,6 +179,7 @@ use glam; use iced::time::Duration; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::cell::RefCell; +use std::hash::{Hash, Hasher}; use std::rc::Rc; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -2429,11 +2445,7 @@ impl Scene { } } if !changes.is_empty() { - // A Modified delta can change layer/style/block references as well - // as coordinates. Rebuild the reverse dependency map lazily on its - // next use so later targeted global-property updates never follow - // stale ownership. - self.invalidate_dependency_index(); + self.refresh_dependency_index_for_changes(&changes); } let cached_light_changed = self.lighting_cache.borrow().values().any(|lights| { changes @@ -2533,6 +2545,7 @@ impl Scene { // Structural change — drop both per-entity tessellation memos. self.tess_memo.borrow_mut().clear(); self.resident_tess_memo.borrow_mut().clear(); + self.invalidate_dependency_index(); // Can't name the changed handles → force every journal consumer to rebuild. self.push_geometry_delta(epoch, Vec::new(), true); } @@ -2616,6 +2629,7 @@ impl Scene { let epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed); self.geometry_epoch = epoch; self.lighting_cache.borrow_mut().clear(); + self.invalidate_dependency_index(); self.push_geometry_delta(epoch, Vec::new(), true); } @@ -7132,8 +7146,8 @@ impl Scene { ) { continue; } - graph.walk_insert( - &block_use.insert, + graph.walk_block_use( + &block_use, common.handle, |_, _| true, |entity, context| { @@ -7387,7 +7401,10 @@ impl Scene { let changed_live: Vec = changes .iter() .filter_map(|(handle, kind)| { - (!matches!(kind, ChangeKind::Removed)).then_some(*handle) + (!matches!(kind, ChangeKind::Removed) + && !self.entity_temporarily_hidden(*handle) + && self.document.get_entity(*handle).is_some()) + .then_some(*handle) }) .collect(); let wires = Arc::new(self.wire_models_for(&changed_live)); @@ -8847,6 +8864,113 @@ impl Scene { }) } + fn entity_dependency_signature(&self, entity: &EntityType) -> u64 { + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + let common = entity.common(); + common.handle.hash(&mut hasher); + common.owner_handle.hash(&mut hasher); + normalize_name(&common.layer).hash(&mut hasher); + + let category = match entity { + EntityType::Point(_) => 1_u8, + EntityType::Text(_) + | EntityType::MText(_) + | EntityType::AttributeDefinition(_) + | EntityType::AttributeEntity(_) + | EntityType::Dimension(_) + | EntityType::Leader(_) + | EntityType::Tolerance(_) + | EntityType::MultiLeader(_) + | EntityType::Table(_) + | EntityType::Shape(_) => 2, + EntityType::Hatch(_) => 3, + EntityType::Insert(insert) if !insert.attributes.is_empty() => 4, + _ => 0, + }; + category.hash(&mut hasher); + crate::scene::annotative::is_annotative(&self.document, entity).hash(&mut hasher); + + for block_use in render_graph::entity_block_uses(&self.document, entity, 1.0) { + block_use.block.hash(&mut hasher); + normalize_name(&block_use.insert.block_name).hash(&mut hasher); + block_use.role.hash(&mut hasher); + block_use.scale_policy.hash(&mut hasher); + block_use.active.hash(&mut hasher); + } + + match entity { + EntityType::Text(text) => normalize_name(&text.style).hash(&mut hasher), + EntityType::MText(text) => normalize_name(&text.style).hash(&mut hasher), + EntityType::AttributeDefinition(attribute) => { + normalize_name(&attribute.text_style).hash(&mut hasher) + } + EntityType::AttributeEntity(attribute) => { + normalize_name(&attribute.text_style).hash(&mut hasher) + } + EntityType::Insert(insert) => { + for attribute in &insert.attributes { + normalize_name(&attribute.common.layer).hash(&mut hasher); + normalize_name(&attribute.text_style).hash(&mut hasher); + } + } + EntityType::Dimension(dimension) => { + normalize_name(&dimension.base().style_name).hash(&mut hasher) + } + EntityType::Leader(leader) => { + normalize_name(&leader.dimension_style).hash(&mut hasher) + } + EntityType::Tolerance(tolerance) => { + normalize_name(&tolerance.dimension_style_name).hash(&mut hasher) + } + EntityType::Table(table) => { + table.table_style_handle.hash(&mut hasher); + for row in &table.rows { + row.style + .as_ref() + .and_then(|style| style.text_style_handle) + .hash(&mut hasher); + for cell in &row.cells { + cell.style + .as_ref() + .and_then(|style| style.text_style_handle) + .hash(&mut hasher); + for content in &cell.contents { + content.text_style_handle.hash(&mut hasher); + } + } + } + } + EntityType::MultiLeader(leader) => { + leader.style_handle.hash(&mut hasher); + leader.text_style_handle.hash(&mut hasher); + leader.context.text_style_handle.hash(&mut hasher); + } + EntityType::MLine(line) => line.style_handle.hash(&mut hasher), + _ => {} + } + hasher.finish() + } + + fn refresh_dependency_index_for_changes(&self, changes: &[(Handle, ChangeKind)]) { + let mut cache = self.dependency_index_cache.borrow_mut(); + let Some(index) = cache.as_ref() else { + return; + }; + let unchanged = changes.iter().all(|(handle, kind)| { + if !matches!(kind, ChangeKind::Modified) { + return false; + } + let Some(entity) = self.document.get_entity(*handle) else { + return false; + }; + index.signatures.get(handle).copied() + == Some(self.entity_dependency_signature(entity)) + }); + if !unchanged { + cache.take(); + } + } + fn rebuild_dependency_index(&self) -> SceneDependencyIndex { let layout_blocks: HashSet = self .document @@ -8928,6 +9052,9 @@ impl Scene { let mut index = SceneDependencyIndex::default(); for entity in self.document.entities() { let common = entity.common(); + index + .signatures + .insert(common.handle, self.entity_dependency_signature(entity)); let owner = if common.owner_handle.is_null() { membership .get(&common.handle) @@ -9102,17 +9229,20 @@ impl Scene { index } - fn dependency_targets(&self, kind: &str, names: &[String]) -> DependencyTargets { + fn dependency_targets( + &self, + kind: DependencyKind, + names: &[String], + ) -> DependencyTargets { if self.dependency_index_cache.borrow().is_none() { *self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index()); } let cache = self.dependency_index_cache.borrow(); let index = cache.as_ref().unwrap(); let map = match kind { - "layer" => &index.layers, - "text" => &index.text_styles, - "dim" => &index.dim_styles, - _ => unreachable!(), + DependencyKind::Layer => &index.layers, + DependencyKind::TextStyle => &index.text_styles, + DependencyKind::DimStyle => &index.dim_styles, }; let mut combined = DependencyTargets::default(); for name in names { @@ -9148,7 +9278,7 @@ impl Scene { } pub fn invalidate_layer_dependencies(&mut self, names: &[String]) { - let targets = self.dependency_targets("layer", names); + let targets = self.dependency_targets(DependencyKind::Layer, names); self.invalidate_dependency_targets(targets); } @@ -9157,7 +9287,7 @@ impl Scene { } pub fn invalidate_text_style_dependencies_many(&mut self, names: &[String]) { - let targets = self.dependency_targets("text", names); + let targets = self.dependency_targets(DependencyKind::TextStyle, names); self.invalidate_dependency_targets(targets); } @@ -9166,7 +9296,7 @@ impl Scene { } pub fn invalidate_dim_style_dependencies_many(&mut self, names: &[String]) { - let targets = self.dependency_targets("dim", names); + let targets = self.dependency_targets(DependencyKind::DimStyle, names); self.invalidate_dependency_targets(targets); } diff --git a/src/scene/render_graph.rs b/src/scene/render_graph.rs index 69400ad4f..db73f1f56 100644 --- a/src/scene/render_graph.rs +++ b/src/scene/render_graph.rs @@ -15,6 +15,7 @@ use crate::scene::view::render::{ has_resolved_book_color, is_effective_layer_zero, layer_render_style_viewport, render_style_for_block_sub_viewport, render_style_for_viewport, InheritStyle, }; +use crate::scene::BlockScalePolicy; pub type ResolvedStyle = ([f32; 4], f32, [f32; 8], f32, u8); @@ -216,7 +217,7 @@ fn layer_aci(document: &CadDocument, layer: &str) -> u8 { } #[derive(Clone, Debug)] -pub struct RenderContext { +pub struct InstanceContext { pub transform: Transform, pub root_handle: Handle, pub parent_insert: Handle, @@ -227,9 +228,10 @@ pub struct RenderContext { pub depth_scale: f32, pub nesting_depth: usize, pub viewport: Option, + pub scale_policy: BlockScalePolicy, } -impl RenderContext { +impl InstanceContext { fn direct(depth_base: f32, viewport: Option) -> Self { Self { transform: Transform::identity(), @@ -242,6 +244,7 @@ impl RenderContext { depth_scale: 1.0, nesting_depth: 0, viewport, + scale_policy: BlockScalePolicy::FromInsert, } } @@ -314,8 +317,8 @@ impl<'a> RenderSceneGraph<'a> { /// false for an Insert removes its whole subtree. pub fn walk_root(&self, root: SceneRoot, mut visible: V, mut leaf: F) where - V: FnMut(&EntityType, &RenderContext) -> bool, - F: FnMut(&EntityType, &RenderContext), + V: FnMut(&EntityType, &InstanceContext) -> bool, + F: FnMut(&EntityType, &InstanceContext), { let block = root.content_block(); let viewport = match root { @@ -344,12 +347,18 @@ impl<'a> RenderSceneGraph<'a> { .depths .get(&handle.value()) .map_or(0.0, |depth| depth[0]); - let context = RenderContext::direct(direct_depth, viewport); + let context = InstanceContext::direct(direct_depth, viewport); if !self.document_visible(entity) || !visible(entity, &context) { continue; } if let EntityType::Insert(insert) = entity { - self.walk_insert_instances(insert, &context, &mut visible, &mut leaf); + self.walk_insert_instances( + insert, + BlockScalePolicy::FromInsert, + &context, + &mut visible, + &mut leaf, + ); } else { leaf(entity, &context); self.walk_owned_content(entity, &context, &mut visible, &mut leaf, &mut Vec::new()); @@ -359,15 +368,36 @@ impl<'a> RenderSceneGraph<'a> { /// Walk one synthetic or document-owned Insert. Used by entity renderers /// whose content is itself a block reference. - pub fn walk_insert( + pub fn walk_block_use( &self, - insert: &Insert, + block_use: &BlockUse, root_handle: Handle, mut visible: V, mut leaf: F, + ) + where + V: FnMut(&EntityType, &InstanceContext) -> bool, + F: FnMut(&EntityType, &InstanceContext), + { + self.walk_insert_with_policy( + &block_use.insert, + root_handle, + block_use.scale_policy, + &mut visible, + &mut leaf, + ); + } + + fn walk_insert_with_policy( + &self, + insert: &Insert, + root_handle: Handle, + scale_policy: BlockScalePolicy, + visible: &mut V, + leaf: &mut F, ) where - V: FnMut(&EntityType, &RenderContext) -> bool, - F: FnMut(&EntityType, &RenderContext), + V: FnMut(&EntityType, &InstanceContext) -> bool, + F: FnMut(&EntityType, &InstanceContext), { let mut root_insert = insert.clone(); root_insert.common.handle = root_handle; @@ -376,9 +406,15 @@ impl<'a> RenderSceneGraph<'a> { .depths .get(&root_handle.value()) .map_or(0.0, |depth| depth[0]); - let context = RenderContext::direct(depth_base, self.viewport); + let context = InstanceContext::direct(depth_base, self.viewport); if self.document_visible(&entity) && visible(&entity, &context) { - self.walk_insert_instances(&root_insert, &context, &mut visible, &mut leaf); + self.walk_insert_instances( + &root_insert, + scale_policy, + &context, + visible, + leaf, + ); } } @@ -386,12 +422,13 @@ impl<'a> RenderSceneGraph<'a> { fn walk_insert_instances( &self, insert: &Insert, - parent: &RenderContext, + scale_policy: BlockScalePolicy, + parent: &InstanceContext, visible: &mut V, leaf: &mut F, ) where - V: FnMut(&EntityType, &RenderContext) -> bool, - F: FnMut(&EntityType, &RenderContext), + V: FnMut(&EntityType, &InstanceContext) -> bool, + F: FnMut(&EntityType, &InstanceContext), { let insert_entity = EntityType::Insert(insert.clone()); let block_style = parent @@ -424,6 +461,7 @@ impl<'a> RenderSceneGraph<'a> { insert, offset, self.annotation_scale, + scale_policy, ); let transform = local.then(&parent.transform); let mut context = parent.clone(); @@ -435,6 +473,7 @@ impl<'a> RenderSceneGraph<'a> { context.depth_base = depth_base; context.depth_scale = depth_scale; context.nesting_depth += 1; + context.scale_policy = scale_policy; if let Some(filter) = crate::scene::pick::xclip::insert_spatial_filter( self.document, insert, @@ -465,13 +504,13 @@ impl<'a> RenderSceneGraph<'a> { fn walk_block( &self, block_name: &str, - context: &RenderContext, + context: &InstanceContext, visible: &mut V, leaf: &mut F, stack: &mut Vec, ) where - V: FnMut(&EntityType, &RenderContext) -> bool, - F: FnMut(&EntityType, &RenderContext), + V: FnMut(&EntityType, &InstanceContext) -> bool, + F: FnMut(&EntityType, &InstanceContext), { if context.nesting_depth > 32 { return; @@ -504,7 +543,13 @@ impl<'a> RenderSceneGraph<'a> { continue; } stack.push(nested.block_name.clone()); - self.walk_insert_instances(nested, context, visible, leaf); + self.walk_insert_instances( + nested, + BlockScalePolicy::FromInsert, + context, + visible, + leaf, + ); stack.pop(); } _ => { @@ -518,13 +563,13 @@ impl<'a> RenderSceneGraph<'a> { fn walk_owned_content( &self, entity: &EntityType, - context: &RenderContext, + context: &InstanceContext, visible: &mut V, leaf: &mut F, stack: &mut Vec, ) where - V: FnMut(&EntityType, &RenderContext) -> bool, - F: FnMut(&EntityType, &RenderContext), + V: FnMut(&EntityType, &InstanceContext) -> bool, + F: FnMut(&EntityType, &InstanceContext), { for block_use in entity_render_block_uses(self.document, entity, self.annotation_scale) .into_iter() @@ -537,7 +582,13 @@ impl<'a> RenderSceneGraph<'a> { continue; } stack.push(block_use.insert.block_name.clone()); - self.walk_insert_instances(&block_use.insert, context, visible, leaf); + self.walk_insert_instances( + &block_use.insert, + block_use.scale_policy, + context, + visible, + leaf, + ); stack.pop(); } } @@ -595,9 +646,24 @@ pub fn insert_transform_at_scale( document: &CadDocument, insert: &Insert, annotation_scale: f32, +) -> Transform { + insert_transform_with_policy( + document, + insert, + annotation_scale, + BlockScalePolicy::FromInsert, + ) +} + +pub fn insert_transform_with_policy( + document: &CadDocument, + insert: &Insert, + annotation_scale: f32, + scale_policy: BlockScalePolicy, ) -> Transform { let mut transform = insert_transform(document, insert); - if (annotation_scale - 1.0).abs() > 1.0e-6 + if scale_policy == BlockScalePolicy::FromInsert + && (annotation_scale - 1.0).abs() > 1.0e-6 && insert .common .extended_data @@ -615,7 +681,7 @@ pub fn insert_transform_at_scale( transform } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum BlockRole { Insert, DimensionPicture, @@ -631,6 +697,7 @@ pub struct BlockUse { pub block: Handle, pub insert: Insert, pub role: BlockRole, + pub scale_policy: BlockScalePolicy, /// Whether this edge belongs to the active render representation. Inactive /// edges remain dependencies so cache invalidation and purge stay complete. pub active: bool, @@ -654,6 +721,7 @@ fn block_use( document: &CadDocument, mut insert: Insert, role: BlockRole, + scale_policy: BlockScalePolicy, active: bool, replaces_host_wire: bool, suppress_root_points: bool, @@ -667,6 +735,7 @@ fn block_use( block, insert, role, + scale_policy, active, replaces_host_wire, suppress_root_points, @@ -687,6 +756,7 @@ pub fn block_use_from_handle( document, Insert::new(record.name.clone(), insertion), role, + BlockScalePolicy::Applied, true, false, false, @@ -727,6 +797,7 @@ fn entity_owned_block_uses( document, insert.clone(), BlockRole::Insert, + BlockScalePolicy::FromInsert, true, true, false, @@ -743,6 +814,7 @@ fn entity_owned_block_uses( document, insert, BlockRole::DimensionPicture, + BlockScalePolicy::FromInsert, picture, picture, true, @@ -771,6 +843,7 @@ fn entity_owned_block_uses( document, insert, BlockRole::TablePicture, + BlockScalePolicy::FromInsert, true, true, false, @@ -780,15 +853,12 @@ fn entity_owned_block_uses( uses.extend( crate::entities::table::block_cell_inserts(table, document, annotation_scale) .into_iter() - .map(|mut insert| { - insert - .common - .extended_data - .remove_record("AcAnnotativeData"); + .map(|insert| { block_use( document, insert, BlockRole::TableCell, + BlockScalePolicy::Applied, cells_active, false, false, @@ -799,15 +869,12 @@ fn entity_owned_block_uses( } EntityType::MultiLeader(multileader) => { crate::entities::multileader::block_content_insert(document, multileader) - .map(|mut insert| { - insert - .common - .extended_data - .remove_record("AcAnnotativeData"); + .map(|insert| { vec![block_use( document, insert, BlockRole::MultiLeaderContent, + BlockScalePolicy::Applied, true, false, false, @@ -997,8 +1064,9 @@ pub fn insert_instance_transform( insert: &Insert, offset: [f64; 3], annotation_scale: f32, + scale_policy: BlockScalePolicy, ) -> Transform { - let transform = insert_transform_at_scale(document, insert, annotation_scale); + let transform = insert_transform_with_policy(document, insert, annotation_scale, scale_policy); if offset == [0.0; 3] { transform } else { @@ -1007,6 +1075,32 @@ pub fn insert_instance_transform( } } +pub fn insert_instance_translation_delta( + document: &CadDocument, + insert: &Insert, + offset: [f64; 3], + annotation_scale: f32, + scale_policy: BlockScalePolicy, +) -> Vector3 { + let base = insert_instance_transform( + document, + insert, + [0.0; 3], + annotation_scale, + scale_policy, + ) + .apply(Vector3::ZERO); + let instance = insert_instance_transform( + document, + insert, + offset, + annotation_scale, + scale_policy, + ) + .apply(Vector3::ZERO); + instance - base +} + pub fn block_contains_hatch( document: &CadDocument, block_name: &str, @@ -1035,3 +1129,38 @@ pub fn block_contains_hatch( memo.insert(key, contains); contains } + +#[cfg(test)] +mod tests { + use super::*; + use acadrust::xdata::ExtendedDataRecord; + + #[test] + fn applied_block_scale_skips_annotative_rescaling() { + let document = CadDocument::new(); + let mut insert = Insert::new("BLOCK", Vector3::new(10.0, 20.0, 0.0)); + insert + .common + .extended_data + .add_record(ExtendedDataRecord::new("AcAnnotativeData")); + let point = Vector3::new(1.0, 0.0, 0.0); + + let from_insert = insert_transform_with_policy( + &document, + &insert, + 2.0, + BlockScalePolicy::FromInsert, + ) + .apply(point); + let applied = insert_transform_with_policy( + &document, + &insert, + 2.0, + BlockScalePolicy::Applied, + ) + .apply(point); + + assert!((from_insert.x - 12.0).abs() < 1.0e-9); + assert!((applied.x - 11.0).abs() < 1.0e-9); + } +} diff --git a/tests/text_font_rendering.rs b/tests/text_font_rendering.rs index eb741373c..0209a3b82 100644 --- a/tests/text_font_rendering.rs +++ b/tests/text_font_rendering.rs @@ -59,6 +59,7 @@ fn expand_block_mtext( &Default::default(), ); expand_insert( + &doc, &cache, &ins, Handle::new(999), @@ -86,6 +87,8 @@ fn expand_block_mtext( // Annotation scale: 1.0 = no annotative scaling, matching the // `BlockCache::build(&doc, 1.0, ...)` call above. 1.0, + OpenCADStudio::scene::BlockScalePolicy::FromInsert, + false, ) .expect("block defn is cached") }