Skip to main content

ifc_lite_geometry/
mesh.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Mesh data structures
6
7use nalgebra::{Point3, Vector3};
8
9/// Coordinate shift for RTC (Relative-to-Center) rendering
10/// Stores the offset subtracted from coordinates to improve Float32 precision
11#[derive(Debug, Clone, Copy, Default)]
12pub struct CoordinateShift {
13    /// X offset (subtracted from all X coordinates)
14    pub x: f64,
15    /// Y offset (subtracted from all Y coordinates)
16    pub y: f64,
17    /// Z offset (subtracted from all Z coordinates)
18    pub z: f64,
19}
20
21impl CoordinateShift {
22    /// Create a new coordinate shift
23    #[inline]
24    pub fn new(x: f64, y: f64, z: f64) -> Self {
25        Self { x, y, z }
26    }
27
28    /// Create shift from a Point3
29    #[inline]
30    pub fn from_point(point: Point3<f64>) -> Self {
31        Self {
32            x: point.x,
33            y: point.y,
34            z: point.z,
35        }
36    }
37
38    /// Check if shift is significant (>10km from origin)
39    #[inline]
40    pub fn is_significant(&self) -> bool {
41        const THRESHOLD: f64 = 10000.0; // 10km
42        self.x.abs() > THRESHOLD || self.y.abs() > THRESHOLD || self.z.abs() > THRESHOLD
43    }
44
45    /// Check if shift is zero (no shifting needed)
46    #[inline]
47    pub fn is_zero(&self) -> bool {
48        self.x == 0.0 && self.y == 0.0 && self.z == 0.0
49    }
50}
51
52/// Triangle mesh
53#[derive(Debug, Clone)]
54pub struct Mesh {
55    /// Vertex positions (x, y, z)
56    pub positions: Vec<f32>,
57    /// Vertex normals (nx, ny, nz)
58    pub normals: Vec<f32>,
59    /// Triangle indices (i0, i1, i2)
60    pub indices: Vec<u32>,
61}
62
63/// A sub-mesh with its source geometry item ID.
64/// Used to track which geometry items contribute to an element's mesh,
65/// allowing per-item color/style lookup.
66#[derive(Debug, Clone)]
67pub struct SubMesh {
68    /// The geometry item ID (e.g., IfcFacetedBrep ID) for style lookup
69    pub geometry_id: u32,
70    /// The triangulated mesh data
71    pub mesh: Mesh,
72}
73
74impl SubMesh {
75    /// Create a new sub-mesh
76    pub fn new(geometry_id: u32, mesh: Mesh) -> Self {
77        Self { geometry_id, mesh }
78    }
79}
80
81/// Collection of sub-meshes from an element, preserving per-item identity
82#[derive(Debug, Clone, Default)]
83pub struct SubMeshCollection {
84    pub sub_meshes: Vec<SubMesh>,
85}
86
87impl SubMeshCollection {
88    /// Create a new empty collection
89    pub fn new() -> Self {
90        Self { sub_meshes: Vec::new() }
91    }
92
93    /// Add a sub-mesh
94    pub fn add(&mut self, geometry_id: u32, mesh: Mesh) {
95        if !mesh.is_empty() {
96            self.sub_meshes.push(SubMesh::new(geometry_id, mesh));
97        }
98    }
99
100    /// Check if collection is empty
101    pub fn is_empty(&self) -> bool {
102        self.sub_meshes.is_empty()
103    }
104
105    /// Get number of sub-meshes
106    pub fn len(&self) -> usize {
107        self.sub_meshes.len()
108    }
109
110    /// Merge all sub-meshes into a single mesh (loses per-item identity)
111    pub fn into_combined_mesh(self) -> Mesh {
112        let mut combined = Mesh::new();
113        for sub in self.sub_meshes {
114            combined.merge(&sub.mesh);
115        }
116        combined
117    }
118
119    /// Iterate over sub-meshes
120    pub fn iter(&self) -> impl Iterator<Item = &SubMesh> {
121        self.sub_meshes.iter()
122    }
123}
124
125impl Mesh {
126    /// Create a new empty mesh
127    pub fn new() -> Self {
128        Self {
129            positions: Vec::new(),
130            normals: Vec::new(),
131            indices: Vec::new(),
132        }
133    }
134
135    /// Create a mesh with capacity
136    pub fn with_capacity(vertex_count: usize, index_count: usize) -> Self {
137        Self {
138            positions: Vec::with_capacity(vertex_count * 3),
139            normals: Vec::with_capacity(vertex_count * 3),
140            indices: Vec::with_capacity(index_count),
141        }
142    }
143    
144    /// Create a mesh from a single triangle
145    pub fn from_triangle(v0: &Point3<f64>, v1: &Point3<f64>, v2: &Point3<f64>, normal: &Vector3<f64>) -> Self {
146        let mut mesh = Self::with_capacity(3, 3);
147        mesh.positions = vec![
148            v0.x as f32, v0.y as f32, v0.z as f32,
149            v1.x as f32, v1.y as f32, v1.z as f32,
150            v2.x as f32, v2.y as f32, v2.z as f32,
151        ];
152        mesh.normals = vec![
153            normal.x as f32, normal.y as f32, normal.z as f32,
154            normal.x as f32, normal.y as f32, normal.z as f32,
155            normal.x as f32, normal.y as f32, normal.z as f32,
156        ];
157        mesh.indices = vec![0, 1, 2];
158        mesh
159    }
160
161    /// Add a vertex with normal
162    #[inline]
163    pub fn add_vertex(&mut self, position: Point3<f64>, normal: Vector3<f64>) {
164        self.positions.push(position.x as f32);
165        self.positions.push(position.y as f32);
166        self.positions.push(position.z as f32);
167
168        self.normals.push(normal.x as f32);
169        self.normals.push(normal.y as f32);
170        self.normals.push(normal.z as f32);
171    }
172
173    /// Add a vertex with normal, applying coordinate shift in f64 BEFORE f32 conversion
174    /// This preserves precision for large coordinates (georeferenced models)
175    ///
176    /// # Arguments
177    /// * `position` - Vertex position in world coordinates (f64)
178    /// * `normal` - Vertex normal
179    /// * `shift` - Coordinate shift to subtract (in f64) before converting to f32
180    ///
181    /// # Precision
182    /// For coordinates like 5,000,000m (Swiss UTM), direct f32 conversion loses ~1m precision.
183    /// By subtracting the centroid first (in f64), we convert small values (0-100m range)
184    /// which preserves sub-millimeter precision.
185    #[inline]
186    pub fn add_vertex_with_shift(
187        &mut self,
188        position: Point3<f64>,
189        normal: Vector3<f64>,
190        shift: &CoordinateShift,
191    ) {
192        // Subtract shift in f64 precision BEFORE converting to f32
193        // This is the key to preserving precision for large coordinates
194        let shifted_x = position.x - shift.x;
195        let shifted_y = position.y - shift.y;
196        let shifted_z = position.z - shift.z;
197
198        self.positions.push(shifted_x as f32);
199        self.positions.push(shifted_y as f32);
200        self.positions.push(shifted_z as f32);
201
202        self.normals.push(normal.x as f32);
203        self.normals.push(normal.y as f32);
204        self.normals.push(normal.z as f32);
205    }
206
207    /// Apply coordinate shift to existing positions in-place
208    /// Uses f64 intermediate for precision when subtracting large offsets
209    #[inline]
210    pub fn apply_shift(&mut self, shift: &CoordinateShift) {
211        if shift.is_zero() {
212            return;
213        }
214        for chunk in self.positions.chunks_exact_mut(3) {
215            // Convert to f64, subtract, convert back to f32
216            chunk[0] = (chunk[0] as f64 - shift.x) as f32;
217            chunk[1] = (chunk[1] as f64 - shift.y) as f32;
218            chunk[2] = (chunk[2] as f64 - shift.z) as f32;
219        }
220    }
221
222    /// Add a triangle
223    #[inline]
224    pub fn add_triangle(&mut self, i0: u32, i1: u32, i2: u32) {
225        self.indices.push(i0);
226        self.indices.push(i1);
227        self.indices.push(i2);
228    }
229
230    /// Merge another mesh into this one
231    #[inline]
232    pub fn merge(&mut self, other: &Mesh) {
233        if other.is_empty() {
234            return;
235        }
236
237        let vertex_offset = (self.positions.len() / 3) as u32;
238
239        // Pre-allocate for the incoming data
240        self.positions.reserve(other.positions.len());
241        self.normals.reserve(other.normals.len());
242        self.indices.reserve(other.indices.len());
243
244        self.positions.extend_from_slice(&other.positions);
245        self.normals.extend_from_slice(&other.normals);
246
247        // Vectorized index offset - more cache-friendly than loop
248        self.indices
249            .extend(other.indices.iter().map(|&i| i + vertex_offset));
250    }
251
252    /// Batch merge multiple meshes at once (more efficient than individual merges)
253    #[inline]
254    pub fn merge_all(&mut self, meshes: &[Mesh]) {
255        // Calculate total size needed
256        let total_positions: usize = meshes.iter().map(|m| m.positions.len()).sum();
257        let total_indices: usize = meshes.iter().map(|m| m.indices.len()).sum();
258
259        // Reserve capacity upfront to avoid reallocations
260        self.positions.reserve(total_positions);
261        self.normals.reserve(total_positions);
262        self.indices.reserve(total_indices);
263
264        // Merge all meshes
265        for mesh in meshes {
266            if !mesh.is_empty() {
267                let vertex_offset = (self.positions.len() / 3) as u32;
268                self.positions.extend_from_slice(&mesh.positions);
269                self.normals.extend_from_slice(&mesh.normals);
270                self.indices
271                    .extend(mesh.indices.iter().map(|&i| i + vertex_offset));
272            }
273        }
274    }
275
276    /// Get vertex count
277    #[inline]
278    pub fn vertex_count(&self) -> usize {
279        self.positions.len() / 3
280    }
281
282    /// Get triangle count
283    #[inline]
284    pub fn triangle_count(&self) -> usize {
285        self.indices.len() / 3
286    }
287
288    /// Check if mesh is empty
289    #[inline]
290    pub fn is_empty(&self) -> bool {
291        self.positions.is_empty()
292    }
293
294    /// Calculate bounds (min, max) - optimized with chunk iteration
295    #[inline]
296    pub fn bounds(&self) -> (Point3<f32>, Point3<f32>) {
297        if self.is_empty() {
298            return (Point3::origin(), Point3::origin());
299        }
300
301        let mut min = Point3::new(f32::MAX, f32::MAX, f32::MAX);
302        let mut max = Point3::new(f32::MIN, f32::MIN, f32::MIN);
303
304        // Use chunks for better cache locality
305        self.positions.chunks_exact(3).for_each(|chunk| {
306            let (x, y, z) = (chunk[0], chunk[1], chunk[2]);
307            min.x = min.x.min(x);
308            min.y = min.y.min(y);
309            min.z = min.z.min(z);
310            max.x = max.x.max(x);
311            max.y = max.y.max(y);
312            max.z = max.z.max(z);
313        });
314
315        (min, max)
316    }
317
318    /// Calculate centroid in f64 precision (for RTC offset calculation)
319    /// Returns the average of all vertex positions
320    #[inline]
321    pub fn centroid_f64(&self) -> Point3<f64> {
322        if self.is_empty() {
323            return Point3::origin();
324        }
325
326        let mut sum = Point3::new(0.0f64, 0.0f64, 0.0f64);
327        let count = self.positions.len() / 3;
328
329        self.positions.chunks_exact(3).for_each(|chunk| {
330            sum.x += chunk[0] as f64;
331            sum.y += chunk[1] as f64;
332            sum.z += chunk[2] as f64;
333        });
334
335        Point3::new(
336            sum.x / count as f64,
337            sum.y / count as f64,
338            sum.z / count as f64,
339        )
340    }
341
342    /// Clear the mesh
343    #[inline]
344    pub fn clear(&mut self) {
345        self.positions.clear();
346        self.normals.clear();
347        self.indices.clear();
348    }
349
350    /// Filter out triangles with edges exceeding the threshold
351    /// This removes "stretched" triangles that span unreasonably large distances,
352    /// which can occur when disconnected geometry is incorrectly merged.
353    ///
354    /// Uses a conservative threshold (500m) to only catch clearly broken geometry,
355    /// not legitimate large elements like long beams or walls.
356    ///
357    /// # Arguments
358    /// * `max_edge_length` - Maximum allowed edge length in meters (default: 500m)
359    ///
360    /// # Returns
361    /// Number of triangles removed
362    pub fn filter_stretched_triangles(&mut self, max_edge_length: f32) -> usize {
363        if self.is_empty() {
364            return 0;
365        }
366
367        let max_edge_sq = max_edge_length * max_edge_length;
368        let mut valid_indices = Vec::new();
369        let mut removed_count = 0;
370
371        // Check each triangle
372        for i in (0..self.indices.len()).step_by(3) {
373            let i0 = self.indices[i] as usize;
374            let i1 = self.indices[i + 1] as usize;
375            let i2 = self.indices[i + 2] as usize;
376
377            if i0 * 3 + 2 >= self.positions.len()
378                || i1 * 3 + 2 >= self.positions.len()
379                || i2 * 3 + 2 >= self.positions.len()
380            {
381                // Invalid indices - skip
382                removed_count += 1;
383                continue;
384            }
385
386            let p0 = (
387                self.positions[i0 * 3],
388                self.positions[i0 * 3 + 1],
389                self.positions[i0 * 3 + 2],
390            );
391            let p1 = (
392                self.positions[i1 * 3],
393                self.positions[i1 * 3 + 1],
394                self.positions[i1 * 3 + 2],
395            );
396            let p2 = (
397                self.positions[i2 * 3],
398                self.positions[i2 * 3 + 1],
399                self.positions[i2 * 3 + 2],
400            );
401
402            // Calculate squared edge lengths
403            let edge01_sq = (p1.0 - p0.0).powi(2) + (p1.1 - p0.1).powi(2) + (p1.2 - p0.2).powi(2);
404            let edge12_sq = (p2.0 - p1.0).powi(2) + (p2.1 - p1.1).powi(2) + (p2.2 - p1.2).powi(2);
405            let edge20_sq = (p0.0 - p2.0).powi(2) + (p0.1 - p2.1).powi(2) + (p0.2 - p2.2).powi(2);
406
407            // Check if any edge exceeds threshold
408            if edge01_sq <= max_edge_sq && edge12_sq <= max_edge_sq && edge20_sq <= max_edge_sq {
409                // Triangle is valid - keep it
410                valid_indices.push(self.indices[i]);
411                valid_indices.push(self.indices[i + 1]);
412                valid_indices.push(self.indices[i + 2]);
413            } else {
414                // Triangle has stretched edge - remove it
415                removed_count += 1;
416            }
417        }
418
419        self.indices = valid_indices;
420        removed_count
421    }
422}
423
424impl Default for Mesh {
425    fn default() -> Self {
426        Self::new()
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn test_mesh_creation() {
436        let mesh = Mesh::new();
437        assert!(mesh.is_empty());
438        assert_eq!(mesh.vertex_count(), 0);
439        assert_eq!(mesh.triangle_count(), 0);
440    }
441
442    #[test]
443    fn test_add_vertex() {
444        let mut mesh = Mesh::new();
445        mesh.add_vertex(Point3::new(1.0, 2.0, 3.0), Vector3::new(0.0, 0.0, 1.0));
446        assert_eq!(mesh.vertex_count(), 1);
447        assert_eq!(mesh.positions, vec![1.0, 2.0, 3.0]);
448        assert_eq!(mesh.normals, vec![0.0, 0.0, 1.0]);
449    }
450
451    #[test]
452    fn test_merge() {
453        let mut mesh1 = Mesh::new();
454        mesh1.add_vertex(Point3::new(0.0, 0.0, 0.0), Vector3::z());
455        mesh1.add_triangle(0, 1, 2);
456
457        let mut mesh2 = Mesh::new();
458        mesh2.add_vertex(Point3::new(1.0, 1.0, 1.0), Vector3::y());
459        mesh2.add_triangle(0, 1, 2);
460
461        mesh1.merge(&mesh2);
462        assert_eq!(mesh1.vertex_count(), 2);
463        assert_eq!(mesh1.triangle_count(), 2);
464    }
465
466    #[test]
467    fn test_coordinate_shift_creation() {
468        let shift = CoordinateShift::new(500000.0, 5000000.0, 100.0);
469        assert!(shift.is_significant());
470        assert!(!shift.is_zero());
471
472        let zero_shift = CoordinateShift::default();
473        assert!(!zero_shift.is_significant());
474        assert!(zero_shift.is_zero());
475    }
476
477    #[test]
478    fn test_add_vertex_with_shift_preserves_precision() {
479        // Test case: Swiss UTM coordinates (typical large coordinate scenario)
480        // Without shifting: 5000000.123 as f32 = 5000000.0 (loses 0.123m precision!)
481        // With shifting: (5000000.123 - 5000000.0) as f32 = 0.123 (full precision preserved)
482
483        let mut mesh = Mesh::new();
484
485        // Large coordinates typical of Swiss UTM (EPSG:2056)
486        let p1 = Point3::new(2679012.123456, 1247892.654321, 432.111);
487        let p2 = Point3::new(2679012.223456, 1247892.754321, 432.211);
488
489        // Create shift from approximate centroid
490        let shift = CoordinateShift::new(2679012.0, 1247892.0, 432.0);
491
492        mesh.add_vertex_with_shift(p1, Vector3::z(), &shift);
493        mesh.add_vertex_with_shift(p2, Vector3::z(), &shift);
494
495        // Verify shifted positions have sub-millimeter precision
496        // p1 shifted: (0.123456, 0.654321, 0.111)
497        // p2 shifted: (0.223456, 0.754321, 0.211)
498        assert!((mesh.positions[0] - 0.123456).abs() < 0.0001); // X1
499        assert!((mesh.positions[1] - 0.654321).abs() < 0.0001); // Y1
500        assert!((mesh.positions[2] - 0.111).abs() < 0.0001);    // Z1
501        assert!((mesh.positions[3] - 0.223456).abs() < 0.0001); // X2
502        assert!((mesh.positions[4] - 0.754321).abs() < 0.0001); // Y2
503        assert!((mesh.positions[5] - 0.211).abs() < 0.0001);    // Z2
504
505        // Verify relative distances are preserved with high precision
506        let dx = mesh.positions[3] - mesh.positions[0];
507        let dy = mesh.positions[4] - mesh.positions[1];
508        let dz = mesh.positions[5] - mesh.positions[2];
509
510        // Expected: dx=0.1, dy=0.1, dz=0.1
511        assert!((dx - 0.1).abs() < 0.0001);
512        assert!((dy - 0.1).abs() < 0.0001);
513        assert!((dz - 0.1).abs() < 0.0001);
514    }
515
516    #[test]
517    fn test_apply_shift_to_existing_mesh() {
518        let mut mesh = Mesh::new();
519
520        // Add vertices with large coordinates (already converted to f32 - some precision lost)
521        mesh.positions = vec![
522            500000.0, 5000000.0, 0.0,
523            500010.0, 5000010.0, 10.0,
524        ];
525        mesh.normals = vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0];
526
527        // Apply shift
528        let shift = CoordinateShift::new(500000.0, 5000000.0, 0.0);
529        mesh.apply_shift(&shift);
530
531        // Verify positions are shifted
532        assert!((mesh.positions[0] - 0.0).abs() < 0.001);
533        assert!((mesh.positions[1] - 0.0).abs() < 0.001);
534        assert!((mesh.positions[3] - 10.0).abs() < 0.001);
535        assert!((mesh.positions[4] - 10.0).abs() < 0.001);
536    }
537
538    #[test]
539    fn test_centroid_f64() {
540        let mut mesh = Mesh::new();
541        mesh.positions = vec![
542            0.0, 0.0, 0.0,
543            10.0, 10.0, 10.0,
544            20.0, 20.0, 20.0,
545        ];
546        mesh.normals = vec![0.0; 9];
547
548        let centroid = mesh.centroid_f64();
549        assert!((centroid.x - 10.0).abs() < 0.001);
550        assert!((centroid.y - 10.0).abs() < 0.001);
551        assert!((centroid.z - 10.0).abs() < 0.001);
552    }
553
554    #[test]
555    fn test_precision_comparison_shifted_vs_unshifted() {
556        // This test quantifies the precision improvement from shifting
557        // Using Swiss UTM coordinates as example
558
559        // Two points that are exactly 0.001m (1mm) apart
560        let base_x = 2679012.0;
561        let base_y = 1247892.0;
562        let offset = 0.001; // 1mm
563
564        let p1 = Point3::new(base_x, base_y, 0.0);
565        let p2 = Point3::new(base_x + offset, base_y, 0.0);
566
567        // Without shift - convert directly to f32
568        let p1_f32_direct = (p1.x as f32, p1.y as f32);
569        let p2_f32_direct = (p2.x as f32, p2.y as f32);
570        let diff_direct = p2_f32_direct.0 - p1_f32_direct.0;
571
572        // With shift - subtract centroid first, then convert
573        let shift = CoordinateShift::new(base_x, base_y, 0.0);
574        let p1_shifted = ((p1.x - shift.x) as f32, (p1.y - shift.y) as f32);
575        let p2_shifted = ((p2.x - shift.x) as f32, (p2.y - shift.y) as f32);
576        let diff_shifted = p2_shifted.0 - p1_shifted.0;
577
578        println!("Direct f32 difference (should be ~0.001): {}", diff_direct);
579        println!("Shifted f32 difference (should be ~0.001): {}", diff_shifted);
580
581        // The shifted version should be much closer to the true 1mm difference
582        let error_direct = (diff_direct - offset as f32).abs();
583        let error_shifted = (diff_shifted - offset as f32).abs();
584
585        println!("Error without shift: {}m", error_direct);
586        println!("Error with shift: {}m", error_shifted);
587
588        // The shifted version should have significantly less error
589        // (At least 100x better precision for typical Swiss coordinates)
590        assert!(
591            error_shifted < error_direct || error_shifted < 0.0001,
592            "Shifted precision should be better than direct conversion"
593        );
594    }
595}