manifold-rust 0.9.1

Pure Rust port of the Manifold 3D geometry library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
// sort.rs — Phase 5: SortGeometry, Morton codes, vertex/face sorting
//
// Ports src/sort.cpp from the Manifold C++ library.
// The Collider is stubbed (Phase 10 will implement it fully).

use crate::linalg::Vec3;
use crate::types::{Box as BBox, Halfedge};
use crate::impl_mesh::ManifoldImpl;

// -----------------------------------------------------------------------
// Morton code (30-bit, 10 bits per axis)
// -----------------------------------------------------------------------

const K_NO_CODE: u32 = 0xFFFF_FFFFu32;

/// Spread the low 10 bits of v into bits 0,3,6,9,...,27 (every 3rd bit).
/// This is the inverse of the interleaving needed for a 3D Morton code.
#[inline]
fn spread_bits3(mut v: u32) -> u32 {
    v = 0xFF0000FFu32 & v.wrapping_mul(0x00010001u32);
    v = 0x0F00F00Fu32 & v.wrapping_mul(0x00000101u32);
    v = 0xC30C30C3u32 & v.wrapping_mul(0x00000011u32);
    v = 0x49249249u32 & v.wrapping_mul(0x00000005u32);
    v
}

/// Compute a 30-bit Morton code for a position within the given bounding box.
/// Returns K_NO_CODE for NaN positions (unreferenced vertices).
pub fn morton_code(position: Vec3, bbox: &BBox) -> u32 {
    if position.x.is_nan() {
        return K_NO_CODE;
    }
    morton_code_impl(position, bbox)
}

fn morton_code_impl(position: Vec3, bbox: &BBox) -> u32 {
    let range = bbox.max - bbox.min;
    let xyz = (position - bbox.min) / range;
    let x_f = (1024.0 * xyz.x).min(1023.0).max(0.0);
    let y_f = (1024.0 * xyz.y).min(1023.0).max(0.0);
    let z_f = (1024.0 * xyz.z).min(1023.0).max(0.0);
    let x = spread_bits3(x_f as u32);
    let y = spread_bits3(y_f as u32);
    let z = spread_bits3(z_f as u32);
    x * 4 + y * 2 + z
}

// -----------------------------------------------------------------------
// SortVerts
// -----------------------------------------------------------------------

/// Sorts vertices by their Morton code and removes NaN-flagged vertices.
/// Updates all halfedge vertex references accordingly.
pub fn sort_verts(mesh: &mut ManifoldImpl) {
    let num_vert = mesh.vert_pos.len();
    let bbox = mesh.bbox;

    // Compute Morton code for each vertex
    let vert_morton: Vec<u32> = mesh.vert_pos.iter()
        .map(|&p| morton_code(p, &bbox))
        .collect();

    // Build sorted index array
    let mut vert_new2old: Vec<i32> = (0..num_vert as i32).collect();
    vert_new2old.sort_by(|&a, &b| vert_morton[a as usize].cmp(&vert_morton[b as usize]));

    // Find how many survive (NaN verts get K_NO_CODE, sort to end)
    let new_num_vert = vert_new2old.partition_point(|&v| vert_morton[v as usize] < K_NO_CODE);
    let vert_new2old_trimmed = &vert_new2old[..new_num_vert];

    reindex_verts(mesh, vert_new2old_trimmed, num_vert);

    // Permute vert positions (only surviving verts)
    let old_pos = mesh.vert_pos.clone();
    mesh.vert_pos.resize(new_num_vert, Vec3::new(0.0, 0.0, 0.0));
    for (new_idx, &old_idx) in vert_new2old_trimmed.iter().enumerate() {
        mesh.vert_pos[new_idx] = old_pos[old_idx as usize];
    }

    // Permute vert normals if present
    if mesh.vert_normal.len() == num_vert {
        let old_n = mesh.vert_normal.clone();
        mesh.vert_normal.resize(new_num_vert, Vec3::new(0.0, 0.0, 0.0));
        for (new_idx, &old_idx) in vert_new2old_trimmed.iter().enumerate() {
            mesh.vert_normal[new_idx] = old_n[old_idx as usize];
        }
    }
}

/// Updates halfedge start/end vert indices from old→new index mapping.
/// `vert_new2old[new] = old` — we invert to get `vert_old2new[old] = new`.
pub fn reindex_verts(mesh: &mut ManifoldImpl, vert_new2old: &[i32], old_num_vert: usize) {
    let mut vert_old2new = vec![-1i32; old_num_vert];
    for (new_idx, &old_idx) in vert_new2old.iter().enumerate() {
        vert_old2new[old_idx as usize] = new_idx as i32;
    }
    let has_prop = mesh.num_prop > 0;
    for edge in mesh.halfedge.iter_mut() {
        if edge.start_vert < 0 {
            continue;
        }
        edge.start_vert = vert_old2new[edge.start_vert as usize];
        edge.end_vert = vert_old2new[edge.end_vert as usize];
        if !has_prop {
            edge.prop_vert = edge.start_vert;
        }
    }
}

// -----------------------------------------------------------------------
// GetFaceBoxMorton
// -----------------------------------------------------------------------

/// Computes per-face bounding boxes and Morton codes.
/// Faces with removed halfedges (pairedHalfedge < 0) get K_NO_CODE.
pub fn get_face_box_morton(mesh: &ManifoldImpl) -> (Vec<BBox>, Vec<u32>) {
    let num_tri = mesh.num_tri();
    let bbox = mesh.bbox;
    let mut face_box = vec![BBox::default(); num_tri];
    let mut face_morton = vec![0u32; num_tri];

    for face in 0..num_tri {
        if mesh.halfedge[3 * face].paired_halfedge < 0 {
            face_morton[face] = K_NO_CODE;
            continue;
        }
        let mut center = Vec3::new(0.0, 0.0, 0.0);
        for i in 0..3 {
            let pos = mesh.vert_pos[mesh.halfedge[3 * face + i].start_vert as usize];
            center = center + pos;
            face_box[face].union_point(pos);
        }
        center = center / 3.0;
        face_morton[face] = morton_code_impl(center, &bbox);
    }

    (face_box, face_morton)
}

// -----------------------------------------------------------------------
// SortFaces / GatherFaces
// -----------------------------------------------------------------------

/// Sorts faces by Morton code, removing faces flagged for removal (K_NO_CODE).
/// Updates `face_box` and `face_morton` in-place.
pub fn sort_faces(mesh: &mut ManifoldImpl, face_box: &mut Vec<BBox>, face_morton: &mut Vec<u32>) {
    let num_tri = face_box.len();
    let mut face_new2old: Vec<usize> = (0..num_tri).collect();

    // Stable sort by Morton code (removed tris get K_NO_CODE → sorted last)
    face_new2old.sort_by(|&a, &b| face_morton[a].cmp(&face_morton[b]));

    // Trim removed faces
    let new_num_tri = face_new2old.partition_point(|&f| face_morton[f] < K_NO_CODE);
    face_new2old.truncate(new_num_tri);

    // Permute face_morton and face_box to match new order
    let old_morton = face_morton.clone();
    let old_box = face_box.clone();
    face_morton.resize(new_num_tri, 0);
    face_box.resize(new_num_tri, BBox::default());
    for (new_f, &old_f) in face_new2old.iter().enumerate() {
        face_morton[new_f] = old_morton[old_f];
        face_box[new_f] = old_box[old_f];
    }

    gather_faces(mesh, &face_new2old);
}

/// Reorders halfedges (and related arrays) according to face_new2old permutation.
pub fn gather_faces(mesh: &mut ManifoldImpl, face_new2old: &[usize]) {
    let num_tri = face_new2old.len();
    let old_num_tri = mesh.num_tri();

    // Permute tri_ref if present
    if mesh.mesh_relation.tri_ref.len() == old_num_tri {
        let old_tri_ref = mesh.mesh_relation.tri_ref.clone();
        mesh.mesh_relation.tri_ref.resize(num_tri, Default::default());
        for (new_f, &old_f) in face_new2old.iter().enumerate() {
            mesh.mesh_relation.tri_ref[new_f] = old_tri_ref[old_f];
        }
    }

    // Permute face normals if present
    if mesh.face_normal.len() == old_num_tri {
        let old_normals = mesh.face_normal.clone();
        mesh.face_normal.resize(num_tri, Vec3::new(0.0, 0.0, 0.0));
        for (new_f, &old_f) in face_new2old.iter().enumerate() {
            mesh.face_normal[new_f] = old_normals[old_f];
        }
    }

    // Build faceOld2New for pairedHalfedge remapping
    let mut face_old2new = vec![-1i32; old_num_tri];
    for (new_f, &old_f) in face_new2old.iter().enumerate() {
        face_old2new[old_f] = new_f as i32;
    }

    // Gather halfedges from old layout into new
    let old_halfedge = mesh.halfedge.clone();
    let old_tangent = mesh.halfedge_tangent.clone();
    let has_tangent = !old_tangent.is_empty();

    mesh.halfedge.resize(3 * num_tri, Halfedge::default());
    if has_tangent {
        mesh.halfedge_tangent.resize(3 * num_tri, Default::default());
    }

    for new_face in 0..num_tri {
        let old_face = face_new2old[new_face];
        for i in 0..3 {
            let old_edge_idx = 3 * old_face + i;
            let new_edge_idx = 3 * new_face + i;
            let mut edge = old_halfedge[old_edge_idx];
            // Remap pairedHalfedge
            if edge.paired_halfedge >= 0 {
                let paired_old_face = (edge.paired_halfedge / 3) as usize;
                let offset = edge.paired_halfedge % 3;
                edge.paired_halfedge = 3 * face_old2new[paired_old_face] + offset;
            }
            mesh.halfedge[new_edge_idx] = edge;
            if has_tangent {
                mesh.halfedge_tangent[new_edge_idx] = old_tangent[old_edge_idx];
            }
        }
    }
}

// -----------------------------------------------------------------------
// CompactProps
// -----------------------------------------------------------------------

/// Removes unreferenced property vertices and reindexes propVerts.
pub fn compact_props(mesh: &mut ManifoldImpl) {
    if mesh.num_prop == 0 {
        return;
    }
    let num_prop = mesh.num_prop;
    let num_prop_verts = mesh.properties.len() / num_prop;

    // Mark which prop verts are referenced
    let mut keep = vec![false; num_prop_verts];
    for edge in &mesh.halfedge {
        if edge.prop_vert >= 0 && (edge.prop_vert as usize) < num_prop_verts {
            keep[edge.prop_vert as usize] = true;
        }
    }

    // Build prefix sum for old→new mapping
    let mut prop_old2new = vec![0i32; num_prop_verts + 1];
    for i in 0..num_prop_verts {
        prop_old2new[i + 1] = prop_old2new[i] + if keep[i] { 1 } else { 0 };
    }
    let new_num_prop_verts = prop_old2new[num_prop_verts] as usize;

    // Compact properties array
    let old_prop = mesh.properties.clone();
    mesh.properties.resize(num_prop * new_num_prop_verts, 0.0);
    for old_idx in 0..num_prop_verts {
        if !keep[old_idx] {
            continue;
        }
        let new_idx = prop_old2new[old_idx] as usize;
        for p in 0..num_prop {
            mesh.properties[new_idx * num_prop + p] = old_prop[old_idx * num_prop + p];
        }
    }

    // Remap propVert indices in halfedges
    for edge in mesh.halfedge.iter_mut() {
        if edge.prop_vert >= 0 {
            edge.prop_vert = prop_old2new[edge.prop_vert as usize];
        }
    }
}

// -----------------------------------------------------------------------
// SortGeometry — main entry point
// -----------------------------------------------------------------------

/// Sorts vertices and faces by Morton code, removes flagged-for-deletion
/// elements, and compacts property arrays. Should be called after
/// `create_halfedges()` to finalize the mesh topology.
///
/// Note: Collider construction is done in Phase 10.
pub fn sort_geometry(mesh: &mut ManifoldImpl) {
    if mesh.halfedge.is_empty() {
        return;
    }
    sort_verts(mesh);
    let (mut face_box, mut face_morton) = get_face_box_morton(mesh);
    sort_faces(mesh, &mut face_box, &mut face_morton);
    if mesh.halfedge.is_empty() {
        return;
    }
    compact_props(mesh);

    debug_assert!(
        mesh.halfedge.len() % 6 == 0,
        "Not an even number of halfedges after sorting (expected multiple of 6, got {})",
        mesh.halfedge.len()
    );
}

// -----------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::linalg::Mat3x4;
    use crate::impl_mesh::ManifoldImpl;

    #[test]
    fn test_spread_bits3() {
        // SpreadBits3(0b1111111111) should interleave into alternating positions
        // Values verified against C++ constexpr evaluation
        assert_eq!(spread_bits3(0), 0);
        assert_eq!(spread_bits3(1), 1);
        // Each bit of the input lands 3 positions apart in the output
        assert_eq!(spread_bits3(0b10), 0b1000);
        assert_eq!(spread_bits3(0b11), 0b1001);
        assert_eq!(spread_bits3(0b100), 0b1000000);
    }

    #[test]
    fn test_morton_code_basic() {
        let bbox = BBox {
            min: Vec3::new(0.0, 0.0, 0.0),
            max: Vec3::new(1.0, 1.0, 1.0),
        };
        // Origin → all zeros → code 0
        let code_origin = morton_code(Vec3::new(0.0, 0.0, 0.0), &bbox);
        assert_eq!(code_origin, 0);

        // NaN → K_NO_CODE
        let code_nan = morton_code(Vec3::new(f64::NAN, 0.0, 0.0), &bbox);
        assert_eq!(code_nan, K_NO_CODE);

        // Center of cube should be a positive code less than K_NO_CODE
        let code_center = morton_code(Vec3::new(0.5, 0.5, 0.5), &bbox);
        assert!(code_center > 0 && code_center < K_NO_CODE);
    }

    #[test]
    fn test_morton_code_ordering() {
        // Points closer together should (generally) have closer Morton codes.
        // More specifically: points sorted by Morton code produce a Z-curve traversal.
        let bbox = BBox {
            min: Vec3::new(0.0, 0.0, 0.0),
            max: Vec3::new(8.0, 8.0, 8.0),
        };
        let p0 = morton_code(Vec3::new(0.0, 0.0, 0.0), &bbox);
        let p1 = morton_code(Vec3::new(1.0, 0.0, 0.0), &bbox);
        let p2 = morton_code(Vec3::new(2.0, 0.0, 0.0), &bbox);
        // These should be strictly increasing along x with y=z=0
        assert!(p0 < p1);
        assert!(p1 < p2);
    }

    #[test]
    fn test_sort_geometry_tetrahedron() {
        let mut m = ManifoldImpl::tetrahedron(&Mat3x4::identity());
        // Should have 4 vertices and 12 halfedges (4 triangles) before sort
        assert_eq!(m.vert_pos.len(), 4);
        assert_eq!(m.halfedge.len(), 12);
        sort_geometry(&mut m);
        // Sort shouldn't remove any valid verts/faces
        assert_eq!(m.vert_pos.len(), 4);
        assert_eq!(m.halfedge.len(), 12);
    }

    #[test]
    fn test_sort_geometry_cube() {
        let mut m = ManifoldImpl::cube(&Mat3x4::identity());
        let vert_count = m.vert_pos.len();
        let halfedge_count = m.halfedge.len();
        sort_geometry(&mut m);
        assert_eq!(m.vert_pos.len(), vert_count);
        assert_eq!(m.halfedge.len(), halfedge_count);
        // After sort, paired halfedges should still be valid
        for (i, edge) in m.halfedge.iter().enumerate() {
            assert!(edge.paired_halfedge >= 0,
                "halfedge {} has invalid paired_halfedge {}", i, edge.paired_halfedge);
            let paired = &m.halfedge[edge.paired_halfedge as usize];
            assert_eq!(paired.paired_halfedge, i as i32,
                "halfedge {} paired -> {} but paired doesn't point back", i, edge.paired_halfedge);
        }
    }

    #[test]
    fn test_reindex_verts_identity() {
        let mut m = ManifoldImpl::tetrahedron(&Mat3x4::identity());
        let n = m.vert_pos.len();
        // Identity permutation should not change anything
        let identity: Vec<i32> = (0..n as i32).collect();
        let before: Vec<_> = m.halfedge.iter().map(|e| (e.start_vert, e.end_vert)).collect();
        reindex_verts(&mut m, &identity, n);
        let after: Vec<_> = m.halfedge.iter().map(|e| (e.start_vert, e.end_vert)).collect();
        assert_eq!(before, after);
    }

    #[test]
    fn test_sort_faces_manifold_preserved() {
        // After sort_geometry, mesh must still be 2-manifold
        let mut m = ManifoldImpl::cube(&Mat3x4::identity());
        sort_geometry(&mut m);
        assert!(m.is_2_manifold());
    }
}