forked from 1b-agent/opencadstudio-upstream
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:
1 parent
03555ce8bd
commit
def72e435e
5 files changed
+789
-56
No files matched your search
+1
-1
@@ -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>,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod app;
|
||||
pub mod command;
|
||||
pub mod entities;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
mod app;
|
||||
mod command;
|
||||
mod entities;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user