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 2026-08-12 11:01:36 +03:00
commit 7b4af0aa75
6 changed files with 408 additions and 399 deletions

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,

View file

@ -221,8 +221,12 @@ impl MeshMaterial {
}
pub fn apply_to(&self, set: &mut super::mesh_model::MeshLodSet) {
for lod in &mut set.lods {
lod.color = self.diffuse;
if set.instance_source.is_some() {
set.instance_color = Some(self.diffuse);
} else {
for lod in &mut set.lods {
lod.color = self.diffuse;
}
}
set.material = Some(self.clone());
}
@ -236,7 +240,7 @@ impl MeshMaterial {
self.apply_to(set);
set.face_materials.clear();
let handles: rustc_hash::FxHashSet<Handle> = set
.lods
.geometry_lods()
.iter()
.flat_map(|lod| lod.triangle_material_handles.iter().flatten().copied())
.collect();

View file

@ -104,6 +104,12 @@ pub struct MeshLodSet {
pub instance_source: Option<std::sync::Arc<MeshInstanceSource>>,
/// Accumulated block-local → world transform for this rendered instance.
pub instance_transform: Option<acadrust::types::Transform>,
/// Parent INSERT selected for this rendered block instance.
pub instance_handle: Option<acadrust::Handle>,
/// Effective colour after INSERT inheritance, without copying the mesh.
pub instance_color: Option<[f32; 4]>,
/// Precise world bounds used by the interaction index.
pub instance_aabb: Option<[f64; 6]>,
}
#[derive(Clone, Debug)]
@ -112,6 +118,7 @@ pub struct MeshInstanceSource {
pub lods: Vec<MeshModel>,
pub edge_verts: Vec<[f32; 3]>,
pub edge_verts_low: Vec<[f32; 3]>,
pub curved_gens: Vec<CurvedGen>,
}
/// 3D bounds of every LOD's vertices: `([min_x, min_y, max_x, max_y], [min_z, max_z])`.
@ -225,6 +232,9 @@ impl MeshLodSet {
z_aabb,
instance_source: None,
instance_transform: None,
instance_handle: None,
instance_color: None,
instance_aabb: None,
}
}
@ -250,7 +260,35 @@ impl MeshLodSet {
lods: self.lods.clone(),
edge_verts: self.edge_verts.clone(),
edge_verts_low: self.edge_verts_low.clone(),
curved_gens: self.curved_gens.clone(),
}));
self.instance_transform = None;
self.instance_handle = None;
self.instance_color = None;
self.instance_aabb = None;
}
pub fn geometry_lods(&self) -> &[MeshModel] {
self.instance_source
.as_ref()
.map_or(self.lods.as_slice(), |source| source.lods.as_slice())
}
pub fn entity_handle(&self) -> Option<acadrust::Handle> {
self.instance_handle.or_else(|| {
self.lods
.first()
.and_then(|mesh| mesh.name.parse::<u64>().ok())
.map(acadrust::Handle::new)
})
}
pub fn display_color(&self) -> Option<[f32; 4]> {
self.instance_color.or_else(|| {
self.geometry_lods()
.iter()
.find(|mesh| !mesh.indices.is_empty())
.map(|mesh| mesh.color)
})
}
}

View file

@ -658,64 +658,183 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
hits.into_iter().map(|(_, name)| name).collect()
}
pub fn mesh_click_hit<'a>(
pub(crate) fn mesh_click_hit<'a>(
cursor: Point,
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
meshes: impl Iterator<
Item = (
Handle,
&'a MeshModel,
Option<acadrust::types::Transform>,
[f64; 6],
),
>,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
) -> Option<Handle> {
let mut best: Option<(f32, Handle)> = None;
for (handle, mesh) in meshes {
let v = &mesh.verts;
let idx = &mesh.indices;
let lo = &mesh.verts_low;
// Indexed meshes reuse most vertices across several triangles.
// Project once per vertex instead of three matrix transforms per
// triangle; dense solid misses are otherwise the worst-case hover.
let projected: Vec<(Point, f32)> = v
.iter()
.enumerate()
.map(|(i, &vertex)| {
let ndc =
view_rot.project_point3((mesh_vert(vertex, lo, i) - eye).as_vec3());
(
Point::new(
(ndc.x + 1.0) * 0.5 * bounds.width,
(1.0 - ndc.y) * 0.5 * bounds.height,
let profile = crate::perf::enabled().then(std::time::Instant::now);
let mut set_count = 0usize;
let mut bound_hits = 0usize;
let mut exact_triangles = 0usize;
let ndc = glam::Vec3::new(
cursor.x / bounds.width * 2.0 - 1.0,
1.0 - cursor.y / bounds.height * 2.0,
0.0,
);
let inverse_view = view_rot.inverse();
let near = eye + inverse_view.project_point3(ndc).as_dvec3();
let far = eye
+ inverse_view
.project_point3(glam::Vec3::new(ndc.x, ndc.y, 1.0))
.as_dvec3();
let world_direction = (far - near).normalize_or_zero();
if !near.is_finite() || !world_direction.is_finite() || world_direction.length_squared() < 1e-18
{
return None;
}
let mut best: Option<(f64, Handle)> = None;
for (handle, mesh, transform, aabb) in meshes {
set_count += 1;
let Some((near_t, _)) = ray_aabb(near, world_direction, aabb) else {
continue;
};
bound_hits += 1;
exact_triangles += mesh.indices.len() / 3;
if best.is_some_and(|(distance, _)| near_t > distance) {
continue;
}
let model = transform.map(codec_transform_matrix);
let (origin, direction) = if let Some(model) = model {
if !model.is_finite() || model.determinant().abs() <= 1e-18 {
continue;
}
let inverse = model.inverse();
let origin = inverse.transform_point3(near);
let direction = inverse.transform_vector3(world_direction).normalize_or_zero();
(origin, direction)
} else {
(near, world_direction)
};
if direction.length_squared() < 1e-18 {
continue;
}
let local_t = mesh
.indices
.chunks_exact(3)
.filter_map(|triangle| {
ray_triangle(
origin,
direction,
mesh_vert(
mesh.verts[triangle[0] as usize],
&mesh.verts_low,
triangle[0] as usize,
),
mesh_vert(
mesh.verts[triangle[1] as usize],
&mesh.verts_low,
triangle[1] as usize,
),
mesh_vert(
mesh.verts[triangle[2] as usize],
&mesh.verts_low,
triangle[2] as usize,
),
ndc.z,
)
})
.collect();
let mut t = 0;
while t + 2 < idx.len() {
let tri = [idx[t] as usize, idx[t + 1] as usize, idx[t + 2] as usize];
t += 3;
let mut sp = [Point::ORIGIN; 3];
let mut depth = 0.0f32;
for (j, &k) in tri.iter().enumerate() {
let (point, z) = projected[k];
sp[j] = point;
depth += z;
}
if point_in_polygon(cursor, &sp) {
let d = depth / 3.0;
if best.map_or(true, |(bd, _)| d < bd) {
best = Some((d, handle));
}
break; // one hit per mesh is enough
.min_by(f64::total_cmp);
if let Some(local_t) = local_t {
let local_hit = origin + direction * local_t;
let world_hit = model.map_or(local_hit, |model| model.transform_point3(local_hit));
let distance = (world_hit - near).dot(world_direction);
if distance >= 0.0 && best.is_none_or(|(current, _)| distance < current) {
best = Some((distance, handle));
}
}
}
if let Some(started) = profile {
crate::perf_record!(
"[perf] mesh-ray {:>7.1}ms sets={} bounds={} source_triangles={}",
started.elapsed().as_secs_f64() * 1000.0,
set_count,
bound_hits,
exact_triangles,
);
}
best.map(|(_, h)| h)
}
fn codec_transform_matrix(transform: acadrust::types::Transform) -> glam::DMat4 {
let matrix = transform.matrix.m;
glam::DMat4::from_cols_array(&[
matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0],
matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1],
matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2],
matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3],
])
}
fn ray_aabb(origin: glam::DVec3, direction: glam::DVec3, aabb: [f64; 6]) -> Option<(f64, f64)> {
let mut near = 0.0_f64;
let mut far = f64::INFINITY;
for axis in 0..3 {
let origin = origin[axis];
let direction = direction[axis];
if direction.abs() <= 1e-18 {
if origin < aabb[axis] || origin > aabb[axis + 3] {
return None;
}
continue;
}
let first = (aabb[axis] - origin) / direction;
let second = (aabb[axis + 3] - origin) / direction;
near = near.max(first.min(second));
far = far.min(first.max(second));
if far < near {
return None;
}
}
Some((near, far))
}
fn ray_triangle(
origin: glam::DVec3,
direction: glam::DVec3,
a: glam::DVec3,
b: glam::DVec3,
c: glam::DVec3,
) -> Option<f64> {
let edge1 = b - a;
let edge2 = c - a;
let cross = direction.cross(edge2);
let determinant = edge1.dot(cross);
if determinant.abs() <= 1e-12 {
return None;
}
let inverse = determinant.recip();
let offset = origin - a;
let u = offset.dot(cross) * inverse;
if !(0.0..=1.0).contains(&u) {
return None;
}
let q = offset.cross(edge1);
let v = direction.dot(q) * inverse;
if v < 0.0 || u + v > 1.0 {
return None;
}
let distance = edge2.dot(q) * inverse;
(distance >= 0.0).then_some(distance)
}
/// Reconstruct a mesh vertex's absolute f64 position from its high/low pair —
/// without the low residual the f32 high alone is ~0.5 m off at UTM scale and
/// box / lasso / face selection lands on the wrong place.
#[inline]
fn mesh_vert(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 {
fn mesh_vert(
hi: [f32; 3],
low: &[[f32; 3]],
i: usize,
) -> glam::DVec3 {
let l = low.get(i).copied().unwrap_or([0.0; 3]);
glam::DVec3::new(
hi[0] as f64 + l[0] as f64,
@ -727,6 +846,7 @@ fn mesh_vert(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 {
/// Project a mesh's vertices to screen space.
fn project_mesh_verts(
mesh: &MeshModel,
transform: Option<acadrust::types::Transform>,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
@ -735,7 +855,12 @@ fn project_mesh_verts(
.iter()
.enumerate()
.map(|(i, &w)| {
let ndc = view_rot.project_point3((mesh_vert(w, &mesh.verts_low, i) - eye).as_vec3());
let point = mesh_vert(w, &mesh.verts_low, i);
let point = transform.map_or(point, |transform| {
let point = transform.apply(acadrust::types::Vector3::new(point.x, point.y, point.z));
glam::DVec3::new(point.x, point.y, point.z)
});
let ndc = view_rot.project_point3((point - eye).as_vec3());
Point::new(
(ndc.x + 1.0) * 0.5 * bounds.width,
(1.0 - ndc.y) * 0.5 * bounds.height,
@ -769,7 +894,13 @@ pub fn mesh_box_hit<'a>(
a: Point,
b: Point,
crossing: bool,
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
meshes: impl Iterator<
Item = (
Handle,
&'a MeshModel,
Option<acadrust::types::Transform>,
),
>,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
@ -784,8 +915,8 @@ pub fn mesh_box_hit<'a>(
Point::new(min_x, max_y),
];
let mut out = Vec::new();
for (h, mesh) in meshes {
let proj = project_mesh_verts(mesh, view_rot, eye, bounds);
for (h, mesh, transform) in meshes {
let proj = project_mesh_verts(mesh, transform, view_rot, eye, bounds);
if proj.is_empty() {
continue;
}
@ -807,7 +938,13 @@ pub fn mesh_box_hit<'a>(
pub fn mesh_poly_hit<'a>(
poly: &[Point],
crossing: bool,
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
meshes: impl Iterator<
Item = (
Handle,
&'a MeshModel,
Option<acadrust::types::Transform>,
),
>,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
@ -816,8 +953,8 @@ pub fn mesh_poly_hit<'a>(
return Vec::new();
}
let mut out = Vec::new();
for (h, mesh) in meshes {
let proj = project_mesh_verts(mesh, view_rot, eye, bounds);
for (h, mesh, transform) in meshes {
let proj = project_mesh_verts(mesh, transform, view_rot, eye, bounds);
if proj.is_empty() {
continue;
}

View file

@ -975,8 +975,9 @@ fn mesh_spatial_key(set: &MeshLodSet, bounds: [f32; 6]) -> u64 {
struct MeshBatchPart<'a> {
set: &'a MeshLodSet,
mesh: &'a MeshModel,
display_mesh: &'a MeshModel,
uv_mesh: &'a MeshModel,
entity_handle: Option<acadrust::Handle>,
display_color: [f32; 4],
material: Option<&'a crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
indices: std::sync::Arc<[u32]>,
@ -1157,9 +1158,7 @@ fn build_instanced_chunk(
.set
.visual_style
.as_ref()
.map_or(first.display_mesh.color, |style| {
style.edge_color(first.display_mesh.color)
});
.map_or(first.display_color, |style| style.edge_color(first.display_color));
let edge_key = (source_handle, edge_color.map(f32::to_bits));
let shared_edge_buffer = first
.include_edges
@ -1229,13 +1228,7 @@ fn build_instanced_chunk(
bounds[3] = bounds[3].max(part.set.world_aabb[2]);
bounds[4] = bounds[4].max(part.set.world_aabb[3]);
bounds[5] = bounds[5].max(part.set.z_aabb[1]);
if let Some(handle) = part
.display_mesh
.name
.parse::<u64>()
.ok()
.map(acadrust::Handle::new)
{
if let Some(handle) = part.entity_handle {
handles.insert(handle);
if first.include_faces {
highlights.push(MeshBatchRange {
@ -1332,24 +1325,6 @@ fn mesh_bounds(mesh: &MeshModel) -> [f32; 6] {
bounds
}
fn material_uses_model_mapper(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> bool {
material.is_some_and(|material| {
[
&material.diffuse_map,
&material.specular_map,
&material.reflection_map,
&material.opacity_map,
&material.bump_map,
&material.refraction_map,
&material.normal_map,
]
.into_iter()
.any(|map| map.image.is_some() && map.auto_transform & 4 != 0)
})
}
fn material_is_transparent(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4],
@ -1557,34 +1532,30 @@ pub fn build_mesh_batch_filtered(
let mut face_partitions: rustc_hash::FxHashMap<FacePartitionKey, Vec<CachedFacePart<'_>>> =
rustc_hash::FxHashMap::default();
for set in sets {
let Some(display_mesh) = set.lods.iter().find(|mesh| !mesh.indices.is_empty()) else {
let Some(mesh) = set
.geometry_lods()
.iter()
.find(|mesh| !mesh.indices.is_empty())
else {
continue;
};
let display_handle = display_mesh
.name
.parse::<u64>()
.ok()
.map(acadrust::Handle::new);
let display_handle = set.entity_handle();
if handles.is_some_and(|wanted| {
display_handle.is_none_or(|handle| !wanted.contains(&handle))
}) {
continue;
}
let source_mesh = set
.instance_source
.as_ref()
.and_then(|source| source.lods.iter().find(|mesh| !mesh.indices.is_empty()));
let mesh = source_mesh.unwrap_or(display_mesh);
let display_color = set.display_color().unwrap_or(mesh.color);
let source_identity = set.instance_source.as_ref().map_or_else(
|| (false, display_mesh as *const MeshModel as usize as u64),
|| (false, mesh as *const MeshModel as usize as u64),
|source| (true, source.handle.value()),
);
let triangle_count = display_mesh.indices.len() / 3;
let triangle_count = mesh.indices.len() / 3;
let has_face_materials =
display_mesh.triangle_material_handles.len() == triangle_count
mesh.triangle_material_handles.len() == triangle_count
&& !set.face_materials.is_empty();
let has_face_colors = display_mesh.triangle_colors.len() == triangle_count
&& display_mesh.triangle_colors.iter().any(Option::is_some);
let has_face_colors = mesh.triangle_colors.len() == triangle_count
&& mesh.triangle_colors.iter().any(Option::is_some);
let include_faces = set
.visual_style
.as_ref()
@ -1595,7 +1566,7 @@ pub fn build_mesh_batch_filtered(
.map_or(true, |style| style.edges_visible());
if !has_face_materials && !has_face_colors {
let base_color =
set.material.as_ref().map_or(display_mesh.color, |material| material.diffuse);
set.material.as_ref().map_or(display_color, |material| material.diffuse);
let color = set
.visual_style
.as_ref()
@ -1603,7 +1574,7 @@ pub fn build_mesh_batch_filtered(
let (shared_indices, shared_hash) = source_indices
.entry(source_identity)
.or_insert_with(|| {
let indices = std::sync::Arc::<[u32]>::from(display_mesh.indices.as_slice());
let indices = std::sync::Arc::<[u32]>::from(mesh.indices.as_slice());
let hash = index_hash(indices.as_ref());
(indices, hash)
})
@ -1611,8 +1582,9 @@ pub fn build_mesh_batch_filtered(
ordered.push(MeshBatchPart {
set,
mesh,
display_mesh,
uv_mesh: mesh,
entity_handle: display_handle,
display_color,
material: set.material.as_ref(),
color,
indices: shared_indices,
@ -1637,10 +1609,10 @@ pub fn build_mesh_batch_filtered(
let partition_key = FacePartitionKey {
instanced: source_identity.0,
source: source_identity.1,
base_material: material_key(set.material.as_ref(), display_mesh.color),
base_material: material_key(set.material.as_ref(), display_color),
face_materials,
visual_style: visual_style_partition_key(set.visual_style.as_ref()),
display_color: display_mesh.color.map(f32::to_bits),
display_color: display_color.map(f32::to_bits),
include_faces,
include_edges,
};
@ -1655,18 +1627,18 @@ pub fn build_mesh_batch_filtered(
Vec<u32>,
),
> = std::collections::BTreeMap::new();
for (triangle, indices) in display_mesh.indices.chunks_exact(3).enumerate() {
for (triangle, indices) in mesh.indices.chunks_exact(3).enumerate() {
let material = if has_face_materials {
display_mesh.triangle_material_handles[triangle]
mesh.triangle_material_handles[triangle]
.and_then(|handle| set.face_materials.get(&handle))
.or(set.material.as_ref())
} else {
set.material.as_ref()
};
let base_color =
material.map_or(display_mesh.color, |material| material.diffuse);
material.map_or(display_color, |material| material.diffuse);
let base_color = if has_face_colors {
display_mesh.triangle_colors[triangle].unwrap_or(base_color)
mesh.triangle_colors[triangle].unwrap_or(base_color)
} else {
base_color
};
@ -1700,8 +1672,9 @@ pub fn build_mesh_batch_filtered(
ordered.push(MeshBatchPart {
set,
mesh,
display_mesh,
uv_mesh: mesh,
entity_handle: display_handle,
display_color,
material: part.material,
color: part.color,
indices: part.indices,
@ -1738,25 +1711,34 @@ pub fn build_mesh_batch_filtered(
mesh_spatial_key(part.set, spatial_bounds),
)
});
let storage_instancing =
device.limits().max_storage_buffers_per_shader_stage > 0;
let storage_instancing = device.limits().max_storage_buffers_per_shader_stage > 0;
let mut instance_groups: std::collections::BTreeMap<
InstanceGroupKey,
Vec<MeshBatchPart<'_>>,
> = std::collections::BTreeMap::new();
let mut direct_parts = Vec::with_capacity(ordered.len());
for mut part in ordered {
let eligible = storage_instancing
&& part.set.instance_transform.is_some()
for part in ordered {
let eligible = part.set.instance_transform.is_some()
&& part.set.instance_source.is_some()
&& !material_uses_model_mapper(part.material)
&& part.mesh.verts.len() <= max_verts
&& part.indices.len() / 3 <= max_tris
&& part
.mesh
.verts
.len()
.saturating_mul(std::mem::size_of::<MeshVertex>())
<= hard_budget
&& part.indices.len().saturating_mul(2 * std::mem::size_of::<u32>())
<= hard_budget
&& part
.set
.instance_source
.as_ref()
.is_some_and(|source| source.edge_verts.len() <= max_verts)
.is_some_and(|source| {
source
.edge_verts
.len()
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
<= hard_budget
})
&& part
.indices
.iter()
@ -1782,46 +1764,9 @@ pub fn build_mesh_batch_filtered(
.or_default()
.push(part);
} else {
// Compatibility and non-instanced meshes keep their already
// transformed display vertices in the ordinary static batch.
part.mesh = part.display_mesh;
direct_parts.push(part);
}
}
let sparse_groups: Vec<_> = instance_groups
.iter()
.filter_map(|(key, parts)| {
let first = parts.first()?;
let geometry_bytes = first
.mesh
.verts
.len()
.saturating_mul(std::mem::size_of::<MeshVertex>())
.saturating_add(first.indices.len().saturating_mul(12))
.saturating_add(
first
.set
.instance_source
.as_ref()
.map_or(0, |source| {
source
.edge_verts
.len()
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
}),
);
let saved_bytes = geometry_bytes.saturating_mul(parts.len().saturating_sub(1));
(parts.len() < 4 && saved_bytes < 512 * 1024).then_some(*key)
})
.collect();
for key in sparse_groups {
if let Some(parts) = instance_groups.remove(&key) {
for mut part in parts {
part.mesh = part.display_mesh;
direct_parts.push(part);
}
}
}
direct_parts.sort_by_key(|part| {
(
material_key(part.material, part.color),
@ -1838,12 +1783,7 @@ pub fn build_mesh_batch_filtered(
let mesh = part.mesh;
let material = part.material;
let part_color = part.color;
let entity_handle = part
.display_mesh
.name
.parse::<u64>()
.ok()
.map(acadrust::Handle::new);
let entity_handle = part.entity_handle;
let key = material_key(material, part_color);
if active_key.is_some_and(|active| active != key)
&& (!verts.is_empty() || !edge_verts.is_empty())
@ -1887,7 +1827,7 @@ pub fn build_mesh_batch_filtered(
let edge_color = set
.visual_style
.as_ref()
.map_or(mesh.color, |style| style.edge_color(mesh.color));
.map_or(part.display_color, |style| style.edge_color(part.display_color));
let vtx = |vi: usize| {
let normal = if has_normals {
mesh.normals[vi]
@ -2259,10 +2199,11 @@ pub fn build_mesh_batch_filtered(
// granularity. Small block definitions get spatial clusters of roughly
// 64256 INSERTs; a huge source is duplicated only a few times.
let max_clusters = ((64 * 1024 * 1024) / source_bytes).clamp(1, 64);
let cluster_len = parts
.len()
.div_ceil(max_clusters)
.max(64);
let cluster_len = if storage_instancing {
parts.len().div_ceil(max_clusters).max(64)
} else {
1
};
for cluster in parts.chunks(cluster_len) {
if let Some((chunk, triangles, profile)) =
build_instanced_chunk(

View file

@ -2694,11 +2694,7 @@ impl Pipeline {
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&crate::scene::model::mesh_model::MeshLodSet>> = Vec::new();
for (index, set) in sets.iter().enumerate() {
let color = set
.lods
.first()
.map(|mesh| mesh.color)
.unwrap_or([0.0, 0.0, 0.0, 1.0]);
let color = set.display_color().unwrap_or([0.0, 0.0, 0.0, 1.0]);
let key = match (&set.instance_source, set.instance_transform) {
(Some(source), Some(transform)) => {
let matrix = &transform.matrix.m;
@ -2771,11 +2767,7 @@ impl Pipeline {
)
})
.collect();
let color = source
.lods
.first()
.map(|mesh| mesh.color)
.unwrap_or([0.0, 0.0, 0.0, 1.0]);
let color = source.display_color().unwrap_or([0.0, 0.0, 0.0, 1.0]);
let mk = |w: glam::DVec3| -> SilhouetteVertex {
let (hx, hy, hz) = (w.x as f32, w.y as f32, w.z as f32);
SilhouetteVertex {
@ -2813,7 +2805,13 @@ impl Pipeline {
});
}
};
for generator in &source.curved_gens {
let generators = source
.instance_source
.as_ref()
.map_or(source.curved_gens.as_slice(), |instance| {
instance.curved_gens.as_slice()
});
for generator in generators {
let transformed;
let silhouette_source = if let Some(transform) = source.instance_transform {
let origin = transform.apply(acadrust::types::Vector3::ZERO);
@ -3259,12 +3257,7 @@ impl Pipeline {
}
let current_handles: rustc_hash::FxHashSet<_> = meshes
.iter()
.filter_map(|set| {
set.lods
.first()
.and_then(|mesh| mesh.name.parse::<u64>().ok())
.map(acadrust::Handle::new)
})
.filter_map(MeshLodSet::entity_handle)
.collect();
let changed: rustc_hash::FxHashSet<_> = changes
.iter()