Skip to main content

manifold_rust/
polygon.rs

1// Phase 3: Polygon Triangulation — ported from src/polygon.cpp, src/tree2d.h/cpp, src/utils.h
2//
3// Key algorithm: ear-clipping with 2D KD-tree acceleration, convex fast-path,
4// hole key-holing. Must produce identical triangulations to the C++ version.
5
6use std::collections::HashMap;
7use crate::linalg::{Vec2, IVec3};
8use crate::types::{Halfedge, PolyVert, PolygonsIdx, SimplePolygonIdx, Polygons, Rect, K_PRECISION};
9
10#[path = "polygon_earclip.rs"]
11mod polygon_earclip;
12use polygon_earclip::EarClip;
13
14type IVec3Out = IVec3;
15
16const INVALID: usize = usize::MAX;
17const K_BEST: f64 = f64::NEG_INFINITY;
18
19// ---------------------------------------------------------------------------
20// CCW (from utils.h)
21// ---------------------------------------------------------------------------
22
23/// Determines if p0, p1, p2 are wound CCW, CW, or colinear within tolerance.
24/// Returns 1 (CCW), -1 (CW), or 0 (colinear within tol).
25pub fn ccw(p0: Vec2, p1: Vec2, p2: Vec2, tol: f64) -> i32 {
26    let v1 = p1 - p0;
27    let v2 = p2 - p0;
28    let area = v1.x * v2.y - v1.y * v2.x;
29    let base2 = (v1.x * v1.x + v1.y * v1.y).max(v2.x * v2.x + v2.y * v2.y);
30    if area * area * 4.0 <= base2 * tol * tol {
31        0
32    } else if area > 0.0 {
33        1
34    } else {
35        -1
36    }
37}
38
39#[inline]
40fn determinant2x2(a: Vec2, b: Vec2) -> f64 {
41    a.x * b.y - a.y * b.x
42}
43
44#[inline]
45fn safe_normalize_2d(v: Vec2) -> Vec2 {
46    let len = (v.x * v.x + v.y * v.y).sqrt();
47    if len == 0.0 || !len.is_finite() {
48        Vec2::new(0.0, 0.0)
49    } else {
50        Vec2::new(v.x / len, v.y / len)
51    }
52}
53
54#[inline]
55fn dot2d(a: Vec2, b: Vec2) -> f64 {
56    a.x * b.x + a.y * b.y
57}
58
59// ---------------------------------------------------------------------------
60// 2D KD-tree (from tree2d.h/cpp)
61// ---------------------------------------------------------------------------
62
63/// Recursive in-place kd-tree construction on a PolyVert slice.
64/// Alternates between sorting by x and y.
65fn build_two_d_tree_impl(points: &mut [PolyVert], sort_x: bool) {
66    if sort_x {
67        points.sort_by(|a, b| a.pos.x.partial_cmp(&b.pos.x).unwrap_or(std::cmp::Ordering::Equal));
68    } else {
69        points.sort_by(|a, b| a.pos.y.partial_cmp(&b.pos.y).unwrap_or(std::cmp::Ordering::Equal));
70    }
71    if points.len() < 2 {
72        return;
73    }
74    let mid = points.len() / 2;
75    build_two_d_tree_impl(&mut points[..mid], !sort_x);
76    build_two_d_tree_impl(&mut points[mid + 1..], !sort_x);
77}
78
79fn build_two_d_tree(points: &mut Vec<PolyVert>) {
80    if points.len() <= 8 {
81        return;
82    }
83    build_two_d_tree_impl(points.as_mut_slice(), true);
84}
85
86/// Query the 2D kd-tree, calling `f` for every point inside rect `r`.
87fn query_two_d_tree<F: FnMut(PolyVert)>(points: &[PolyVert], r: Rect, mut f: F) {
88    if points.len() <= 8 {
89        for p in points {
90            if r.contains_point(p.pos) {
91                f(*p);
92            }
93        }
94        return;
95    }
96
97    // Stack-based traversal
98    let mut stack: Vec<(Rect, usize, usize, i32)> = Vec::with_capacity(64); // (current_rect, start, len, level)
99
100    // Initial rect: infinite
101    let inf_rect = Rect {
102        min: Vec2::new(f64::NEG_INFINITY, f64::NEG_INFINITY),
103        max: Vec2::new(f64::INFINITY, f64::INFINITY),
104    };
105    stack.push((inf_rect, 0, points.len(), 0));
106
107    while let Some((current, start, len, level)) = stack.pop() {
108        if len <= 8 {
109            for p in &points[start..start + len] {
110                if r.contains_point(p.pos) {
111                    f(*p);
112                }
113            }
114            continue;
115        }
116
117        let mid = len / 2;
118        let middle = points[start + mid];
119
120        let mut left = current;
121        let mut right = current;
122        if level % 2 == 0 {
123            left.max.x = middle.pos.x;
124            right.min.x = middle.pos.x;
125        } else {
126            left.max.y = middle.pos.y;
127            right.min.y = middle.pos.y;
128        }
129
130        if r.contains_point(middle.pos) {
131            f(middle);
132        }
133
134        if left.does_overlap(&r) {
135            stack.push((left, start, mid, level + 1));
136        }
137        if right.does_overlap(&r) {
138            stack.push((right, start + mid + 1, len - mid - 1, level + 1));
139        }
140    }
141}
142
143// ---------------------------------------------------------------------------
144// IsConvex fast-path check
145// ---------------------------------------------------------------------------
146
147fn is_convex(polys: &PolygonsIdx, epsilon: f64) -> bool {
148    for poly in polys {
149        if poly.is_empty() {
150            continue;
151        }
152        let first_edge = poly[0].pos - poly[poly.len() - 1].pos;
153        let mut last_edge = safe_normalize_2d(first_edge);
154        for v in 0..poly.len() {
155            let edge = if v + 1 < poly.len() {
156                poly[v + 1].pos - poly[v].pos
157            } else {
158                first_edge
159            };
160            let det = determinant2x2(last_edge, edge);
161            if det <= 0.0 || (det.abs() < epsilon && dot2d(last_edge, edge) < 0.0) {
162                return false;
163            }
164            last_edge = safe_normalize_2d(edge);
165        }
166    }
167    true
168}
169
170// ---------------------------------------------------------------------------
171// TriangulateConvex — fast alternating triangulation for convex polygons
172// ---------------------------------------------------------------------------
173
174fn triangulate_convex(polys: &PolygonsIdx) -> Vec<IVec3Out> {
175    let num_tri: usize = polys.iter().map(|p| p.len().saturating_sub(2)).sum();
176    let mut triangles = Vec::with_capacity(num_tri);
177    for poly in polys {
178        let mut i = 0usize;
179        let mut k = poly.len().saturating_sub(1);
180        let mut right = true;
181        while i + 1 < k {
182            let j = if right { i + 1 } else { k - 1 };
183            triangles.push(IVec3Out::new(poly[i].idx, poly[j].idx, poly[k].idx));
184            if right {
185                i = j;
186            } else {
187                k = j;
188            }
189            right = !right;
190        }
191    }
192    triangles
193}
194
195// ---------------------------------------------------------------------------
196// HalfedgeTriangulation — triangulation result as paired halfedges
197// ---------------------------------------------------------------------------
198
199/// Triangulation result represented directly as halfedges, ported from
200/// `HalfedgeTriangulation` in C++ `polygon_internal.h`.
201///
202/// `halfedges[0..contour_end]` are the *exterior* contour halfedges (the
203/// input edges reversed, so their `end_vert` holds the original `idx` of the
204/// edge's start point). `halfedges[contour_end..]` hold three halfedges per
205/// output triangle. All vertex fields are original `idx` values.
206///
207/// Pairing happens incrementally with a LIFO multimap keyed on `(start, end)`,
208/// so duplicate vertex pairs (from degenerate/overlapping polygons) still pair
209/// one-to-one within the triangulation. This is what `Face2Tri` relies on to
210/// keep boolean results manifold on exactly-coplanar faces — re-deriving pairs
211/// from vertex positions after the fact cannot distinguish duplicates.
212pub struct HalfedgeTriangulation {
213    pub halfedges: Vec<Halfedge>,
214    pub contour_end: usize,
215}
216
217impl HalfedgeTriangulation {
218    pub fn num_tri(&self) -> usize {
219        (self.halfedges.len() - self.contour_end) / 3
220    }
221}
222
223/// Mirrors `HalfedgeTriangulation::AddHalfedge`: pair against the most
224/// recently added unpaired opposite edge, else record as unpaired.
225fn add_halfedge(
226    halfedges: &mut Vec<Halfedge>,
227    edge2halfedge: &mut HashMap<(i32, i32), Vec<i32>>,
228    start: i32,
229    end: i32,
230) {
231    let idx = halfedges.len() as i32;
232    let mut data = Halfedge {
233        start_vert: start,
234        end_vert: end,
235        paired_halfedge: -1,
236        prop_vert: -1,
237    };
238    if let Some(reverse) = edge2halfedge.get_mut(&(end, start)) {
239        if let Some(pair) = reverse.pop() {
240            data.paired_halfedge = pair;
241            halfedges[pair as usize].paired_halfedge = idx;
242            if reverse.is_empty() {
243                edge2halfedge.remove(&(end, start));
244            }
245            halfedges.push(data);
246            return;
247        }
248    }
249    edge2halfedge.entry((start, end)).or_default().push(idx);
250    halfedges.push(data);
251}
252
253/// Triangulates indexed polygons, returning both the triangle halfedges and
254/// the contour halfedges with their pairing. Port of C++
255/// `TriangulateIdxHalfedges` (polygon.cpp): the C++ triangulators build the
256/// `HalfedgeTriangulation` incrementally via `AddTriangle`; here we replay the
257/// triangle list (which `triangulate_idx` emits in exactly that call order)
258/// through the same `AddHalfedge` sequence, which yields identical pairing.
259pub fn triangulate_idx_halfedges(
260    polys: &PolygonsIdx,
261    epsilon: f64,
262    allow_convex: bool,
263) -> HalfedgeTriangulation {
264    let triangles = triangulate_idx(polys, epsilon, allow_convex);
265
266    let mut result = HalfedgeTriangulation {
267        halfedges: Vec::new(),
268        contour_end: 0,
269    };
270    let mut edge2halfedge: HashMap<(i32, i32), Vec<i32>> = HashMap::new();
271
272    // AddContours: store the exterior contour halfedge, opposite the filled
273    // contour, so each triangle edge on the boundary pairs against it.
274    for poly in polys {
275        for i in 0..poly.len() {
276            let start = poly[i].idx;
277            let end = poly[if i + 1 < poly.len() { i + 1 } else { 0 }].idx;
278            add_halfedge(&mut result.halfedges, &mut edge2halfedge, end, start);
279        }
280    }
281    result.contour_end = result.halfedges.len();
282
283    for tri in &triangles {
284        add_halfedge(&mut result.halfedges, &mut edge2halfedge, tri.x, tri.y);
285        add_halfedge(&mut result.halfedges, &mut edge2halfedge, tri.y, tri.z);
286        add_halfedge(&mut result.halfedges, &mut edge2halfedge, tri.z, tri.x);
287    }
288
289    // Mirror of C++ Finalize() MANIFOLD_DEBUG checks.
290    #[cfg(debug_assertions)]
291    {
292        debug_assert!(
293            edge2halfedge.is_empty(),
294            "triangulation has unpaired halfedges"
295        );
296        for (i, he) in result.halfedges.iter().enumerate() {
297            let pair = he.paired_halfedge;
298            debug_assert!(
299                pair >= 0 && (pair as usize) < result.halfedges.len(),
300                "invalid paired halfedge"
301            );
302            let other = &result.halfedges[pair as usize];
303            debug_assert!(
304                other.paired_halfedge == i as i32,
305                "halfedge pair is not reciprocal"
306            );
307            debug_assert!(
308                he.start_vert == other.end_vert && he.end_vert == other.start_vert,
309                "halfedge pair endpoints do not match"
310            );
311        }
312    }
313
314    result
315}
316
317// ---------------------------------------------------------------------------
318// Public API
319// ---------------------------------------------------------------------------
320
321/// Triangulates indexed polygons. Returns triangle indices referencing original vertex indices.
322pub fn triangulate_idx(polys: &PolygonsIdx, epsilon: f64, allow_convex: bool) -> Vec<IVec3Out> {
323    if polys.is_empty() || polys.iter().all(|p| p.is_empty()) {
324        return Vec::new();
325    }
326    if allow_convex && is_convex(polys, epsilon) {
327        return triangulate_convex(polys);
328    }
329    let (triangles, _updated_eps) = EarClip::new(polys, epsilon).triangulate();
330    triangles
331}
332
333/// Triangulates unindexed polygons. Vertices are indexed sequentially across all contours.
334pub fn triangulate(polygons: &Polygons, epsilon: f64, allow_convex: bool) -> Vec<IVec3Out> {
335    let mut idx = 0i32;
336    let mut polygons_indexed: PolygonsIdx = Vec::new();
337    for poly in polygons {
338        let simple: SimplePolygonIdx = poly
339            .iter()
340            .map(|&pos| {
341                let v = PolyVert { pos, idx };
342                idx += 1;
343                v
344            })
345            .collect();
346        polygons_indexed.push(simple);
347    }
348    triangulate_idx(&polygons_indexed, epsilon, allow_convex)
349}
350
351#[cfg(test)]
352#[path = "polygon_tests.rs"]
353mod tests;