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            if i + 2 >= self.indices.len() {
374                break;
375            }
376            let i0 = self.indices[i] as usize;
377            let i1 = self.indices[i + 1] as usize;
378            let i2 = self.indices[i + 2] as usize;
379
380            if i0 * 3 + 2 >= self.positions.len()
381                || i1 * 3 + 2 >= self.positions.len()
382                || i2 * 3 + 2 >= self.positions.len()
383            {
384                // Invalid indices - skip
385                removed_count += 1;
386                continue;
387            }
388
389            let p0 = (
390                self.positions[i0 * 3],
391                self.positions[i0 * 3 + 1],
392                self.positions[i0 * 3 + 2],
393            );
394            let p1 = (
395                self.positions[i1 * 3],
396                self.positions[i1 * 3 + 1],
397                self.positions[i1 * 3 + 2],
398            );
399            let p2 = (
400                self.positions[i2 * 3],
401                self.positions[i2 * 3 + 1],
402                self.positions[i2 * 3 + 2],
403            );
404
405            // Calculate squared edge lengths
406            let edge01_sq = (p1.0 - p0.0).powi(2) + (p1.1 - p0.1).powi(2) + (p1.2 - p0.2).powi(2);
407            let edge12_sq = (p2.0 - p1.0).powi(2) + (p2.1 - p1.1).powi(2) + (p2.2 - p1.2).powi(2);
408            let edge20_sq = (p0.0 - p2.0).powi(2) + (p0.1 - p2.1).powi(2) + (p0.2 - p2.2).powi(2);
409
410            // Check if any edge exceeds threshold
411            if edge01_sq <= max_edge_sq && edge12_sq <= max_edge_sq && edge20_sq <= max_edge_sq {
412                // Triangle is valid - keep it
413                valid_indices.push(self.indices[i]);
414                valid_indices.push(self.indices[i + 1]);
415                valid_indices.push(self.indices[i + 2]);
416            } else {
417                // Triangle has stretched edge - remove it
418                removed_count += 1;
419            }
420        }
421
422        self.indices = valid_indices;
423        removed_count
424    }
425}
426
427impl Default for Mesh {
428    fn default() -> Self {
429        Self::new()
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_mesh_creation() {
439        let mesh = Mesh::new();
440        assert!(mesh.is_empty());
441        assert_eq!(mesh.vertex_count(), 0);
442        assert_eq!(mesh.triangle_count(), 0);
443    }
444
445    #[test]
446    fn test_add_vertex() {
447        let mut mesh = Mesh::new();
448        mesh.add_vertex(Point3::new(1.0, 2.0, 3.0), Vector3::new(0.0, 0.0, 1.0));
449        assert_eq!(mesh.vertex_count(), 1);
450        assert_eq!(mesh.positions, vec![1.0, 2.0, 3.0]);
451        assert_eq!(mesh.normals, vec![0.0, 0.0, 1.0]);
452    }
453
454    #[test]
455    fn test_merge() {
456        let mut mesh1 = Mesh::new();
457        mesh1.add_vertex(Point3::new(0.0, 0.0, 0.0), Vector3::z());
458        mesh1.add_triangle(0, 1, 2);
459
460        let mut mesh2 = Mesh::new();
461        mesh2.add_vertex(Point3::new(1.0, 1.0, 1.0), Vector3::y());
462        mesh2.add_triangle(0, 1, 2);
463
464        mesh1.merge(&mesh2);
465        assert_eq!(mesh1.vertex_count(), 2);
466        assert_eq!(mesh1.triangle_count(), 2);
467    }
468
469    #[test]
470    fn test_coordinate_shift_creation() {
471        let shift = CoordinateShift::new(500000.0, 5000000.0, 100.0);
472        assert!(shift.is_significant());
473        assert!(!shift.is_zero());
474
475        let zero_shift = CoordinateShift::default();
476        assert!(!zero_shift.is_significant());
477        assert!(zero_shift.is_zero());
478    }
479
480    #[test]
481    fn test_add_vertex_with_shift_preserves_precision() {
482        // Test case: Swiss UTM coordinates (typical large coordinate scenario)
483        // Without shifting: 5000000.123 as f32 = 5000000.0 (loses 0.123m precision!)
484        // With shifting: (5000000.123 - 5000000.0) as f32 = 0.123 (full precision preserved)
485
486        let mut mesh = Mesh::new();
487
488        // Large coordinates typical of Swiss UTM (EPSG:2056)
489        let p1 = Point3::new(2679012.123456, 1247892.654321, 432.111);
490        let p2 = Point3::new(2679012.223456, 1247892.754321, 432.211);
491
492        // Create shift from approximate centroid
493        let shift = CoordinateShift::new(2679012.0, 1247892.0, 432.0);
494
495        mesh.add_vertex_with_shift(p1, Vector3::z(), &shift);
496        mesh.add_vertex_with_shift(p2, Vector3::z(), &shift);
497
498        // Verify shifted positions have sub-millimeter precision
499        // p1 shifted: (0.123456, 0.654321, 0.111)
500        // p2 shifted: (0.223456, 0.754321, 0.211)
501        assert!((mesh.positions[0] - 0.123456).abs() < 0.0001); // X1
502        assert!((mesh.positions[1] - 0.654321).abs() < 0.0001); // Y1
503        assert!((mesh.positions[2] - 0.111).abs() < 0.0001);    // Z1
504        assert!((mesh.positions[3] - 0.223456).abs() < 0.0001); // X2
505        assert!((mesh.positions[4] - 0.754321).abs() < 0.0001); // Y2
506        assert!((mesh.positions[5] - 0.211).abs() < 0.0001);    // Z2
507
508        // Verify relative distances are preserved with high precision
509        let dx = mesh.positions[3] - mesh.positions[0];
510        let dy = mesh.positions[4] - mesh.positions[1];
511        let dz = mesh.positions[5] - mesh.positions[2];
512
513        // Expected: dx=0.1, dy=0.1, dz=0.1
514        assert!((dx - 0.1).abs() < 0.0001);
515        assert!((dy - 0.1).abs() < 0.0001);
516        assert!((dz - 0.1).abs() < 0.0001);
517    }
518
519    #[test]
520    fn test_apply_shift_to_existing_mesh() {
521        let mut mesh = Mesh::new();
522
523        // Add vertices with large coordinates (already converted to f32 - some precision lost)
524        mesh.positions = vec![
525            500000.0, 5000000.0, 0.0,
526            500010.0, 5000010.0, 10.0,
527        ];
528        mesh.normals = vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0];
529
530        // Apply shift
531        let shift = CoordinateShift::new(500000.0, 5000000.0, 0.0);
532        mesh.apply_shift(&shift);
533
534        // Verify positions are shifted
535        assert!((mesh.positions[0] - 0.0).abs() < 0.001);
536        assert!((mesh.positions[1] - 0.0).abs() < 0.001);
537        assert!((mesh.positions[3] - 10.0).abs() < 0.001);
538        assert!((mesh.positions[4] - 10.0).abs() < 0.001);
539    }
540
541    #[test]
542    fn test_centroid_f64() {
543        let mut mesh = Mesh::new();
544        mesh.positions = vec![
545            0.0, 0.0, 0.0,
546            10.0, 10.0, 10.0,
547            20.0, 20.0, 20.0,
548        ];
549        mesh.normals = vec![0.0; 9];
550
551        let centroid = mesh.centroid_f64();
552        assert!((centroid.x - 10.0).abs() < 0.001);
553        assert!((centroid.y - 10.0).abs() < 0.001);
554        assert!((centroid.z - 10.0).abs() < 0.001);
555    }
556
557    #[test]
558    fn test_precision_comparison_shifted_vs_unshifted() {
559        // This test quantifies the precision improvement from shifting
560        // Using Swiss UTM coordinates as example
561
562        // Two points that are exactly 0.001m (1mm) apart
563        let base_x = 2679012.0;
564        let base_y = 1247892.0;
565        let offset = 0.001; // 1mm
566
567        let p1 = Point3::new(base_x, base_y, 0.0);
568        let p2 = Point3::new(base_x + offset, base_y, 0.0);
569
570        // Without shift - convert directly to f32
571        let p1_f32_direct = (p1.x as f32, p1.y as f32);
572        let p2_f32_direct = (p2.x as f32, p2.y as f32);
573        let diff_direct = p2_f32_direct.0 - p1_f32_direct.0;
574
575        // With shift - subtract centroid first, then convert
576        let shift = CoordinateShift::new(base_x, base_y, 0.0);
577        let p1_shifted = ((p1.x - shift.x) as f32, (p1.y - shift.y) as f32);
578        let p2_shifted = ((p2.x - shift.x) as f32, (p2.y - shift.y) as f32);
579        let diff_shifted = p2_shifted.0 - p1_shifted.0;
580
581        println!("Direct f32 difference (should be ~0.001): {}", diff_direct);
582        println!("Shifted f32 difference (should be ~0.001): {}", diff_shifted);
583
584        // The shifted version should be much closer to the true 1mm difference
585        let error_direct = (diff_direct - offset as f32).abs();
586        let error_shifted = (diff_shifted - offset as f32).abs();
587
588        println!("Error without shift: {}m", error_direct);
589        println!("Error with shift: {}m", error_shifted);
590
591        // The shifted version should have significantly less error
592        // (At least 100x better precision for typical Swiss coordinates)
593        assert!(
594            error_shifted < error_direct || error_shifted < 0.0001,
595            "Shifted precision should be better than direct conversion"
596        );
597    }
598}