Skip to main content

manifold_rust/
face_op.rs

1// face_op.rs — Phase 7a: Face normals, coplanarity, vertex normals
2//
3// Ports src/face_op.cpp, the face-normal and coplanarity portions of
4// src/impl.cpp (SetNormalsAndCoplanar, CalculateVertNormals), and the
5// GetAxisAlignedProjection utility from src/shared.h.
6
7use std::collections::BTreeMap;
8
9use crate::linalg::{Vec2, Vec3, cross, dot, normalize, length2};
10use crate::math;
11use crate::polygon::{ccw, triangulate_idx_halfedges, HalfedgeTriangulation};
12use crate::types::{next_halfedge, Halfedge, PolyVert, PolygonsIdx, TriRef};
13use crate::impl_mesh::ManifoldImpl;
14
15// -----------------------------------------------------------------------
16// Proj2x3 — 2-row, 3-column projection matrix (drops one axis)
17//
18// Used to project 3D mesh positions onto a 2D plane for CCW tests and
19// triangulation. Mirrors `mat2x3` in the C++ linalg.h library.
20// -----------------------------------------------------------------------
21
22/// A 2×3 projection matrix: maps Vec3 → Vec2 via dot products with two rows.
23#[derive(Clone, Copy, Debug)]
24pub struct Proj2x3 {
25    pub row0: Vec3,
26    pub row1: Vec3,
27}
28
29impl Proj2x3 {
30    /// Apply the projection: `[dot(row0, v), dot(row1, v)]`.
31    #[inline]
32    pub fn apply(&self, v: Vec3) -> Vec2 {
33        Vec2::new(dot(self.row0, v), dot(self.row1, v))
34    }
35}
36
37// -----------------------------------------------------------------------
38// GetAxisAlignedProjection
39// -----------------------------------------------------------------------
40
41/// Returns a projection matrix that drops the largest-magnitude axis of
42/// `normal`, producing a 2D view aligned with the face plane.
43///
44/// Mirrors `GetAxisAlignedProjection` in `src/shared.h`.
45pub fn get_axis_aligned_projection(normal: Vec3) -> Proj2x3 {
46    let abs = Vec3::new(normal.x.abs(), normal.y.abs(), normal.z.abs());
47
48    // mat3x2 columns (each col is a Vec3); transposed to get mat2x3 rows.
49    let (row0, row1, xyz_max) = if abs.z > abs.x && abs.z > abs.y {
50        // Drop Z, keep X and Y
51        (Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), normal.z)
52    } else if abs.y > abs.x {
53        // Drop Y, keep Z and X
54        (Vec3::new(0.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0), normal.y)
55    } else {
56        // Drop X, keep Y and Z
57        (Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 0.0, 1.0), normal.x)
58    };
59
60    // If the dominant axis is negative, flip the first row so that the
61    // projected winding order is consistent.
62    if xyz_max < 0.0 {
63        Proj2x3 { row0: Vec3::new(-row0.x, -row0.y, -row0.z), row1 }
64    } else {
65        Proj2x3 { row0, row1 }
66    }
67}
68
69// -----------------------------------------------------------------------
70// SetNormalsAndCoplanar
71// -----------------------------------------------------------------------
72
73/// Computes face normals and flood-fills coplanar face groups, then
74/// calls `calculate_vert_normals` to compute per-vertex normals.
75///
76/// Mirrors `Manifold::Impl::SetNormalsAndCoplanar()` in `src/impl.cpp`.
77pub fn set_normals_and_coplanar(mesh: &mut ManifoldImpl) {
78    let num_tri = mesh.num_tri();
79    mesh.face_normal.resize(num_tri, Vec3::new(0.0, 0.0, 1.0));
80
81    // Struct for sorting triangles by area
82    struct TriPriority {
83        area2: f64,
84        tri: usize,
85    }
86
87    // Compute face normals and priorities (sort largest faces first)
88    let mut tri_priority: Vec<TriPriority> = (0..num_tri)
89        .map(|tri| {
90            // Mark coplanarID as unset
91            if tri < mesh.mesh_relation.tri_ref.len() {
92                mesh.mesh_relation.tri_ref[tri].coplanar_id = -1;
93            }
94
95            if mesh.halfedge[3 * tri].start_vert < 0 {
96                return TriPriority { area2: 0.0, tri };
97            }
98            let v = mesh.vert_pos[mesh.halfedge[3 * tri].start_vert as usize];
99            let n = cross(
100                mesh.vert_pos[mesh.halfedge[3 * tri].end_vert as usize] - v,
101                mesh.vert_pos[mesh.halfedge[3 * tri + 1].end_vert as usize] - v,
102            );
103            let normal = normalize(n);
104            mesh.face_normal[tri] = if normal.x.is_nan() {
105                Vec3::new(0.0, 0.0, 1.0)
106            } else {
107                normal
108            };
109            TriPriority { area2: length2(n), tri }
110        })
111        .collect();
112
113    // Sort by area descending (largest triangles first → better coplanar seeds)
114    tri_priority.sort_by(|a, b| b.area2.partial_cmp(&a.area2).unwrap_or(std::cmp::Ordering::Equal));
115
116    // Flood-fill coplanar groups from each unassigned face
117    let mut interior_halfedges: Vec<usize> = Vec::new();
118    for tp in &tri_priority {
119        let tri = tp.tri;
120        if tri >= mesh.mesh_relation.tri_ref.len() {
121            continue;
122        }
123        if mesh.mesh_relation.tri_ref[tri].coplanar_id >= 0 {
124            continue;
125        }
126
127        mesh.mesh_relation.tri_ref[tri].coplanar_id = tri as i32;
128        if mesh.halfedge[3 * tri].start_vert < 0 {
129            continue;
130        }
131
132        let base = mesh.vert_pos[mesh.halfedge[3 * tri].start_vert as usize];
133        let normal = mesh.face_normal[tri];
134
135        interior_halfedges.clear();
136        interior_halfedges.push(3 * tri);
137        interior_halfedges.push(3 * tri + 1);
138        interior_halfedges.push(3 * tri + 2);
139
140        while let Some(h) = interior_halfedges.pop() {
141            let paired = mesh.halfedge[h].paired_halfedge;
142            if paired < 0 {
143                continue;
144            }
145            let h2 = next_halfedge(paired) as usize;
146            let h2_tri = h2 / 3;
147            if h2_tri >= mesh.mesh_relation.tri_ref.len() {
148                continue;
149            }
150            if mesh.mesh_relation.tri_ref[h2_tri].coplanar_id >= 0 {
151                continue;
152            }
153
154            let v = mesh.vert_pos[mesh.halfedge[h2].end_vert as usize];
155            if (dot(v - base, normal)).abs() < mesh.tolerance {
156                mesh.mesh_relation.tri_ref[h2_tri].coplanar_id = tri as i32;
157                mesh.face_normal[h2_tri] = normal;
158
159                // Avoid re-pushing paired interior halfedges (cancel out)
160                let last = interior_halfedges.last().copied();
161                if last == Some(mesh.halfedge[h2].paired_halfedge as usize) {
162                    interior_halfedges.pop();
163                } else {
164                    interior_halfedges.push(h2 as usize);
165                }
166                interior_halfedges.push(next_halfedge(h2 as i32) as usize);
167            }
168        }
169    }
170
171    calculate_vert_normals(mesh);
172}
173
174// -----------------------------------------------------------------------
175// CalculateVertNormals
176// -----------------------------------------------------------------------
177
178/// Computes per-vertex normals as angle-weighted averages of incident face normals.
179///
180/// Mirrors `Manifold::Impl::CalculateVertNormals()` in `src/impl.cpp`.
181pub fn calculate_vert_normals(mesh: &mut ManifoldImpl) {
182    let num_vert = mesh.vert_pos.len();
183    mesh.vert_normal.resize(num_vert, Vec3::new(0.0, 0.0, 0.0));
184
185    // For each vertex, find the first halfedge that starts there
186    let mut vert_first_edge = vec![i32::MAX; num_vert];
187    for (i, edge) in mesh.halfedge.iter().enumerate() {
188        let sv = edge.start_vert;
189        if sv >= 0 && (sv as usize) < num_vert {
190            let sv = sv as usize;
191            if (i as i32) < vert_first_edge[sv] {
192                vert_first_edge[sv] = i as i32;
193            }
194        }
195    }
196
197    // Each vertex's normal reads only shared mesh data and its own walk, so
198    // the per-vertex work parallelizes with results identical to sequential
199    // (the accumulation order WITHIN a vertex is the fixed ForVert walk).
200    let halfedge = &mesh.halfedge;
201    let vert_pos = &mesh.vert_pos;
202    let face_normal = &mesh.face_normal;
203    let normals: Vec<Vec3> = crate::par::maybe_par_map(num_vert, 10_000, |vert| {
204        let first_edge = vert_first_edge[vert];
205        if first_edge == i32::MAX {
206            return Vec3::new(0.0, 0.0, 0.0);
207        }
208
209        let mut normal = Vec3::new(0.0, 0.0, 0.0);
210
211        // ForVert equivalent: walk CW around the vertex. C++ ForVert (impl.h)
212        // STEPS FIRST and calls func after, so first_edge is processed LAST.
213        // The visit order fixes the float accumulation order of the
214        // angle-weighted normal sum — it must match C++ bit-for-bit because
215        // vertex normals feed the Boolean3 SOS tie-breaks.
216        let mut current = first_edge as usize;
217        loop {
218            let paired = halfedge[current].paired_halfedge;
219            if paired < 0 {
220                break;
221            }
222            current = next_halfedge(paired) as usize;
223            let h = &halfedge[current];
224            let tri_verts = [
225                h.start_vert as usize,
226                h.end_vert as usize,
227                halfedge[next_halfedge(current as i32) as usize].end_vert as usize,
228            ];
229
230            // Avoid degenerate triangles
231            if tri_verts[0] < vert_pos.len()
232                && tri_verts[1] < vert_pos.len()
233                && tri_verts[2] < vert_pos.len()
234            {
235                let curr_edge_dir = vert_pos[tri_verts[1]] - vert_pos[tri_verts[0]];
236                let prev_edge_dir = vert_pos[tri_verts[0]] - vert_pos[tri_verts[2]];
237                let curr_len = length2(curr_edge_dir).sqrt();
238                let prev_len = length2(prev_edge_dir).sqrt();
239
240                if curr_len > 0.0 && prev_len > 0.0 {
241                    let curr_norm = curr_edge_dir / curr_len;
242                    let prev_norm = prev_edge_dir / prev_len;
243                    if curr_norm.x.is_finite() && prev_norm.x.is_finite() {
244                        let d = dot(prev_norm, curr_norm).clamp(-1.0, 1.0);
245                        // Negate because prevEdge points into vert and currEdge points away
246                        let phi = math::acos(-d);
247                        if phi.is_finite() && current / 3 < face_normal.len() {
248                            normal = normal + face_normal[current / 3] * phi;
249                        }
250                    }
251                }
252            }
253
254            if current == first_edge as usize {
255                break;
256            }
257        }
258
259        let len = length2(normal).sqrt();
260        if len > 0.0 { normal / len } else { Vec3::new(0.0, 0.0, 0.0) }
261    });
262    mesh.vert_normal = normals;
263}
264
265// -----------------------------------------------------------------------
266// GetBarycentric — barycentric coordinates of point in triangle
267// -----------------------------------------------------------------------
268
269/// Compute barycentric coordinates of `v` with respect to triangle `tri_pos`.
270/// Returns [u, v, w] where vertex i has weight uvw[i].
271/// Returns exact 1.0 for vertices within `tolerance` of a triangle vertex,
272/// and exact 0.0 for points within tolerance of an edge.
273///
274/// Mirrors `GetBarycentric` in `src/shared.h`.
275pub fn get_barycentric(v: Vec3, tri_pos: [Vec3; 3], tolerance: f64) -> Vec3 {
276    let edges = [
277        tri_pos[2] - tri_pos[1],
278        tri_pos[0] - tri_pos[2],
279        tri_pos[1] - tri_pos[0],
280    ];
281    let d2 = [
282        dot(edges[0], edges[0]),
283        dot(edges[1], edges[1]),
284        dot(edges[2], edges[2]),
285    ];
286    let long_side = if d2[0] > d2[1] && d2[0] > d2[2] {
287        0
288    } else if d2[1] > d2[2] {
289        1
290    } else {
291        2
292    };
293    let cross_p = cross(edges[0], edges[1]);
294    let area2 = dot(cross_p, cross_p);
295    let tol2 = tolerance * tolerance;
296
297    let mut uvw = Vec3::splat(0.0);
298    for i in 0..3 {
299        let dv = v - tri_pos[i];
300        if dot(dv, dv) < tol2 {
301            uvw = Vec3::splat(0.0);
302            match i {
303                0 => uvw.x = 1.0,
304                1 => uvw.y = 1.0,
305                _ => uvw.z = 1.0,
306            }
307            return uvw;
308        }
309    }
310
311    if d2[long_side] < tol2 {
312        // Degenerate point
313        return Vec3::new(1.0, 0.0, 0.0);
314    } else if area2 > d2[long_side] * tol2 {
315        // Triangle case
316        for i in 0..3 {
317            let j = (i + 1) % 3;
318            let cross_pv = cross(edges[i], v - tri_pos[j]);
319            let area2v = dot(cross_pv, cross_pv);
320            let val = if area2v < d2[i] * tol2 {
321                0.0
322            } else {
323                dot(cross_pv, cross_p)
324            };
325            match i {
326                0 => uvw.x = val,
327                1 => uvw.y = val,
328                _ => uvw.z = val,
329            }
330        }
331        let sum = uvw.x + uvw.y + uvw.z;
332        uvw = uvw / sum;
333        return uvw;
334    } else {
335        // Line case
336        let next_v = (long_side + 1) % 3;
337        let alpha = dot(v - tri_pos[next_v], edges[long_side]) / d2[long_side];
338        uvw = Vec3::splat(0.0);
339        let last_v = (next_v + 1) % 3;
340        match next_v {
341            0 => uvw.x = 1.0 - alpha,
342            1 => uvw.y = 1.0 - alpha,
343            _ => uvw.z = 1.0 - alpha,
344        }
345        match last_v {
346            0 => uvw.x = alpha,
347            1 => uvw.y = alpha,
348            _ => uvw.z = alpha,
349        }
350        return uvw;
351    }
352}
353
354// -----------------------------------------------------------------------
355// AssembleHalfedges — group halfedges into polygon loops
356// -----------------------------------------------------------------------
357
358/// Given a slice of halfedges (from a polygonal face), group them into polygon
359/// loops by following start_vert → end_vert chains. Returns a vec of polygon
360/// loops, where each loop is a vec of halfedge indices (offset by
361/// `start_halfedge_idx`).
362///
363/// Mirrors `AssembleHalfedges` in `src/face_op.cpp`.
364pub fn assemble_halfedges(
365    halfedges: &[Halfedge],
366    start_halfedge_idx: i32,
367) -> Vec<Vec<i32>> {
368    // Build multimap: start_vert → local edge index
369    let mut vert_edge: BTreeMap<i32, Vec<usize>> = BTreeMap::new();
370    for (i, he) in halfedges.iter().enumerate() {
371        vert_edge.entry(he.start_vert).or_default().push(i);
372    }
373
374    let mut polys: Vec<Vec<i32>> = Vec::new();
375    let mut start_edge: usize = 0;
376    let mut this_edge: usize = start_edge;
377
378    loop {
379        if this_edge == start_edge {
380            // Find next unvisited edge
381            if vert_edge.is_empty() {
382                break;
383            }
384            let (&_vert, edges) = vert_edge.iter().next().unwrap();
385            start_edge = edges[0];
386            this_edge = start_edge;
387            polys.push(Vec::new());
388        }
389        polys.last_mut().unwrap().push(start_halfedge_idx + this_edge as i32);
390        let end_vert = halfedges[this_edge].end_vert;
391        let edges = vert_edge.get_mut(&end_vert).expect("non-manifold edge");
392        // Remove the first occurrence
393        let pos = 0; // take first available
394        this_edge = edges.remove(pos);
395        if edges.is_empty() {
396            vert_edge.remove(&end_vert);
397        }
398    }
399    polys
400}
401
402/// Project polygon loops into 2D using projection matrix and vertex positions.
403///
404/// Mirrors `ProjectPolygons` in `src/face_op.cpp`.
405pub fn project_polygons(
406    polys: &[Vec<i32>],
407    halfedge: &[Halfedge],
408    vert_pos: &[Vec3],
409    projection: &Proj2x3,
410) -> PolygonsIdx {
411    let mut polygons: PolygonsIdx = Vec::new();
412    for poly in polys {
413        let mut simple_poly = Vec::new();
414        for &edge in poly {
415            let vert = halfedge[edge as usize].start_vert;
416            simple_poly.push(PolyVert {
417                pos: projection.apply(vert_pos[vert as usize]),
418                idx: edge,
419            });
420        }
421        polygons.push(simple_poly);
422    }
423    polygons
424}
425
426// -----------------------------------------------------------------------
427// Face2Tri — triangulate polygonal faces into triangle halfedges
428// -----------------------------------------------------------------------
429
430/// Port of `WriteLocalTriangles` (face_op.cpp): write 1–2 triangles for a
431/// tri/quad face. `triangles` entries are face-halfedge indices. Interior
432/// edges (the quad diagonal) pair against each other by matching those
433/// indices; boundary edges record their output halfedge in
434/// `contour2tri[originalFaceHalfedgeIndex]` for the cross-face pairing pass.
435fn write_local_triangles(
436    output: &mut [Halfedge],
437    contour2tri: &mut [i32],
438    face_halfedge: &[Halfedge],
439    first_tri: usize,
440    triangles: &[[i32; 3]],
441) {
442    debug_assert!(triangles.len() <= 2, "local face path only handles tris/quads");
443    let first_out = 3 * first_tri as i32;
444    // (start, end, out) — start/end are face-halfedge indices, out is the
445    // output halfedge index.
446    let mut local_edges: [[i32; 3]; 6] = [[0; 3]; 6];
447    let mut num_edge = 0usize;
448    for tri in triangles {
449        for i in 0..3 {
450            let out = first_out + num_edge as i32;
451            let start = tri[i];
452            let end = tri[(i + 1) % 3];
453            local_edges[num_edge] = [start, end, out];
454            output[out as usize].start_vert = face_halfedge[start as usize].start_vert;
455            // C++ Halfedges derives end verts from triangle adjacency; the
456            // Rust Halfedge stores them, so set explicitly.
457            output[out as usize].end_vert = face_halfedge[end as usize].start_vert;
458            output[out as usize].prop_vert = face_halfedge[start as usize].prop_vert;
459            output[out as usize].paired_halfedge = -1;
460            num_edge += 1;
461        }
462    }
463
464    for i in 0..num_edge {
465        let edge = local_edges[i];
466        let mut pair = -1;
467        for j in 0..num_edge {
468            if local_edges[j][0] == edge[1] && local_edges[j][1] == edge[0] {
469                pair = local_edges[j][2];
470                break;
471            }
472        }
473        if pair >= 0 {
474            output[edge[2] as usize].paired_halfedge = pair;
475        } else {
476            contour2tri[edge[0] as usize] = edge[2];
477        }
478    }
479}
480
481/// Port of `WriteGeneralTriangulation` (face_op.cpp): write the triangles of a
482/// `HalfedgeTriangulation`. Triangle-halfedge vertex fields hold face-halfedge
483/// indices; interior pairs translate directly, while contour pairs record the
484/// triangle halfedge lying on each original edge in `contour2tri`.
485fn write_general_triangulation(
486    output: &mut [Halfedge],
487    contour2tri: &mut [i32],
488    face_halfedge: &[Halfedge],
489    first_tri: usize,
490    triangulation: &HalfedgeTriangulation,
491) {
492    let first_out = 3 * first_tri as i32;
493    let contour_end = triangulation.contour_end;
494    let num_tri_halfedge = 3 * triangulation.num_tri();
495
496    for local in 0..num_tri_halfedge {
497        let out = (first_out as usize) + local;
498        let edge = &triangulation.halfedges[contour_end + local];
499        output[out].start_vert = face_halfedge[edge.start_vert as usize].start_vert;
500        output[out].end_vert = face_halfedge[edge.end_vert as usize].start_vert;
501        output[out].prop_vert = face_halfedge[edge.start_vert as usize].prop_vert;
502        if edge.paired_halfedge >= contour_end as i32 {
503            output[out].paired_halfedge = first_out + edge.paired_halfedge - contour_end as i32;
504        } else {
505            output[out].paired_halfedge = -1;
506        }
507    }
508
509    for contour in 0..contour_end {
510        let edge = &triangulation.halfedges[contour];
511        if edge.paired_halfedge < 0 {
512            continue;
513        }
514        debug_assert!(
515            edge.paired_halfedge >= contour_end as i32,
516            "contour paired to another contour"
517        );
518        // Contour halfedges are the input edges reversed, so end_vert holds
519        // the original face-halfedge index of the edge's start.
520        let boundary = edge.end_vert;
521        debug_assert!(
522            boundary >= 0 && (boundary as usize) < contour2tri.len(),
523            "contour edge index out of bounds"
524        );
525        contour2tri[boundary as usize] = first_out + edge.paired_halfedge - contour_end as i32;
526    }
527}
528
529/// Triangulates the faces represented by `face_edge` (polygon boundaries in
530/// `mesh.halfedge`) and `halfedge_ref` (per-halfedge TriRef). On entry
531/// `mesh.halfedge` holds general polygonal faces with valid cross-face
532/// pairing (from boolean result assembly); on return it holds proper
533/// triangles and `mesh.mesh_relation.tri_ref` is populated.
534///
535/// Mirrors `Manifold::Impl::Face2Tri` in `src/face_op.cpp` (v3.5.0): pairing
536/// is face-local during triangulation, then boundary edges are paired across
537/// faces via the *original* `paired_halfedge` relationships — never re-derived
538/// from vertex pairs, which would mispair duplicate edges on degenerate
539/// (exactly-coplanar) faces and produce a non-manifold result.
540pub fn face2tri(
541    mesh: &mut ManifoldImpl,
542    face_edge: &[i32],
543    halfedge_ref: &[TriRef],
544    allow_convex: bool,
545) {
546    // C++ passes faceHalfedge as a separate view and writes halfedge_ fresh;
547    // the Rust assembly built the polygonal faces in mesh.halfedge, so take it.
548    let face_halfedge = std::mem::take(&mut mesh.halfedge);
549    let num_faces = face_edge.len() - 1;
550    let mut contour2tri = vec![-1i32; face_halfedge.len()];
551
552    // First pass: count triangles per face; run the general triangulator for
553    // faces with more than four edges. Each face triangulates independently
554    // (C++ spawns a TBB task per general face), so the parallel path yields
555    // identical per-face results, merged in face order.
556    let face_normal_ref = &mesh.face_normal;
557    let vert_pos_ref = &mesh.vert_pos;
558    let epsilon = mesh.epsilon;
559    let general: Vec<Option<HalfedgeTriangulation>> =
560        crate::par::maybe_par_map(num_faces, 512, |face| {
561            let num_edge = (face_edge[face + 1] - face_edge[face]) as usize;
562            if num_edge <= 4 {
563                return None;
564            }
565            let first_edge = face_edge[face] as usize;
566            let last_edge = face_edge[face + 1] as usize;
567            let projection = get_axis_aligned_projection(face_normal_ref[face]);
568            let polys_loops =
569                assemble_halfedges(&face_halfedge[first_edge..last_edge], first_edge as i32);
570            let polys = project_polygons(&polys_loops, &face_halfedge, vert_pos_ref, &projection);
571            Some(triangulate_idx_halfedges(&polys, epsilon, allow_convex))
572        });
573
574    let mut tri_offset = vec![0usize; face_edge.len()];
575    let mut results: std::collections::HashMap<usize, HalfedgeTriangulation> =
576        std::collections::HashMap::new();
577    for (face, triangulation) in general.into_iter().enumerate() {
578        let num_edge = (face_edge[face + 1] - face_edge[face]) as usize;
579        if num_edge == 0 {
580            continue;
581        }
582        debug_assert!(num_edge >= 3, "face has less than three edges");
583        tri_offset[face] = num_edge - 2;
584        if let Some(t) = triangulation {
585            tri_offset[face] = t.num_tri();
586            results.insert(face, t);
587        }
588    }
589
590    // Exclusive scan of triangle counts → per-face output offsets.
591    let mut acc = 0usize;
592    for entry in tri_offset.iter_mut() {
593        let count = *entry;
594        *entry = acc;
595        acc += count;
596    }
597    let num_tri = acc;
598
599    let mut new_halfedge = vec![
600        Halfedge {
601            start_vert: -1,
602            end_vert: -1,
603            paired_halfedge: -1,
604            prop_vert: -1,
605        };
606        3 * num_tri
607    ];
608    let mut tri_normal = vec![Vec3::new(0.0, 0.0, 0.0); num_tri];
609    let mut tri_ref = vec![TriRef::default(); num_tri];
610
611    for face in 0..num_faces {
612        let first_edge = face_edge[face] as usize;
613        let last_edge = face_edge[face + 1] as usize;
614        let num_edge = last_edge - first_edge;
615        if num_edge == 0 {
616            continue;
617        }
618        let normal = mesh.face_normal[face];
619        let first_tri = tri_offset[face];
620        let face_num_tri;
621
622        if num_edge == 3 {
623            // Single triangle — sort edges into correct winding order.
624            let mut tri_edge = [first_edge as i32, first_edge as i32 + 1, first_edge as i32 + 2];
625            let mut tri = [
626                face_halfedge[first_edge].start_vert,
627                face_halfedge[first_edge + 1].start_vert,
628                face_halfedge[first_edge + 2].start_vert,
629            ];
630            let mut ends = [
631                face_halfedge[first_edge].end_vert,
632                face_halfedge[first_edge + 1].end_vert,
633                face_halfedge[first_edge + 2].end_vert,
634            ];
635            if ends[0] == tri[2] {
636                tri_edge.swap(1, 2);
637                tri.swap(1, 2);
638                ends.swap(1, 2);
639            }
640            debug_assert!(
641                ends[0] == tri[1] && ends[1] == tri[2] && ends[2] == tri[0],
642                "these 3 edges do not form a triangle!"
643            );
644            write_local_triangles(
645                &mut new_halfedge,
646                &mut contour2tri,
647                &face_halfedge,
648                first_tri,
649                &[tri_edge],
650            );
651            face_num_tri = 1;
652        } else if num_edge == 4 {
653            // Quad — split into two triangles along the better diagonal.
654            let projection = get_axis_aligned_projection(normal);
655            let tri_ccw = |t: [i32; 3]| -> bool {
656                ccw(
657                    projection.apply(mesh.vert_pos[face_halfedge[t[0] as usize].start_vert as usize]),
658                    projection.apply(mesh.vert_pos[face_halfedge[t[1] as usize].start_vert as usize]),
659                    projection.apply(mesh.vert_pos[face_halfedge[t[2] as usize].start_vert as usize]),
660                    mesh.epsilon,
661                ) >= 0
662            };
663
664            let quad_loops =
665                assemble_halfedges(&face_halfedge[first_edge..last_edge], first_edge as i32);
666            let quad = &quad_loops[0]; // Should be exactly one loop
667
668            let tris0 = [
669                [quad[0], quad[1], quad[2]],
670                [quad[0], quad[2], quad[3]],
671            ];
672            let tris1 = [
673                [quad[1], quad[2], quad[3]],
674                [quad[0], quad[1], quad[3]],
675            ];
676
677            let choice = if !(tri_ccw(tris0[0]) && tri_ccw(tris0[1])) {
678                1
679            } else if tri_ccw(tris1[0]) && tri_ccw(tris1[1]) {
680                let diag0 = mesh.vert_pos[face_halfedge[quad[0] as usize].start_vert as usize]
681                    - mesh.vert_pos[face_halfedge[quad[2] as usize].start_vert as usize];
682                let diag1 = mesh.vert_pos[face_halfedge[quad[1] as usize].start_vert as usize]
683                    - mesh.vert_pos[face_halfedge[quad[3] as usize].start_vert as usize];
684                if length2(diag0) > length2(diag1) { 1 } else { 0 }
685            } else {
686                0
687            };
688
689            let chosen = if choice == 0 { &tris0 } else { &tris1 };
690            write_local_triangles(
691                &mut new_halfedge,
692                &mut contour2tri,
693                &face_halfedge,
694                first_tri,
695                chosen,
696            );
697            face_num_tri = 2;
698        } else {
699            // General triangulation
700            let triangulation = results
701                .get(&face)
702                .expect("general face missing triangulation result");
703            write_general_triangulation(
704                &mut new_halfedge,
705                &mut contour2tri,
706                &face_halfedge,
707                first_tri,
708                triangulation,
709            );
710            face_num_tri = triangulation.num_tri();
711        }
712
713        // WriteTriRefs
714        let ref_tri = halfedge_ref[first_edge];
715        for t in 0..face_num_tri {
716            tri_normal[first_tri + t] = normal;
717            tri_ref[first_tri + t] = ref_tri;
718        }
719    }
720
721    // Cross-face pairing: connect each face-boundary output halfedge to its
722    // counterpart via the original assembly pairing.
723    for edge in 0..face_halfedge.len() {
724        let tri_edge = contour2tri[edge];
725        if tri_edge < 0 {
726            continue;
727        }
728        let pair = face_halfedge[edge].paired_halfedge;
729        if pair < 0 {
730            continue;
731        }
732        let pair_tri = contour2tri[pair as usize];
733        debug_assert!(pair_tri >= 0, "boundary edge did not triangulate with its pair");
734        new_halfedge[tri_edge as usize].paired_halfedge = pair_tri;
735    }
736
737    mesh.halfedge = new_halfedge;
738    mesh.face_normal = tri_normal;
739    mesh.mesh_relation.tri_ref = tri_ref;
740}
741
742// -----------------------------------------------------------------------
743// ReorderHalfedges — canonical ordering within each triangle
744// -----------------------------------------------------------------------
745
746/// Reorders halfedges within each face so the one with the smallest start_vert
747/// is first, then fixes paired_halfedge references.
748///
749/// Mirrors `Manifold::Impl::ReorderHalfedges` in `src/sort.cpp`.
750pub fn reorder_halfedges(mesh: &mut ManifoldImpl) {
751    let num_tri = mesh.halfedge.len() / 3;
752
753    // Step 1: rotate each triangle so smallest start_vert is first
754    for tri in 0..num_tri {
755        let base = tri * 3;
756        let face = [
757            mesh.halfedge[base],
758            mesh.halfedge[base + 1],
759            mesh.halfedge[base + 2],
760        ];
761        if face[0].start_vert < 0 {
762            continue;
763        }
764        let mut index = 0;
765        for i in 1..3 {
766            if face[i].start_vert < face[index].start_vert {
767                index = i;
768            }
769        }
770        for i in 0..3 {
771            mesh.halfedge[base + i] = face[(index + i) % 3];
772        }
773    }
774
775    // Step 2: fix paired_halfedge references
776    for tri in 0..num_tri {
777        for i in 0..3 {
778            let base = tri * 3 + i;
779            let curr = mesh.halfedge[base];
780            if curr.start_vert < 0 {
781                break; // skip collapsed triangle
782            }
783            if curr.paired_halfedge < 0 {
784                continue; // unpaired halfedge
785            }
786            let opp_face = curr.paired_halfedge as usize / 3;
787            let mut index = -1i32;
788            for j in 0..3 {
789                if curr.start_vert == mesh.halfedge[opp_face * 3 + j].end_vert {
790                    index = j as i32;
791                }
792            }
793            mesh.halfedge[base].paired_halfedge = opp_face as i32 * 3 + index;
794        }
795    }
796}
797
798// -----------------------------------------------------------------------
799// Tests
800// -----------------------------------------------------------------------
801
802#[cfg(test)]
803mod tests {
804    use super::*;
805    use crate::linalg::Mat3x4;
806    use crate::impl_mesh::ManifoldImpl;
807
808    #[test]
809    fn test_get_axis_aligned_projection_z() {
810        // Normal primarily in Z: should project to XY plane
811        let proj = get_axis_aligned_projection(Vec3::new(0.0, 0.0, 1.0));
812        let v = Vec3::new(3.0, 4.0, 5.0);
813        let p = proj.apply(v);
814        assert!((p.x - 3.0).abs() < 1e-12);
815        assert!((p.y - 4.0).abs() < 1e-12);
816    }
817
818    #[test]
819    fn test_get_axis_aligned_projection_y() {
820        // Normal primarily in Y: should project to ZX plane
821        let proj = get_axis_aligned_projection(Vec3::new(0.0, 1.0, 0.0));
822        let v = Vec3::new(3.0, 4.0, 5.0);
823        let p = proj.apply(v);
824        assert!((p.x - 5.0).abs() < 1e-12, "expected z=5, got {}", p.x);
825        assert!((p.y - 3.0).abs() < 1e-12, "expected x=3, got {}", p.y);
826    }
827
828    #[test]
829    fn test_get_axis_aligned_projection_x() {
830        // Normal primarily in X: should project to YZ plane
831        let proj = get_axis_aligned_projection(Vec3::new(1.0, 0.0, 0.0));
832        let v = Vec3::new(3.0, 4.0, 5.0);
833        let p = proj.apply(v);
834        assert!((p.x - 4.0).abs() < 1e-12, "expected y=4, got {}", p.x);
835        assert!((p.y - 5.0).abs() < 1e-12, "expected z=5, got {}", p.y);
836    }
837
838    #[test]
839    fn test_get_axis_aligned_projection_negative_z() {
840        // Normal primarily in -Z: row0 should be flipped
841        let proj = get_axis_aligned_projection(Vec3::new(0.0, 0.0, -1.0));
842        let v = Vec3::new(3.0, 4.0, 5.0);
843        let p = proj.apply(v);
844        // Flipped first row: x → -x
845        assert!((p.x + 3.0).abs() < 1e-12, "expected -x=-3, got {}", p.x);
846        assert!((p.y - 4.0).abs() < 1e-12);
847    }
848
849    #[test]
850    fn test_set_normals_tetrahedron() {
851        let mut m = ManifoldImpl::tetrahedron(&Mat3x4::identity());
852        set_normals_and_coplanar(&mut m);
853        // Every face normal should be unit length
854        for n in &m.face_normal {
855            let len = length2(*n).sqrt();
856            assert!((len - 1.0).abs() < 1e-10, "face normal not unit: len={}", len);
857        }
858        // Every vert normal should be nonzero (tetrahedron has no degenerate verts)
859        for n in &m.vert_normal {
860            let len = length2(*n).sqrt();
861            assert!(len > 0.0, "vert normal is zero");
862        }
863    }
864
865    #[test]
866    fn test_set_normals_cube() {
867        let mut m = ManifoldImpl::cube(&Mat3x4::identity());
868        set_normals_and_coplanar(&mut m);
869        assert_eq!(m.face_normal.len(), m.num_tri());
870        // Coplanar IDs should be assigned
871        let any_coplanar_id_set = m.mesh_relation.tri_ref.iter()
872            .any(|r| r.coplanar_id >= 0);
873        assert!(any_coplanar_id_set, "no coplanar IDs were set");
874    }
875}
876
877// -----------------------------------------------------------------------
878// Slice & Project — cross-section operations on ManifoldImpl
879// -----------------------------------------------------------------------
880
881use crate::collider::Collider;
882use crate::sort::get_face_box_morton;
883use crate::types::{Box as BBox, Polygons, SimplePolygon};
884
885impl ManifoldImpl {
886    /// Slice the mesh at the given Z height, returning 2D polygon loops.
887    /// Mirrors `Manifold::Impl::Slice` in `src/face_op.cpp`.
888    pub fn slice(&self, height: f64) -> Polygons {
889        let num_tri = self.num_tri();
890        if num_tri == 0 {
891            return vec![];
892        }
893
894        // Build plane query box spanning the full XY extent at the given Z
895        let mut plane = self.bbox;
896        plane.min.z = height;
897        plane.max.z = height;
898
899        // Build BVH for face bounding boxes
900        let (face_box, face_morton) = get_face_box_morton(self);
901        let collider = Collider::new(face_box, face_morton);
902
903        // Find all triangles that straddle the slice plane
904        let mut tris: std::collections::HashSet<usize> = std::collections::HashSet::new();
905        let query = vec![BBox::from_points(plane.min, plane.max)];
906        collider.collisions_with_boxes(&query, false, |_query_idx, tri| {
907            let mut min_z = f64::INFINITY;
908            let mut max_z = f64::NEG_INFINITY;
909            for j in 0..3 {
910                let z = self.vert_pos[self.halfedge[3 * tri + j].start_vert as usize].z;
911                min_z = min_z.min(z);
912                max_z = max_z.max(z);
913            }
914            if min_z <= height && max_z > height {
915                tris.insert(tri);
916            }
917        });
918
919        // Trace polygon loops through intersected triangles
920        let mut polys: Polygons = Vec::new();
921        while !tris.is_empty() {
922            let start_tri = *tris.iter().next().unwrap();
923            let mut poly: SimplePolygon = Vec::new();
924
925            // Find the edge where the slice enters (above→below transition)
926            let mut k = 0usize;
927            for j in 0..3usize {
928                let next_j = (j + 1) % 3;
929                if self.vert_pos[self.halfedge[3 * start_tri + j].start_vert as usize].z > height
930                    && self.vert_pos[self.halfedge[3 * start_tri + next_j].start_vert as usize].z <= height
931                {
932                    k = next_j;
933                    break;
934                }
935            }
936
937            let mut tri = start_tri;
938            loop {
939                tris.remove(&tri);
940                if self.vert_pos[self.halfedge[3 * tri + k].end_vert as usize].z <= height {
941                    k = (k + 1) % 3;
942                }
943
944                let up = &self.halfedge[3 * tri + k];
945                let below = self.vert_pos[up.start_vert as usize];
946                let above = self.vert_pos[up.end_vert as usize];
947                let a = (height - below.z) / (above.z - below.z);
948                // lerp: below + a * (above - below)
949                let pt = Vec2::new(
950                    below.x + a * (above.x - below.x),
951                    below.y + a * (above.y - below.y),
952                );
953                poly.push(pt);
954
955                let pair = up.paired_halfedge;
956                tri = pair as usize / 3;
957                k = ((pair as usize) % 3 + 1) % 3;
958
959                if tri == start_tri {
960                    break;
961                }
962            }
963
964            polys.push(poly);
965        }
966
967        polys
968    }
969
970    /// Project the mesh silhouette onto the XY plane, returning 2D polygon loops.
971    /// Mirrors `Manifold::Impl::Project` in `src/face_op.cpp`.
972    pub fn project(&self) -> Polygons {
973        if self.num_tri() == 0 || self.face_normal.is_empty() {
974            return vec![];
975        }
976
977        let projection = get_axis_aligned_projection(Vec3::new(0.0, 0.0, 1.0));
978
979        // Find cusp edges: silhouette edges where one adjacent face points up
980        // and the other points down (z-component of normals)
981        let mut cusps: Vec<crate::types::Halfedge> = Vec::new();
982        for edge in &self.halfedge {
983            let paired_face = self.halfedge[edge.paired_halfedge as usize].paired_halfedge as usize / 3;
984            let this_face = edge.paired_halfedge as usize / 3;
985            if self.face_normal[paired_face].z >= 0.0 && self.face_normal[this_face].z < 0.0 {
986                cusps.push(*edge);
987            }
988        }
989
990        if cusps.is_empty() {
991            return vec![];
992        }
993
994        let loops = assemble_halfedges(&cusps, 0);
995        let polys_indexed = project_polygons(&loops, &cusps, &self.vert_pos, &projection);
996
997        // Convert PolygonsIdx to Polygons
998        let mut polys: Polygons = Vec::new();
999        for poly in &polys_indexed {
1000            let simple: SimplePolygon = poly.iter().map(|pv| pv.pos).collect();
1001            polys.push(simple);
1002        }
1003
1004        polys
1005    }
1006}