diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 51718ad18..966c6cd1d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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, )>, >, - /// 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> = HashMap::default(); for (index, set) in meshes.iter().enumerate() { - let Some(value) = set - .lods - .first() - .and_then(|mesh| mesh.name.parse::().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::() 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>, + _candidate_handles: Option<&HashSet>, ) -> Vec { let meshes = self.interaction_meshes_arc(); let lookup = self.mesh_pick_lookup(&meshes); - let handles: Vec = candidate_handles.map_or_else( - || lookup.keys().copied().collect(), - |candidates| candidates.iter().copied().collect(), - ); + let handles: Vec = 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>, + _candidate_handles: Option<&HashSet>, ) -> Vec { let meshes = self.interaction_meshes_arc(); let lookup = self.mesh_pick_lookup(&meshes); - let handles: Vec = candidate_handles.map_or_else( - || lookup.keys().copied().collect(), - |candidates| candidates.iter().copied().collect(), - ); + let handles: Vec = 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>, + _candidate_handles: Option<&HashSet>, coarse_lod: bool, ) -> Option { - // 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::() - .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>, + _candidate_handles: Option<&HashSet>, ) -> Vec { 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 = candidate_handles.map_or_else( - || lookup.keys().copied().collect(), - |candidates| candidates.iter().copied().collect(), - ); + let handles: Vec = 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>, + _candidate_handles: Option<&HashSet>, ) -> Vec { 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 = candidate_handles.map_or_else( - || lookup.keys().copied().collect(), - |candidates| candidates.iter().copied().collect(), - ); + let handles: Vec = 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, diff --git a/src/scene/model/material_model.rs b/src/scene/model/material_model.rs index 625b1471e..f1c5e1af6 100644 --- a/src/scene/model/material_model.rs +++ b/src/scene/model/material_model.rs @@ -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 = set - .lods + .geometry_lods() .iter() .flat_map(|lod| lod.triangle_material_handles.iter().flatten().copied()) .collect(); diff --git a/src/scene/model/mesh_model.rs b/src/scene/model/mesh_model.rs index 398a7903a..97ee55441 100644 --- a/src/scene/model/mesh_model.rs +++ b/src/scene/model/mesh_model.rs @@ -104,6 +104,12 @@ pub struct MeshLodSet { pub instance_source: Option>, /// Accumulated block-local → world transform for this rendered instance. pub instance_transform: Option, + /// Parent INSERT selected for this rendered block instance. + pub instance_handle: Option, + /// 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, pub edge_verts: Vec<[f32; 3]>, pub edge_verts_low: Vec<[f32; 3]>, + pub curved_gens: Vec, } /// 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 { + self.instance_handle.or_else(|| { + self.lods + .first() + .and_then(|mesh| mesh.name.parse::().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) + }) } } diff --git a/src/scene/pick/hit_test.rs b/src/scene/pick/hit_test.rs index 464247abc..ea61460cc 100644 --- a/src/scene/pick/hit_test.rs +++ b/src/scene/pick/hit_test.rs @@ -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, + meshes: impl Iterator< + Item = ( + Handle, + &'a MeshModel, + Option, + [f64; 6], + ), + >, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle, ) -> Option { - 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 { + 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, 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, + meshes: impl Iterator< + Item = ( + Handle, + &'a MeshModel, + Option, + ), + >, 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, + meshes: impl Iterator< + Item = ( + Handle, + &'a MeshModel, + Option, + ), + >, 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; } diff --git a/src/scene/pipeline/mesh_gpu.rs b/src/scene/pipeline/mesh_gpu.rs index 2c3f2e0e5..4f6f4d996 100644 --- a/src/scene/pipeline/mesh_gpu.rs +++ b/src/scene/pipeline/mesh_gpu.rs @@ -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, + 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::() - .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>> = 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::() - .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, ), > = 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>, > = 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::()) + <= hard_budget + && part.indices.len().saturating_mul(2 * std::mem::size_of::()) + <= 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::()) + <= 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::()) - .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::()) - }), - ); - 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::() - .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 // 64–256 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( diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 8a3ce25a5..a5307afc6 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -2694,11 +2694,7 @@ impl Pipeline { let mut slots = rustc_hash::FxHashMap::default(); let mut groups: Vec> = 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::().ok()) - .map(acadrust::Handle::new) - }) + .filter_map(MeshLodSet::entity_handle) .collect(); let changed: rustc_hash::FxHashSet<_> = changes .iter()