perf(scene): cache block defns + batch wires by style

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 <[email protected]>
This commit is contained in:
HakanandClaude Opus 4.7 committed 2026-05-14 11:12:08 +03:00
1 parent 03555ce8bd
commit def72e435e
5 files changed
+789 -56

No files matched your search

+1 -1
View File
@@ -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<AtomicU8>,
+2
View File
@@ -1,3 +1,5 @@
#![allow(non_snake_case)]
pub mod app;
pub mod command;
pub mod entities;
+2
View File
@@ -1,3 +1,5 @@
#![allow(non_snake_case)]
mod app;
mod command;
mod entities;
+692
View File
@@ -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<TangentGeom>,
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<LocalSub>,
}
#[derive(Default, Debug)]
pub struct BlockCache {
defns: HashMap<String, Arc<BlockDefn>>,
}
impl BlockCache {
pub fn new() -> Self {
Self::default()
}
pub fn defn(&self, block_name: &str) -> Option<&Arc<BlockDefn>> {
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<String> {
use std::collections::HashSet;
let mut seen: HashSet<String> = HashSet::new();
let mut queue: Vec<String> = 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<LocalSub> = 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<LocalWire> {
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<Vec<WireModel>> {
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<String> = 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<TangentGeom>,
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<StyleKey, BatchEntry>,
/// Batches that overflowed `MAX_POINTS_PER_BATCH` and have been closed.
closed: Vec<BatchEntry>,
}
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<WireModel> {
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<String>,
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
}
+92 -55
View File
@@ -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<Option<(u64, HashMap<Handle, Handle>)>>,
/// 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<Option<(u64, Arc<block_cache::BlockCache>)>>,
}
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<block_cache::BlockCache> {
{
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<WireModel> = 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<WireModel> {
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<WireModel> = 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;
}