fix(render): share repeated block meshes

Keep placement transforms and bounds separate from immutable mesh data to prevent large drawings from exhausting memory or stalling selection.
This commit is contained in:
Hakan Seven committed 2026-08-12 11:01:53 +03:00
1 parent 5e773fbcce
commit 7b4af0aa75
6 files changed
+408 -399

No files matched your search

+116 -220
View File
@@ -89,6 +89,9 @@ fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64
}
fn mesh_interaction_aabb(set: &model::mesh_model::MeshLodSet) -> Option<[f64; 6]> {
if let Some(aabb) = set.instance_aabb {
return Some(aabb);
}
let mut aabb = [
f64::INFINITY,
f64::INFINITY,
@@ -1213,95 +1216,63 @@ fn offset_mesh_lod_set(mut set: MeshLodSet) -> MeshLodSet {
set
}
/// Instance a block-local mesh into the render frame: apply the accumulated
/// INSERT transform (block-local → world/DXF) then subtract world_offset, so a
/// block scaled at the INSERT renders at the right size. Normals are rotated by
/// the transform's linear part and re-normalized. (#123)
/// Build a lightweight placement over immutable block-local geometry.
fn transform_block_mesh_lod_set(
set: &MeshLodSet,
xform: &acadrust::types::Transform,
) -> MeshLodSet {
use acadrust::types::Vector3;
let mut out = set.clone();
out.instance_transform = Some(*xform);
let mut min_x = f32::INFINITY;
let mut min_y = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut max_y = f32::NEG_INFINITY;
for lod in &mut out.lods {
let has_low = lod.verts_low.len() == lod.verts.len();
if !has_low {
lod.verts_low = vec![[0.0; 3]; lod.verts.len()];
}
for (v, vl) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) {
// Reconstruct the block-local f64, apply the INSERT transform and
// subtract world_offset in f64, then re-split into (high, low).
let w = xform.apply(Vector3::new(
v[0] as f64 + vl[0] as f64,
v[1] as f64 + vl[1] as f64,
v[2] as f64 + vl[2] as f64,
));
let ax = w.x;
let ay = w.y;
let az = w.z;
let hx = ax as f32;
let hy = ay as f32;
let hz = az as f32;
*v = [hx, hy, hz];
*vl = [
(ax - hx as f64) as f32,
(ay - hy as f64) as f32,
(az - hz as f64) as f32,
];
if hx < min_x {
min_x = hx;
}
if hy < min_y {
min_y = hy;
}
if hx > max_x {
max_x = hx;
}
if hy > max_y {
max_y = hy;
}
}
for n in &mut lod.normals {
let d = xform.apply_rotation(Vector3::new(n[0] as f64, n[1] as f64, n[2] as f64));
let len = (d.x * d.x + d.y * d.y + d.z * d.z).sqrt();
if len > 1e-12 {
n[0] = (d.x / len) as f32;
n[1] = (d.y / len) as f32;
n[2] = (d.z / len) as f32;
let source = set.instance_source.clone().unwrap_or_else(|| {
std::sync::Arc::new(model::mesh_model::MeshInstanceSource {
handle: set.entity_handle().unwrap_or(Handle::new(0)),
lods: set.lods.clone(),
edge_verts: set.edge_verts.clone(),
edge_verts_low: set.edge_verts_low.clone(),
curved_gens: set.curved_gens.clone(),
})
});
let mut bounds = [
f64::INFINITY,
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
];
for x in [set.world_aabb[0], set.world_aabb[2]] {
for y in [set.world_aabb[1], set.world_aabb[3]] {
for z in [set.z_aabb[0], set.z_aabb[1]] {
let point = xform.apply(Vector3::new(x as f64, y as f64, z as f64));
for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
bounds[axis] = bounds[axis].min(value);
bounds[axis + 3] = bounds[axis + 3].max(value);
}
}
}
}
// Apply the same INSERT transform to the feature edges.
{
let n = out.edge_verts.len();
if out.edge_verts_low.len() != n {
out.edge_verts_low = vec![[0.0; 3]; n];
}
for (v, vl) in out.edge_verts.iter_mut().zip(out.edge_verts_low.iter_mut()) {
let w = xform.apply(Vector3::new(
v[0] as f64 + vl[0] as f64,
v[1] as f64 + vl[1] as f64,
v[2] as f64 + vl[2] as f64,
));
let (hx, hy, hz) = (w.x as f32, w.y as f32, w.z as f32);
*v = [hx, hy, hz];
*vl = [
(w.x - hx as f64) as f32,
(w.y - hy as f64) as f32,
(w.z - hz as f64) as f32,
];
}
MeshLodSet {
lods: Vec::new(),
material: set.material.clone(),
face_materials: set.face_materials.clone(),
visual_style: set.visual_style.clone(),
complete: set.complete,
edge_verts: Vec::new(),
edge_verts_low: Vec::new(),
curved_gens: Vec::new(),
metrics: set.metrics,
world_aabb: [
bounds[0] as f32,
bounds[1] as f32,
bounds[3] as f32,
bounds[4] as f32,
],
z_aabb: [bounds[2] as f32, bounds[5] as f32],
instance_source: Some(source),
instance_transform: Some(*xform),
instance_handle: None,
instance_color: set.display_color(),
instance_aabb: bounds.iter().all(|value| value.is_finite()).then_some(bounds),
}
if min_x.is_finite() {
out.world_aabb = [min_x, min_y, max_x, max_y];
}
out.recompute_aabb();
out
}
#[derive(Clone, Copy, Debug)]
@@ -1538,8 +1509,7 @@ pub struct Scene {
Arc<crate::scene::pick::interaction_index::InteractionIndex>,
)>,
>,
/// Auxiliary broad phase for objects that have no wire representation:
/// hatch-only and solid-only block instances.
/// Auxiliary broad phase for hatch-only objects.
interaction_handle_index_cache: RefCell<
Option<(
u64,
@@ -6169,15 +6139,11 @@ impl Scene {
}
let mut lookup: HashMap<Handle, Vec<u32>> = HashMap::default();
for (index, set) in meshes.iter().enumerate() {
let Some(value) = set
.lods
.first()
.and_then(|mesh| mesh.name.parse::<u64>().ok())
else {
let Some(handle) = set.entity_handle() else {
continue;
};
lookup
.entry(Handle::new(value))
.entry(handle)
.or_default()
.push(index as u32);
}
@@ -6490,18 +6456,6 @@ impl Scene {
true
}
fn mesh_visible_for_interaction(&self, handle: Handle) -> bool {
self.mesh_entity_visible(handle)
&& self.document.get_entity(handle).is_some_and(|entity| {
!self.interaction_layer_frozen(&entity.common().layer)
&& self.belongs_to_visible_block(
handle,
entity.common().owner_handle,
self.interaction_block_handle(),
)
})
}
/// One transformed mesh per block-definition solid instance reached from an
/// INSERT owned by `layout_block`. Nested INSERTs accumulate their
/// transform. Empty when no block solids exist. (#123)
@@ -6541,8 +6495,7 @@ impl Scene {
return;
};
let inherit = self.mesh_inherit_for_path(&context.insert_path);
let own_alpha =
set.lods.first().map(|mesh| mesh.color[3]).unwrap_or(1.0);
let own_alpha = set.display_color().map_or(1.0, |color| color[3]);
let mut transformed =
transform_block_mesh_lod_set(set, &context.transform);
if let Some(color) = self.block_mesh_override_color(
@@ -6551,9 +6504,7 @@ impl Scene {
inherit.as_ref(),
own_alpha,
) {
for mesh in &mut transformed.lods {
mesh.color = color;
}
transformed.instance_color = Some(color);
if let Some(material) = transformed.material.as_mut() {
if material.handle.is_none() {
material.diffuse = color;
@@ -6569,10 +6520,7 @@ impl Scene {
self.material_base_dir.as_deref(),
);
}
let root_name = context.root_handle.value().to_string();
for mesh in &mut transformed.lods {
mesh.name = root_name.clone();
}
transformed.instance_handle = Some(context.root_handle);
out.push(transformed);
},
);
@@ -7449,20 +7397,6 @@ impl Scene {
}
}
}
for set in self.interaction_meshes_arc().iter() {
let Some(mesh) = set.lods.first() else {
continue;
};
let Ok(handle) = mesh.name.parse::<u64>() else {
continue;
};
if !self.mesh_visible_for_interaction(Handle::new(handle)) {
continue;
}
if let Some(aabb) = mesh_interaction_aabb(set) {
entries.push((handle, aabb));
}
}
let index =
Arc::new(crate::scene::pick::interaction_index::InteractionHandleIndex::build(entries));
*self.interaction_handle_index_cache.borrow_mut() =
@@ -7677,7 +7611,6 @@ impl Scene {
.collect();
let insert_hatches = self.insert_hatches_for_click();
let meshes = self.interaction_meshes_arc();
let mesh_lookup = self.mesh_pick_lookup(&meshes);
handles.extend(
self.interaction_handle_index()
.query_xy(aabb)
@@ -7685,12 +7618,19 @@ impl Scene {
.filter_map(|value| {
let handle = Handle::new(value);
(self.hatch_visible_for_interaction(handle)
|| insert_hatches.contains_key(&handle)
|| (mesh_lookup.contains_key(&handle)
&& self.mesh_visible_for_interaction(handle)))
|| insert_hatches.contains_key(&handle))
.then_some(handle)
}),
);
handles.extend(meshes.iter().filter_map(|set| {
let handle = set.entity_handle()?;
let bounds = mesh_interaction_aabb(set)?;
(bounds[3] >= aabb[0]
&& bounds[0] <= aabb[2]
&& bounds[4] >= aabb[1]
&& bounds[1] <= aabb[3])
.then_some(handle)
}));
handles
}
@@ -7703,22 +7643,17 @@ impl Scene {
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>,
_candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> {
let meshes = self.interaction_meshes_arc();
let lookup = self.mesh_pick_lookup(&meshes);
let handles: Vec<Handle> = candidate_handles.map_or_else(
|| lookup.keys().copied().collect(),
|candidates| candidates.iter().copied().collect(),
);
let handles: Vec<Handle> = lookup.keys().copied().collect();
let mut out = Vec::new();
for handle in handles {
if !self.mesh_visible_for_interaction(handle)
|| matches!(
self.document.get_entity(handle),
Some(EntityType::Insert(_))
)
{
if matches!(
self.document.get_entity(handle),
Some(EntityType::Insert(_))
) {
continue;
}
let Some(indices) = lookup.get(&handle) else {
@@ -7728,12 +7663,12 @@ impl Scene {
.iter()
.filter_map(|&index| meshes.get(index as usize))
.any(|set| {
set.lods.first().is_some_and(|mesh| {
set.geometry_lods().first().is_some_and(|mesh| {
!pick::hit_test::mesh_box_hit(
a,
b,
crossing,
std::iter::once((handle, mesh)),
std::iter::once((handle, mesh, set.instance_transform)),
view_rot,
eye,
bounds,
@@ -7877,22 +7812,17 @@ impl Scene {
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>,
_candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> {
let meshes = self.interaction_meshes_arc();
let lookup = self.mesh_pick_lookup(&meshes);
let handles: Vec<Handle> = candidate_handles.map_or_else(
|| lookup.keys().copied().collect(),
|candidates| candidates.iter().copied().collect(),
);
let handles: Vec<Handle> = lookup.keys().copied().collect();
let mut out = Vec::new();
for handle in handles {
if !self.mesh_visible_for_interaction(handle)
|| matches!(
self.document.get_entity(handle),
Some(EntityType::Insert(_))
)
{
if matches!(
self.document.get_entity(handle),
Some(EntityType::Insert(_))
) {
continue;
}
let Some(indices) = lookup.get(&handle) else {
@@ -7902,11 +7832,11 @@ impl Scene {
.iter()
.filter_map(|&index| meshes.get(index as usize))
.any(|set| {
set.lods.first().is_some_and(|mesh| {
set.geometry_lods().first().is_some_and(|mesh| {
!pick::hit_test::mesh_poly_hit(
poly,
crossing,
std::iter::once((handle, mesh)),
std::iter::once((handle, mesh, set.instance_transform)),
view_rot,
eye,
bounds,
@@ -7971,60 +7901,34 @@ impl Scene {
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>,
_candidate_handles: Option<&HashSet<Handle>>,
coarse_lod: bool,
) -> Option<Handle> {
// Reuse the renderer's expanded mesh set (top-level solids + per-INSERT
// block instances), cached per geometry epoch — so hover no longer
// re-expands every block instance on each move. Every `MeshLodSet`
// carries its handle (in `mesh.name`) and a 3D AABB.
// Reuse the renderer's cached top-level and block-instance mesh set.
let meshes = self.interaction_meshes_arc();
let lookup = candidate_handles.map(|_| self.mesh_pick_lookup(&meshes));
// Candidate handles already came from the precise f64 interaction BVH.
// When no index is active the source is small/non-resident, so exact
// triangles are safer than reintroducing the old f32 AABB precision loss.
let mut sets: Vec<(Handle, &MeshLodSet)> = Vec::new();
if let (Some(handles), Some(lookup)) = (candidate_handles, lookup.as_ref()) {
for handle in handles {
if !self.mesh_visible_for_interaction(*handle) {
continue;
}
let Some(indices) = lookup.get(handle) else {
continue;
};
sets.extend(
indices
.iter()
.filter_map(|&index| meshes.get(index as usize).map(|set| (*handle, set))),
);
}
} else {
sets.extend(meshes.iter().filter_map(|set| {
let handle = set
.lods
.first()?
.name
.parse::<u64>()
.ok()
.map(Handle::new)?;
self.mesh_visible_for_interaction(handle)
.then_some((handle, set))
}));
}
if coarse_lod {
return pick::hit_test::mesh_click_hit(
cursor,
sets.iter()
.filter_map(|(handle, set)| set.lods.last().map(|mesh| (*handle, mesh))),
view_rot,
eye,
bounds,
);
}
let sets: Vec<(Handle, &MeshLodSet)> = meshes
.iter()
.filter_map(|set| {
let handle = set.entity_handle()?;
Some((handle, set))
})
.collect();
pick::hit_test::mesh_click_hit(
cursor,
sets.iter()
.filter_map(|(handle, set)| set.lods.first().map(|mesh| (*handle, mesh))),
sets.iter().filter_map(|(handle, set)| {
let lods = set.geometry_lods();
let mesh = if coarse_lod {
lods.last()?
} else {
lods.first()?
};
Some((
*handle,
mesh,
set.instance_transform,
mesh_interaction_aabb(set)?,
))
}),
view_rot,
eye,
bounds,
@@ -8043,24 +7947,20 @@ impl Scene {
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>,
_candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> {
if self.block_meshes.is_empty() {
return Vec::new();
}
let meshes = self.interaction_meshes_arc();
let lookup = self.mesh_pick_lookup(&meshes);
let handles: Vec<Handle> = candidate_handles.map_or_else(
|| lookup.keys().copied().collect(),
|candidates| candidates.iter().copied().collect(),
);
let handles: Vec<Handle> = lookup.keys().copied().collect();
let mut out = Vec::new();
for handle in handles {
if !matches!(
self.document.get_entity(handle),
Some(EntityType::Insert(_))
) || !self.mesh_visible_for_interaction(handle)
{
) {
continue;
}
let Some(indices) = lookup.get(&handle) else {
@@ -8070,12 +7970,12 @@ impl Scene {
.iter()
.filter_map(|&index| meshes.get(index as usize))
.any(|set| {
set.lods.first().map_or(false, |m| {
set.geometry_lods().first().is_some_and(|mesh| {
!pick::hit_test::mesh_box_hit(
a,
b,
crossing,
std::iter::once((handle, m)),
std::iter::once((handle, mesh, set.instance_transform)),
view_rot,
eye,
bounds,
@@ -8098,24 +7998,20 @@ impl Scene {
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>,
_candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> {
if self.block_meshes.is_empty() {
return Vec::new();
}
let meshes = self.interaction_meshes_arc();
let lookup = self.mesh_pick_lookup(&meshes);
let handles: Vec<Handle> = candidate_handles.map_or_else(
|| lookup.keys().copied().collect(),
|candidates| candidates.iter().copied().collect(),
);
let handles: Vec<Handle> = lookup.keys().copied().collect();
let mut out = Vec::new();
for handle in handles {
if !matches!(
self.document.get_entity(handle),
Some(EntityType::Insert(_))
) || !self.mesh_visible_for_interaction(handle)
{
) {
continue;
}
let Some(indices) = lookup.get(&handle) else {
@@ -8125,11 +8021,11 @@ impl Scene {
.iter()
.filter_map(|&index| meshes.get(index as usize))
.any(|set| {
set.lods.first().map_or(false, |m| {
set.geometry_lods().first().is_some_and(|mesh| {
!pick::hit_test::mesh_poly_hit(
poly,
crossing,
std::iter::once((handle, m)),
std::iter::once((handle, mesh, set.instance_transform)),
view_rot,
eye,
bounds,