From def72e435e1abf695a9d693b37031ba4a66f0da0 Mon Sep 17 00:00:00 2001 From: Hakan Date: Thu, 14 May 2026 11:12:08 +0300 Subject: [PATCH] perf(scene): cache block defns + batch wires by style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tessellates each block_record once into local-coord LocalWires; Insert references transform-copy + ByBlock-resolve the cache rather than explode+re-tessellate per reference. Same-style wires inside one Insert are NaN-batched into a single WireModel to keep wgpu buffer counts sane — large blocks that previously bailed to a marker now render in a handful of buffers each. Co-Authored-By: Claude Opus 4.7 --- src/app/mod.rs | 2 +- src/lib.rs | 2 + src/main.rs | 2 + src/scene/block_cache.rs | 692 +++++++++++++++++++++++++++++++++++++++ src/scene/mod.rs | 147 +++++---- 5 files changed, 789 insertions(+), 56 deletions(-) create mode 100644 src/scene/block_cache.rs diff --git a/src/app/mod.rs b/src/app/mod.rs index 533dfbbc0..695fd670e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -35,7 +35,7 @@ pub const OPEN_PHASE_CACHING: u8 = 2; pub const OPEN_PHASE_FINALIZING: u8 = 3; #[derive(Debug, Clone)] -pub(super) struct OpenProgress { +pub struct OpenProgress { pub name: String, pub size_bytes: u64, pub phase: Arc, diff --git a/src/lib.rs b/src/lib.rs index 91c50225f..3bef8e46c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + pub mod app; pub mod command; pub mod entities; diff --git a/src/main.rs b/src/main.rs index e83c1a526..16707cba9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + mod app; mod command; mod entities; diff --git a/src/scene/block_cache.rs b/src/scene/block_cache.rs new file mode 100644 index 000000000..ba878cab4 --- /dev/null +++ b/src/scene/block_cache.rs @@ -0,0 +1,692 @@ +// Block-definition tessellation cache. +// +// Each block record is tessellated once into block-local coordinates and +// stored as a list of `LocalSub` (either a tessellated primitive wire OR +// an unexpanded reference to a nested INSERT). At Insert use-time we walk +// the defn, transform-copy primitives, and recurse into nested references — +// each nested defn is itself a cache hit, never re-tessellated. +// +// This shape (lazy nested expansion) is essential: a single block like +// `xref-PLANKOTE` can hold ~4700 nested INSERTs, so build-time inlining +// produces a combinatorial blowup. Storing references and expanding on +// demand keeps build work proportional to total entity count. +// +// Cycle detection: at expand-time we maintain a recursion-depth limit and +// a visited set so a self-referential block produces a marker rather than +// recursing forever. + +use std::collections::HashMap; +use std::sync::Arc; + +use acadrust::types::{Color as AcadColor, LineWeight, Transform, Vector3}; +use acadrust::{CadDocument, EntityType, Handle}; + +use crate::scene::tessellate; +use crate::scene::wire_model::{SnapHint, TangentGeom, WireModel}; + +const MAX_NESTING_DEPTH: usize = 32; + +#[derive(Clone, Debug)] +pub struct LocalWire { + pub points: Vec<[f32; 3]>, + pub key_vertices: Vec<[f32; 3]>, + pub snap_pts: Vec<(glam::Vec3, SnapHint)>, + pub tangent_geoms: Vec, + pub fill_tris: Vec<[f32; 3]>, + pub color: [f32; 4], + pub aci: u8, + pub pattern_length: f32, + pub pattern: [f32; 8], + pub line_weight_px: f32, + pub plinegen: bool, + pub color_is_byblock: bool, + pub lt_is_byblock: bool, + pub lw_is_byblock: bool, +} + +#[derive(Clone, Debug)] +pub struct NestedRef { + pub block_name: String, + pub xform: Transform, + /// Nested INSERT's own resolved style (used when child wires need + /// to inherit something via ByBlock). + pub ins_color: [f32; 4], + pub ins_pat_len: f32, + pub ins_pat: [f32; 8], + pub ins_lw_px: f32, + pub color_is_byblock: bool, + pub lt_is_byblock: bool, + pub lw_is_byblock: bool, + pub instance_offsets: Vec<[f64; 3]>, +} + +#[derive(Clone, Debug)] +pub enum LocalSub { + Wire(LocalWire), + Nested(NestedRef), +} + +#[derive(Clone, Debug, Default)] +pub struct BlockDefn { + pub subs: Vec, +} + +#[derive(Default, Debug)] +pub struct BlockCache { + defns: HashMap>, +} + +impl BlockCache { + pub fn new() -> Self { + Self::default() + } + + pub fn defn(&self, block_name: &str) -> Option<&Arc> { + 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. + pub fn build(doc: &CadDocument, anno_scale: f32, bg_color: [f32; 4]) -> Self { + let mut cache = Self::new(); + let referenced = collect_referenced_blocks(doc); + for name in &referenced { + let defn = build_defn(doc, name, anno_scale, bg_color); + cache.defns.insert(name.clone(), Arc::new(defn)); + } + cache + } +} + +/// Walk all entities + all block_record contents collecting every distinct +/// `block_name` that appears in an Insert (transitively). +fn collect_referenced_blocks(doc: &CadDocument) -> Vec { + use std::collections::HashSet; + let mut seen: HashSet = HashSet::new(); + 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()); + } + } + } + while let Some(name) = queue.pop() { + let Some(br) = doc.block_records.get(&name) else { + continue; + }; + for &eh in &br.entity_handles { + 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()); + } + } + } + } + seen.into_iter().collect() +} + +fn build_defn( + doc: &CadDocument, + block_name: &str, + anno_scale: f32, + bg_color: [f32; 4], +) -> BlockDefn { + let br = match doc.block_records.get(block_name) { + Some(br) => br, + None => return BlockDefn::default(), + }; + let cap = br.entity_handles.len(); + let mut subs: Vec = Vec::with_capacity(cap); + for &eh in &br.entity_handles { + let Some(entity) = doc.get_entity(eh) else { + continue; + }; + match entity { + EntityType::Block(_) + | EntityType::BlockEnd(_) + | EntityType::AttributeDefinition(_) => continue, + EntityType::Insert(nested_ins) => { + subs.push(LocalSub::Nested(build_nested_ref(nested_ins, doc, bg_color))); + } + _ => { + if let Some(lw) = tessellate_sub_local(doc, entity, anno_scale, bg_color) { + subs.push(LocalSub::Wire(lw)); + } + } + } + } + BlockDefn { subs } +} + +fn build_nested_ref( + nested_ins: &acadrust::entities::Insert, + doc: &CadDocument, + bg_color: [f32; 4], +) -> NestedRef { + let (mut ins_color, ins_pat_len, ins_pat, ins_lw_px, _) = + crate::scene::render::render_style_for(doc, &EntityType::Insert(nested_ins.clone())); + ins_color = crate::scene::render::adapt_to_bg(ins_color, bg_color); + + NestedRef { + block_name: nested_ins.block_name.clone(), + xform: nested_ins.get_transform(), + ins_color, + ins_pat_len, + ins_pat, + ins_lw_px, + color_is_byblock: nested_ins.common.color == AcadColor::ByBlock, + lt_is_byblock: nested_ins.common.linetype.eq_ignore_ascii_case("byblock"), + lw_is_byblock: matches!(nested_ins.common.line_weight, LineWeight::ByBlock), + instance_offsets: array_offsets(nested_ins), + } +} + +fn tessellate_sub_local( + doc: &CadDocument, + sub: &EntityType, + anno_scale: f32, + bg_color: [f32; 4], +) -> Option { + let h = sub.common().handle; + + // Sanity guard: skip sub-entities whose primary dimension is so large + // that adaptive tessellation will explode into hundreds of millions + // of points. These are typically corrupt-radius primitives that slipped + // past purge_corrupt_entities (finite but absurd values). + if is_unreasonable_extent(sub) { + return None; + } + + let (sub_color, pat_len, pat, lw_px, aci) = crate::scene::render::render_style_for(doc, sub); + let sub_color = crate::scene::render::adapt_to_bg(sub_color, bg_color); + + let color_is_byblock = sub.common().color == AcadColor::ByBlock; + let lt_is_byblock = sub.common().linetype.eq_ignore_ascii_case("byblock"); + let lw_is_byblock = matches!(sub.common().line_weight, LineWeight::ByBlock); + + let wire = tessellate::tessellate( + doc, h, sub, false, sub_color, pat_len, pat, lw_px, [0.0; 3], anno_scale, + ); + + if wire.points.len() > 100_000 { + return None; + } + + Some(LocalWire { + points: wire.points, + key_vertices: wire.key_vertices, + snap_pts: wire.snap_pts, + tangent_geoms: wire.tangent_geoms, + fill_tris: wire.fill_tris, + color: sub_color, + aci, + pattern_length: pat_len, + pattern: pat, + line_weight_px: lw_px, + plinegen: wire.plinegen, + color_is_byblock, + lt_is_byblock, + lw_is_byblock, + }) +} + +// ── Use-time expansion ─────────────────────────────────────────────────────── + +/// Expand one top-level INSERT into world-space WireModels via the cache. +/// +/// 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( + cache: &BlockCache, + ins: &acadrust::entities::Insert, + ins_handle: Handle, + ins_resolved_color: [f32; 4], + ins_pat_len: f32, + ins_pat: [f32; 8], + ins_lw_px: f32, + selected: bool, + world_offset: [f64; 3], + pslt_factor: f32, +) -> Option> { + let defn = cache.defn(&ins.block_name)?; + let xform = ins.get_transform(); + let name = ins_handle.value().to_string(); + let mut batches = Batches::default(); + let mut visited: Vec = Vec::with_capacity(8); + + for offset in &array_offsets(ins) { + 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 ctx = ExpandCtx { + cache, + ins_color: ins_resolved_color, + ins_pat_len, + ins_pat, + ins_lw_px, + selected, + world_offset, + pslt_factor, + }; + expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0); + } + Some(batches.finalize(&name, selected)) +} + +struct ExpandCtx<'a> { + cache: &'a BlockCache, + ins_color: [f32; 4], + ins_pat_len: f32, + ins_pat: [f32; 8], + ins_lw_px: f32, + selected: bool, + world_offset: [f64; 3], + pslt_factor: f32, +} + +/// Style fingerprint used to group local wires into a single GPU buffer. +/// f32 fields are bit-cast to u32 to make the key Hash + Eq. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct StyleKey { + color: [u32; 4], + pattern_length: u32, + pattern: [u32; 8], + line_weight_px: u32, + aci: u8, + plinegen: bool, +} + +#[derive(Default, Debug)] +struct BatchEntry { + color: [f32; 4], + pattern_length: f32, + pattern: [f32; 8], + line_weight_px: f32, + aci: u8, + plinegen: bool, + points: Vec<[f32; 3]>, + snap_pts: Vec<(glam::Vec3, SnapHint)>, + key_vertices: Vec<[f32; 3]>, + tangent_geoms: Vec, + fill_tris: Vec<[f32; 3]>, + min_x: f32, + min_y: f32, + max_x: f32, + max_y: f32, +} + +/// Hard cap on point count for a single batched WireModel. Above this the +/// current batch is finalized (pushed into `closed`) and a fresh one is +/// started under the same style. Each WireModel point becomes ~6 GPU +/// vertices of 96 bytes — 200k points fits well under wgpu's 256 MB +/// per-buffer ceiling. +const MAX_POINTS_PER_BATCH: usize = 200_000; + +#[derive(Default, Debug)] +struct Batches { + by_style: HashMap, + /// Batches that overflowed `MAX_POINTS_PER_BATCH` and have been closed. + closed: Vec, +} + +impl BatchEntry { + fn new(color: [f32; 4], pat_len: f32, pat: [f32; 8], lw_px: f32, aci: u8, plinegen: bool) -> Self { + Self { + color, + pattern_length: pat_len, + pattern: pat, + line_weight_px: lw_px, + aci, + plinegen, + min_x: f32::INFINITY, + min_y: f32::INFINITY, + max_x: f32::NEG_INFINITY, + max_y: f32::NEG_INFINITY, + ..Default::default() + } + } +} + +impl Batches { + fn finalize(self, name: &str, selected: bool) -> Vec { + self.closed + .into_iter() + .chain(self.by_style.into_values()) + .map(|b| { + let aabb = if b.min_x.is_infinite() { + WireModel::UNBOUNDED_AABB + } else { + [b.min_x, b.min_y, b.max_x, b.max_y] + }; + WireModel { + name: name.to_string(), + points: b.points, + color: b.color, + selected, + pattern_length: b.pattern_length, + pattern: b.pattern, + line_weight_px: b.line_weight_px, + aci: b.aci, + snap_pts: b.snap_pts, + tangent_geoms: b.tangent_geoms, + key_vertices: b.key_vertices, + aabb, + plinegen: b.plinegen, + vp_scissor: None, + fill_tris: b.fill_tris, + } + }) + .collect() + } +} + +fn style_key( + color: [f32; 4], + pat_len: f32, + pat: [f32; 8], + lw_px: f32, + aci: u8, + plinegen: bool, +) -> StyleKey { + StyleKey { + color: [ + color[0].to_bits(), + color[1].to_bits(), + color[2].to_bits(), + color[3].to_bits(), + ], + pattern_length: pat_len.to_bits(), + pattern: [ + pat[0].to_bits(), + pat[1].to_bits(), + pat[2].to_bits(), + pat[3].to_bits(), + pat[4].to_bits(), + pat[5].to_bits(), + pat[6].to_bits(), + pat[7].to_bits(), + ], + line_weight_px: lw_px.to_bits(), + aci, + plinegen, + } +} + +fn expand_defn( + defn: &BlockDefn, + accum_xform: &Transform, + ctx: &ExpandCtx, + out: &mut Batches, + visited: &mut Vec, + depth: usize, +) { + if depth > MAX_NESTING_DEPTH { + eprintln!("block_cache: nested-block depth > {MAX_NESTING_DEPTH}, truncating"); + return; + } + for sub in &defn.subs { + match sub { + LocalSub::Wire(lw) => emit_wire(lw, accum_xform, ctx, out), + 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; + }; + // Resolve ByBlock for this nested ref against the outer ctx. + let nested_color = if nref.color_is_byblock { + ctx.ins_color + } else { + nref.ins_color + }; + let (nested_pat_len, nested_pat) = if nref.lt_is_byblock { + (ctx.ins_pat_len, ctx.ins_pat) + } else { + (nref.ins_pat_len, nref.ins_pat) + }; + let nested_lw_px = if nref.lw_is_byblock { + ctx.ins_lw_px + } else { + nref.ins_lw_px + }; + let inner_ctx = ExpandCtx { + cache: ctx.cache, + ins_color: nested_color, + ins_pat_len: nested_pat_len, + ins_pat: nested_pat, + ins_lw_px: nested_lw_px, + selected: ctx.selected, + world_offset: ctx.world_offset, + pslt_factor: ctx.pslt_factor, + }; + visited.push(nref.block_name.clone()); + for offset in &nref.instance_offsets { + let composed = if offset == &[0.0; 3] { + nref.xform.then(accum_xform) + } else { + let translation = Transform::from_translation(Vector3::new( + offset[0], offset[1], offset[2], + )); + translation.then(&nref.xform).then(accum_xform) + }; + expand_defn( + nested_defn, + &composed, + &inner_ctx, + out, + visited, + depth + 1, + ); + } + visited.pop(); + } + } + } +} + +fn emit_wire(lw: &LocalWire, accum_xform: &Transform, ctx: &ExpandCtx, out: &mut Batches) { + if lw.points.is_empty() && lw.fill_tris.is_empty() { + return; + } + let [ox, oy, oz] = ctx.world_offset; + + // Resolve final style for this LocalWire against the outer Insert ctx + // before we hash it into a batch. + let final_color = if ctx.selected { + WireModel::SELECTED + } else if lw.color_is_byblock { + ctx.ins_color + } else { + lw.color + }; + let (final_pat_len, final_pat) = if lw.lt_is_byblock { + (ctx.ins_pat_len, ctx.ins_pat) + } else { + (lw.pattern_length, lw.pattern) + }; + let final_lw_px = if lw.lw_is_byblock { + ctx.ins_lw_px + } else { + lw.line_weight_px + }; + let final_pat_len = final_pat_len * ctx.pslt_factor; + let final_pat = final_pat.map(|v| v * ctx.pslt_factor); + + let key = style_key( + final_color, + final_pat_len, + final_pat, + final_lw_px, + lw.aci, + lw.plinegen, + ); + + // If the open batch for this style would exceed wgpu's per-buffer limit + // after appending this wire, finalize it now and start a fresh batch. + if let Some(existing) = out.by_style.get(&key) { + if existing.points.len() + lw.points.len() + 1 > MAX_POINTS_PER_BATCH { + if let Some(closed) = out.by_style.remove(&key) { + out.closed.push(closed); + } + } + } + let entry = out.by_style.entry(key).or_insert_with(|| { + BatchEntry::new( + final_color, + final_pat_len, + final_pat, + final_lw_px, + lw.aci, + lw.plinegen, + ) + }); + + // NaN separator between previously-appended geometry and this wire so the + // GPU shader treats them as disconnected polylines within one buffer. + let needs_sep = !entry.points.is_empty() + && !entry.points.last().map(|p| p[0].is_nan()).unwrap_or(false); + + if !lw.points.is_empty() { + if needs_sep { + entry.points.push([f32::NAN; 3]); + } + for p in &lw.points { + if p[0].is_nan() { + entry.points.push([f32::NAN; 3]); + continue; + } + let v = accum_xform.apply(Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64)); + let q = [(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]; + if q[0] < entry.min_x { + entry.min_x = q[0]; + } + if q[1] < entry.min_y { + entry.min_y = q[1]; + } + if q[0] > entry.max_x { + entry.max_x = q[0]; + } + if q[1] > entry.max_y { + entry.max_y = q[1]; + } + entry.points.push(q); + } + } + + for p in &lw.key_vertices { + let v = accum_xform.apply(Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64)); + entry + .key_vertices + .push([(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]); + } + for (p, hint) in &lw.snap_pts { + let v = accum_xform.apply(Vector3::new(p.x as f64, p.y as f64, p.z as f64)); + entry.snap_pts.push(( + glam::Vec3::new( + (v.x - ox) as f32, + (v.y - oy) as f32, + (v.z - oz) as f32, + ), + *hint, + )); + } + for tg in &lw.tangent_geoms { + entry + .tangent_geoms + .push(transform_tangent(tg, accum_xform, [ox, oy, oz])); + } + for p in &lw.fill_tris { + let v = accum_xform.apply(Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64)); + entry + .fill_tris + .push([(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]); + } + +} + +fn transform_tangent(tg: &TangentGeom, t: &Transform, woff: [f64; 3]) -> TangentGeom { + let [ox, oy, oz] = woff; + match tg { + TangentGeom::Line { p1, p2 } => { + let q1 = t.apply(Vector3::new(p1[0] as f64, p1[1] as f64, p1[2] as f64)); + let q2 = t.apply(Vector3::new(p2[0] as f64, p2[1] as f64, p2[2] as f64)); + TangentGeom::Line { + p1: [(q1.x - ox) as f32, (q1.y - oy) as f32, (q1.z - oz) as f32], + p2: [(q2.x - ox) as f32, (q2.y - oy) as f32, (q2.z - oz) as f32], + } + } + TangentGeom::Circle { center, radius } => { + let c = t.apply(Vector3::new( + center[0] as f64, + center[1] as f64, + center[2] as f64, + )); + let m = &t.matrix.m; + let sx = ((m[0][0] * m[0][0] + m[0][1] * m[0][1] + m[0][2] * m[0][2]) as f64).sqrt(); + let sy = ((m[1][0] * m[1][0] + m[1][1] * m[1][1] + m[1][2] * m[1][2]) as f64).sqrt(); + let s = ((sx + sy) * 0.5) as f32; + TangentGeom::Circle { + center: [(c.x - ox) as f32, (c.y - oy) as f32, (c.z - oz) as f32], + radius: radius * s, + } + } + } +} + +/// Radius / coordinate cap above which adaptive curve tessellation will +/// allocate hundreds of millions of points. `parameter_division` samples +/// to a fixed chord tolerance, so a Circle of radius 1e10 already produces +/// tens of millions of points. +const SANE_EXTENT: f64 = 1.0e8; + +fn is_unreasonable_extent(e: &EntityType) -> bool { + // Adaptive curve tessellation also explodes on degenerate primitives + // (radius = 0, axes of length 0): `parameter_division` allocates + // proportional to range/tolerance, which underflows when the curve + // collapses to a point. Drop both ends of the spectrum. + match e { + EntityType::Circle(c) => c.radius.abs() < 1.0e-9 || c.radius.abs() > SANE_EXTENT, + EntityType::Arc(a) => a.radius.abs() < 1.0e-9 || a.radius.abs() > SANE_EXTENT, + EntityType::Ellipse(el) => { + let mx = el.major_axis.x.abs() + el.major_axis.y.abs() + el.major_axis.z.abs(); + mx < 1.0e-9 + || el.major_axis.x.abs() > SANE_EXTENT + || el.major_axis.y.abs() > SANE_EXTENT + || el.major_axis.z.abs() > SANE_EXTENT + } + _ => false, + } +} + +fn array_offsets(ins: &acadrust::entities::Insert) -> Vec<[f64; 3]> { + if !ins.is_minsert() { + return vec![[0.0; 3]]; + } + let mut offsets = Vec::with_capacity(ins.instance_count()); + for row in 0..ins.row_count { + for col in 0..ins.column_count { + offsets.push([ + col as f64 * ins.column_spacing, + row as f64 * ins.row_spacing, + 0.0, + ]); + } + } + offsets +} + diff --git a/src/scene/mod.rs b/src/scene/mod.rs index bcc5f4d2e..6338b8946 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1,4 +1,5 @@ pub mod acad_to_truck; +pub mod block_cache; mod camera; pub mod complex_lt; pub mod cxf; @@ -282,6 +283,10 @@ pub struct Scene { /// Reverse map: entity_handle → block_record_handle, built from entity_handles lists. /// Keyed by geometry_epoch. Eliminates the O(B) fallback scan in belongs_to_visible_block. entity_block_map_cache: RefCell)>>, + /// Tessellated block definitions in block-local coords, keyed by geometry_epoch. + /// Lets Insert tessellation transform-copy cached wires instead of + /// clone+explode+re-tessellate per reference. + block_defn_cache: RefCell)>>, } impl Scene { @@ -317,9 +322,38 @@ impl Scene { annotation_scale: 1.0, model_extents_cache: RefCell::new(None), entity_block_map_cache: RefCell::new(None), + block_defn_cache: RefCell::new(None), } } + /// Get (or build on miss) the block-definition cache for the current epoch. + /// Built single-threaded — recursive nested expansion makes parallelization + /// fiddly and the cache only rebuilds when geometry actually changes. + pub(super) fn block_cache_arc(&self) -> Arc { + { + let cache = self.block_defn_cache.borrow(); + if let Some((epoch, ref arc)) = *cache { + if epoch == self.geometry_epoch { + return Arc::clone(arc); + } + } + } + let bg = if self.current_layout == "Model" { + self.bg_color + } else { + self.paper_bg_color + }; + let anno = if self.current_layout == "Model" { + self.annotation_scale + } else { + 1.0 + }; + let built = block_cache::BlockCache::build(&self.document, anno, bg); + let arc = Arc::new(built); + *self.block_defn_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc))); + arc + } + pub fn bump_geometry(&mut self) { self.geometry_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed); } @@ -917,9 +951,11 @@ impl Scene { } else { 1.0 }; + let blk_cache = self.block_cache_arc(); + let blk_ref: &block_cache::BlockCache = &blk_cache; let mut wires: Vec = visible .into_par_iter() - .flat_map(|e| tessellate_entity(doc, sel, avp, woff, bg, anno, e)) + .flat_map(|e| tessellate_entity(doc, sel, avp, woff, bg, anno, e, Some(blk_ref))) .collect(); // Apply draw order via the cached index (O(1) block lookup). @@ -1015,6 +1051,7 @@ impl Scene { } else { 1.0 }; + let blk_cache = self.block_cache_arc(); tessellate_entity( &self.document, &self.selected, @@ -1023,6 +1060,7 @@ impl Scene { bg, anno, e, + Some(&blk_cache), ) } @@ -3495,6 +3533,7 @@ impl Scene { }; let model_block = self.model_space_block_handle(); + let blk_cache = self.block_cache_arc(); self.document .entities() @@ -3533,6 +3572,7 @@ impl Scene { self.bg_color, vp_anno_scale, e, + Some(&blk_cache), ) }) .collect() @@ -3722,6 +3762,7 @@ fn tessellate_entity( bg_color: [f32; 4], anno_scale: f32, e: &EntityType, + block_cache: Option<&block_cache::BlockCache>, ) -> Vec { let h = e.common().handle; let sel = selected.contains(&h); @@ -3817,10 +3858,54 @@ fn tessellate_entity( } if let EntityType::Insert(ins) = e { - // Bail out for huge blocks (typically xref imports): exploding + - // tessellating tens of thousands of sub-entities freezes the UI, and - // any single bad sub can hang tessellation forever. Draw only an - // insertion marker so the user sees the block exists. + // Resolve the INSERT's own style so ByBlock sub-entities can inherit it. + let (ins_color, ins_pat_len, ins_pat, ins_lw_px, _) = render::render_style_for(document, e); + let ins_color = render::adapt_to_bg(ins_color, bg_color); + let [ox, oy, oz] = world_offset; + let ip = glam::Vec3::new( + (ins.insert_point.x - ox) as f32, + (ins.insert_point.y - oy) as f32, + (ins.insert_point.z - oz) as f32, + ); + let marker = WireModel { + name: h.value().to_string(), + points: vec![], + color: entity_color, + selected: sel, + aci: 0, + pattern_length: 0.0, + pattern: [0.0; 8], + line_weight_px: 1.0, + snap_pts: vec![(ip, wire_model::SnapHint::Insertion)], + tangent_geoms: vec![], + key_vertices: vec![], + aabb: WireModel::UNBOUNDED_AABB, + plinegen: true, + vp_scissor: None, + fill_tris: vec![], + }; + + if let Some(cache) = block_cache { + if let Some(mut wires) = block_cache::expand_insert( + cache, + ins, + h, + ins_color, + ins_pat_len, + ins_pat, + ins_lw_px, + sel, + world_offset, + pslt_factor, + ) { + wires.push(marker); + return wires; + } + } + + // Cache miss / unavailable: fall back to the original explode path, + // but keep the safety bail-out for pathologically large blocks so + // the UI doesn't hang on broken or unbounded sub-entity counts. const INSERT_SUB_LIMIT: usize = 5_000; let sub_count = document .block_records @@ -3828,33 +3913,8 @@ fn tessellate_entity( .map(|br| br.entity_handles.len()) .unwrap_or(0); if sub_count > INSERT_SUB_LIMIT { - let [ox, oy, oz] = world_offset; - let ip = glam::Vec3::new( - (ins.insert_point.x - ox) as f32, - (ins.insert_point.y - oy) as f32, - (ins.insert_point.z - oz) as f32, - ); - return vec![WireModel { - name: h.value().to_string(), - points: vec![], - color: entity_color, - selected: sel, - aci, - pattern_length: 0.0, - pattern: [0.0; 8], - line_weight_px: 1.0, - snap_pts: vec![(ip, wire_model::SnapHint::Insertion)], - tangent_geoms: vec![], - key_vertices: vec![], - aabb: WireModel::UNBOUNDED_AABB, - plinegen: true, - vp_scissor: None, - fill_tris: vec![], - }]; + return vec![marker]; } - // Resolve the INSERT's own style so ByBlock sub-entities can inherit it. - let (ins_color, ins_pat_len, ins_pat, ins_lw_px, _) = render::render_style_for(document, e); - let ins_color = render::adapt_to_bg(ins_color, bg_color); let mut wires: Vec = ins .explode_from_document(document) .iter() @@ -3892,30 +3952,7 @@ fn tessellate_entity( vec![wire] }) .collect(); - // Insertion snap at the block reference origin. - let [ox, oy, oz] = world_offset; - let ip = glam::Vec3::new( - (ins.insert_point.x - ox) as f32, - (ins.insert_point.y - oy) as f32, - (ins.insert_point.z - oz) as f32, - ); - wires.push(WireModel { - name: h.value().to_string(), - points: vec![], - color: entity_color, - selected: sel, - aci: 0, - pattern_length: 0.0, - pattern: [0.0; 8], - line_weight_px: 1.0, - snap_pts: vec![(ip, wire_model::SnapHint::Insertion)], - tangent_geoms: vec![], - key_vertices: vec![], - aabb: WireModel::UNBOUNDED_AABB, - plinegen: true, - vp_scissor: None, - fill_tris: vec![], - }); + wires.push(marker); return wires; }