perf: keep heavy work off UI thread

This commit is contained in:
Hakan Seven committed 2026-07-25 18:49:18 +03:00
1 parent 12bf126646
commit deec48ad99
75 files changed
+11702 -518

No files matched your search

+220 -3
View File
@@ -15,7 +15,7 @@
// a visited set so a self-referential block produces a marker rather than
// recursing forever.
use rustc_hash::FxHashMap as HashMap;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::sync::Arc;
use acadrust::types::{Color as AcadColor, LineWeight, Transform, Vector3};
@@ -153,6 +153,29 @@ pub struct BlockDefn {
#[derive(Default, Debug)]
pub struct BlockCache {
defns: HashMap<String, Arc<BlockDefn>>,
prototype_blocks: HashSet<String>,
/// Fully expanded prototype for repeated, non-array inserts. The key omits
/// translation but includes the linear transform and every inherited style
/// input. A matching insert therefore reuses all nested expansion/style
/// work and only applies its translation to the immutable prototype.
expansion_prototypes: std::sync::Mutex<
HashMap<ExpansionPrototypeKey, Arc<std::sync::Mutex<Option<Arc<CachedExpansion>>>>>,
>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct ExpansionPrototypeKey {
block_name: String,
linear: [u64; 9],
insert_style: Vec<u32>,
selected: bool,
is_xref: bool,
}
#[derive(Debug)]
struct CachedExpansion {
translation: [f64; 3],
wires: Arc<Vec<WireModel>>,
}
impl BlockCache {
@@ -181,6 +204,18 @@ impl BlockCache {
use crate::par::prelude::*;
let mut cache = Self::new();
let referenced = collect_referenced_blocks(doc);
let mut reference_counts: HashMap<String, usize> = HashMap::default();
for entity in doc.entities() {
if let EntityType::Insert(insert) = entity {
*reference_counts
.entry(insert.block_name.clone())
.or_default() += insert.instance_count();
}
}
cache.prototype_blocks = reference_counts
.into_iter()
.filter_map(|(name, count)| (count > 1).then_some(name))
.collect();
// Each defn is built independently: nested INSERTs are stored as
// by-name references (`LocalSub::Nested`), never expanded here, so a
// block's build never depends on another block's defn. That makes the
@@ -269,7 +304,6 @@ impl BlockCache {
/// 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 rustc_hash::FxHashSet as HashSet;
let mut seen: HashSet<String> = HashSet::default();
let mut queue: Vec<String> = Vec::new();
@@ -795,6 +829,61 @@ pub fn expand_insert(
xform = xform.then(&scale_about_p);
}
let name = ins_handle.value().to_string();
let prototype_key = if view_aabb.is_none()
&& world_per_pixel.is_none()
&& !ins.is_array()
&& cache.prototype_blocks.contains(&ins.block_name)
{
Some(expansion_prototype_key(
ins,
&xform,
ins_resolved_color,
ins_pat_len,
ins_pat,
ins_lw_px,
ins_layer,
selected,
pslt_factor,
is_xref,
bg_color,
anno_scale,
))
} else {
None
};
let prototype_slot = prototype_key.as_ref().map(|key| {
let mut prototypes = cache
.expansion_prototypes
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
Arc::clone(
prototypes
.entry(key.clone())
.or_insert_with(|| Arc::new(std::sync::Mutex::new(None))),
)
});
let mut prototype_guard = prototype_slot
.as_ref()
.map(|slot| slot.lock().unwrap_or_else(|poisoned| poisoned.into_inner()));
if let Some(cached) = prototype_guard
.as_ref()
.and_then(|guard| guard.as_ref())
.cloned()
{
let translation = transform_translation(&xform);
let delta = [
translation[0] - cached.translation[0],
translation[1] - cached.translation[1],
translation[2] - cached.translation[2],
];
return Some(
cached
.wires
.iter()
.map(|wire| translated_prototype_wire(wire, &name, delta))
.collect(),
);
}
let mut batches = Batches::default();
let mut visited: Vec<String> = Vec::with_capacity(8);
@@ -848,7 +937,135 @@ pub fn expand_insert(
};
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0, (0.0, 1.0));
}
Some(batches.finalize(&name, selected, bg_color))
let wires = batches.finalize(&name, selected, bg_color);
if let Some(guard) = prototype_guard.as_mut() {
let cached = Arc::new(CachedExpansion {
translation: transform_translation(&xform),
wires: Arc::new(wires.clone()),
});
**guard = Some(cached);
}
Some(wires)
}
fn transform_translation(transform: &Transform) -> [f64; 3] {
[
transform.matrix.m[0][3],
transform.matrix.m[1][3],
transform.matrix.m[2][3],
]
}
#[allow(clippy::too_many_arguments)]
fn expansion_prototype_key(
ins: &acadrust::entities::Insert,
transform: &Transform,
ins_color: [f32; 4],
ins_pat_len: f32,
ins_pat: [f32; 8],
ins_lw_px: f32,
ins_layer: crate::scene::view::render::InheritStyle,
selected: bool,
pslt_factor: f32,
is_xref: bool,
bg_color: [f32; 4],
anno_scale: f32,
) -> ExpansionPrototypeKey {
let matrix = &transform.matrix.m;
let linear = [
matrix[0][0].to_bits(),
matrix[0][1].to_bits(),
matrix[0][2].to_bits(),
matrix[1][0].to_bits(),
matrix[1][1].to_bits(),
matrix[1][2].to_bits(),
matrix[2][0].to_bits(),
matrix[2][1].to_bits(),
matrix[2][2].to_bits(),
];
let mut insert_style = Vec::with_capacity(32);
insert_style.extend(ins_color.map(f32::to_bits));
insert_style.push(ins_pat_len.to_bits());
insert_style.extend(ins_pat.map(f32::to_bits));
insert_style.push(ins_lw_px.to_bits());
insert_style.extend(ins_layer.color.map(f32::to_bits));
insert_style.push(ins_layer.pat_len.to_bits());
insert_style.extend(ins_layer.pat.map(f32::to_bits));
insert_style.push(ins_layer.lw_px.to_bits());
insert_style.push(pslt_factor.to_bits());
insert_style.extend(bg_color.map(f32::to_bits));
insert_style.push(anno_scale.to_bits());
ExpansionPrototypeKey {
block_name: ins.block_name.clone(),
linear,
insert_style,
selected,
is_xref,
}
}
fn translated_prototype_wire(source: &WireModel, name: &str, delta: [f64; 3]) -> WireModel {
let mut wire = source.clone();
wire.name = name.to_string();
translate_double_single(&mut wire.points, &mut wire.points_low, delta);
translate_double_single(&mut wire.fill_tris, &mut wire.fill_tris_low, delta);
translate_double_single(&mut wire.pick_tris, &mut wire.pick_tris_low, delta);
for (point, _) in &mut wire.snap_pts {
point.x += delta[0];
point.y += delta[1];
point.z += delta[2];
}
for point in &mut wire.key_vertices {
point[0] += delta[0];
point[1] += delta[1];
point[2] += delta[2];
}
let delta_f32 = [delta[0] as f32, delta[1] as f32, delta[2] as f32];
for tangent in &mut wire.tangent_geoms {
match tangent {
TangentGeom::Line { p1, p2 } => {
for axis in 0..3 {
p1[axis] += delta_f32[axis];
p2[axis] += delta_f32[axis];
}
}
TangentGeom::Circle { center, .. } => {
for axis in 0..3 {
center[axis] += delta_f32[axis];
}
}
}
}
if !wire.text_verts.is_empty() {
wire.text_verts =
crate::scene::model::wire_model::map_text_verts(&wire.text_verts, |x, y, z| {
(x + delta[0], y + delta[1], z + delta[2])
});
}
if wire.aabb != WireModel::UNBOUNDED_AABB {
wire.aabb[0] += delta_f32[0];
wire.aabb[1] += delta_f32[1];
wire.aabb[2] += delta_f32[0];
wire.aabb[3] += delta_f32[1];
}
wire
}
fn translate_double_single(points: &mut [[f32; 3]], lows: &mut Vec<[f32; 3]>, delta: [f64; 3]) {
if points.is_empty() {
return;
}
if lows.len() != points.len() {
lows.resize(points.len(), [0.0; 3]);
}
for (point, low) in points.iter_mut().zip(lows.iter_mut()) {
for axis in 0..3 {
let value = point[axis] as f64 + low[axis] as f64 + delta[axis];
let high = value as f32;
point[axis] = high;
low[axis] = (value - high as f64) as f32;
}
}
}
fn aabb_pixel_size(local_aabb: [f32; 4], world_per_pixel: f32) -> f32 {