Skip to main content

ogeom_mesh/
boolean.rs

1//! Booleans between closed triangle meshes, decided exactly.
2//!
3//! [`boolean`] takes two closed, outward-wound, 2-manifold meshes that do
4//! not cross themselves and answers their union, intersection or
5//! difference as one more such mesh. [`boolean_with_shape`] meshes a solid
6//! first and does the same.
7//!
8//! # Exact decisions, rounded new vertices
9//!
10//! Every decision is the exact sign of a polynomial in the input
11//! coordinates: which side of a plane a point lies on, whether two
12//! triangles meet, the order of points along a line, whether a point lies
13//! inside a triangle, the winding number of a mesh about a point. A point
14//! the operands make, where an edge of one crosses a triangle of the other
15//! or two coplanar edges cross, is never rounded while decisions are made
16//! on it: it is kept as the input coordinates that define it, and every
17//! test on it is evaluated in interval arithmetic first and exactly when
18//! the interval cannot tell. So the topology of the answer is the topology
19//! of the exact boolean of the two polyhedra.
20//!
21//! Only at the end are the new vertices rounded to `f64`, each to the
22//! coordinates nearest its exact position within a few units in the last
23//! place. [`MeshBoolean::rounding`] bounds how far any moved. The input
24//! vertices kept are not moved at all. Rounding leaves the connectivity as
25//! decided; where two operands' faces cross at a vanishing angle it can
26//! tilt a sliver piece, a few units in the last place across, past its
27//! neighbour.
28//!
29//! # How
30//!
31//! 1. Every pair of triangles whose boxes touch is met exactly: the points
32//!    and segments they share.
33//! 2. Points found more than once under different names, as where an edge
34//!    passes exactly through a vertex, are merged by exact comparison.
35//! 3. Each triangle that met the other operand is triangulated afresh with
36//!    those points and segments as constraints, the points on its edges
37//!    shared with its neighbours, so each cut operand is again closed.
38//! 4. The pieces are grouped into regions that no segment separates, and
39//!    each region is classified once: on the other operand, where a piece
40//!    lies in a coplanar triangle of it, and otherwise inside or outside it
41//!    by an exact winding number.
42//! 5. The operation keeps the regions it selects, and the pieces are welded
43//!    at the shared points.
44//!
45//! # Coplanar faces
46//!
47//! Where the operands share a plane, the shared area is cut out of both
48//! along the other's edges, and each piece there is *on* the other
49//! operand, its normal the same as the other's or opposite. One copy of a
50//! shared face with the same normal is kept by a union or an intersection,
51//! and none by a difference; a shared face with opposite normals, where
52//! the solids touch from outside, is dropped by a union and an
53//! intersection and kept, from the first operand, by a difference. These
54//! are the boundaries of the regularised booleans.
55//!
56//! # Refused
57//!
58//! An operand that is open, not 2-manifold, has a degenerate triangle,
59//! crosses itself, or is wound inside out, by name. Operands that do not
60//! meet, neither touching nor one holding the other. A result that would
61//! not be a 2-manifold, where the operands touch along an edge or at a
62//! point without overlapping there.
63//!
64//! # References
65//!
66//! The implicit points and the filtered predicates over them follow
67//! M. Attene, *Indirect Predicates for Geometric Constructions*, and the
68//! pipeline of pairs, per-triangle cuts and region labels follows
69//! G. Cherchi, M. Livesu, R. Scateni and M. Attene, *Fast and Robust Mesh
70//! Arrangements using Floating-point Arithmetic*, and G. Cherchi,
71//! F. Pellacini, M. Attene and M. Livesu, *Interactive and Robust Mesh
72//! Booleans*. Rays through edges are resolved as in H. Edelsbrunner and
73//! E. P. Mücke, *Simulation of Simplicity*; points are found by the
74//! stochastic walk of O. Devillers, S. Pion and M. Teillaud, *Walking in a
75//! Triangulation*.
76
77mod classify;
78mod exact;
79mod pairs;
80mod points;
81mod split;
82
83use ogeom_core::{
84    Exact, FastMap, FastSet, OgeomResult, Predicates, Sign, Tolerances, ogeom_bail, parallel,
85};
86use ogeom_math::{Aabb, Point};
87use ogeom_topo::{Model, Shape, Triangulation};
88
89use self::pairs::{Key, Side, Touch, Tri};
90use self::points::{Def, Mid, P3, Pt, by_weight, normal_approx, normal_sign, orient2d};
91use crate::{Bvh, Deflection};
92
93/// A boolean operation.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
95pub enum BoolKind {
96    /// Everything in either operand.
97    Union,
98    /// Everything in both operands.
99    Intersection,
100    /// Everything in the first operand and not in the second.
101    Difference,
102}
103
104/// The answer of a mesh boolean.
105#[derive(Debug, Clone, PartialEq)]
106pub struct MeshBoolean {
107    /// The result: closed, 2-manifold and wound outward, or empty where
108    /// nothing is left. Its vertices are the input vertices kept, first
109    /// operand first, each at its input position exactly and in input
110    /// order, then the new ones from [`MeshBoolean::first_new_vertex`]
111    /// on. It carries no normals and no parameters.
112    pub mesh: Triangulation,
113    /// For each triangle of the result, whether it is new: a piece of an
114    /// input triangle that the other operand cut. A triangle not flagged is
115    /// an input triangle, kept whole, possibly wound the other way.
116    pub cut: Vec<bool>,
117    /// The index of the first vertex made where the operands meet.
118    pub first_new_vertex: usize,
119    /// The farthest any new vertex lies from the exact point it rounds:
120    /// a bound, of the order of the last place of its coordinates.
121    pub rounding: f64,
122}
123
124/// The boolean `op` of two closed meshes.
125///
126/// Each operand must be closed and 2-manifold: every edge has exactly two
127/// triangles running it opposite ways, and the triangles about each vertex
128/// form one fan. It must have no triangle whose corners are collinear, must
129/// not cross or touch itself, and must be wound so that its triangles face
130/// out of the solid it bounds, which may have cavities.
131///
132/// Every decision is exact; only the new vertices are rounded, see the
133/// [module documentation](self).
134///
135/// # Errors
136///
137/// [`OgeomError::NullObject`](ogeom_core::OgeomError::NullObject) if an
138/// operand has no triangles.
139/// [`OgeomError::Range`](ogeom_core::OgeomError::Range) if a triangle names
140/// a vertex its mesh does not have, and
141/// [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric) if a corner is
142/// not finite.
143/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if an
144/// operand is open, is not 2-manifold, has a degenerate triangle, crosses
145/// itself or is wound inside out, if the operands do not meet, or if the
146/// result would not be 2-manifold.
147pub fn boolean(a: &Triangulation, b: &Triangulation, op: BoolKind) -> OgeomResult<MeshBoolean> {
148    let [first, second]: [OgeomResult<Operand<'_>>; 2] = parallel::map_ordered(
149        &[(a, 0, "first"), (b, 1, "second")],
150        |_, &(m, side, name)| Operand::new(m, side, name),
151    )
152    .try_into()
153    .map_err(|_| ogeom_core::ogeom_err!(Invariant, "two operands made other than two"))?;
154    let ops = [first?, second?];
155    for o in &ops {
156        o.check_embedded(0)?;
157    }
158    for o in &ops {
159        o.check_wound(o.name, None, 0)?;
160    }
161    let touches = meet_all(&ops[0], &ops[1]);
162    let mut table = Table::new(&ops);
163    let mut marks: [Vec<Mark>; 2] = [
164        vec![Mark::default(); ops[0].tris.len()],
165        vec![Mark::default(); ops[1].tris.len()],
166    ];
167    for (ta, tb, touch) in &touches {
168        table.take(&mut marks, *ta, *tb, touch);
169    }
170    table.merge(&ops);
171    let flat: [Vec<usize>; 2] = [ops[0].drops()?, ops[1].drops()?];
172    for side in 0..2 {
173        table.settle(&ops[side], &flat[side], &mut marks[side]);
174    }
175    let mut fixed: FastSet<(u32, u32)> = FastSet::default();
176    let cuts = [
177        table.cut(&ops[0], &flat[0], &marks[0], &mut fixed)?,
178        table.cut(&ops[1], &flat[1], &marks[1], &mut fixed)?,
179    ];
180    let mut labels: [Vec<(Vec<usize>, Label)>; 2] = [Vec::new(), Vec::new()];
181    for side in 0..2 {
182        let regions = regions(&ops[side], &cuts[side], &fixed)?;
183        labels[side] = table.label(
184            &ops,
185            side,
186            &flat[side],
187            &marks[side],
188            &cuts[side].pieces,
189            regions,
190        )?;
191    }
192    let meets = !touches.is_empty()
193        || labels
194            .iter()
195            .flatten()
196            .any(|(_, label)| *label != Label::Out);
197    if !meets {
198        ogeom_bail!(
199            Construction,
200            "the operands do not meet: neither touches or holds the other"
201        );
202    }
203    assemble(
204        &table,
205        &cuts,
206        &labels,
207        op,
208        a.deflection_met && b.deflection_met,
209    )
210}
211
212/// The boolean `op` of a closed mesh and a solid, the solid meshed first
213/// to `chord`: [`triangulate`](crate::triangulate()) with
214/// [`Deflection::with_chord`], then [`boolean`] with the mesh as the
215/// first operand.
216///
217/// # Errors
218///
219/// As [`Deflection::with_chord`], [`triangulate`](crate::triangulate()) and
220/// [`boolean`]; the solid's mesh is the second operand in [`boolean`]'s
221/// messages.
222pub fn boolean_with_shape(
223    mesh: &Triangulation,
224    model: &Model,
225    shape: &Shape,
226    op: BoolKind,
227    chord: f64,
228    tol: Tolerances,
229) -> OgeomResult<MeshBoolean> {
230    let solid = crate::triangulate(model, shape, Deflection::with_chord(chord)?, tol)?;
231    boolean(mesh, &solid, op)
232}
233
234/// Check that `mesh` bounds a solid as [`boolean`] asks of an operand:
235/// closed, 2-manifold, no degenerate triangle, wound outward, and meeting
236/// itself only where triangles share corners, that last tested only on the
237/// pairs holding a triangle numbered `first` or after. Returns the mesh's
238/// hierarchy.
239///
240/// # Errors
241///
242/// As [`boolean`] for an operand, named `name` in the messages.
243pub(crate) fn check_solid(
244    mesh: &Triangulation,
245    name: &'static str,
246    first: usize,
247) -> OgeomResult<Bvh> {
248    let operand = Operand::new(mesh, 0, name)?;
249    operand.check_embedded(first)?;
250    operand.check_wound(name, None, 0)?;
251    Ok(operand.bvh)
252}
253
254/// A mesh checked by [`check_solid`], kept to check cavities against.
255pub(crate) struct Solid<'a>(Operand<'a>);
256
257/// [`check_solid`] with `first` zero, the checked mesh kept.
258///
259/// # Errors
260///
261/// As [`check_solid`].
262pub(crate) fn check_solid_kept<'a>(
263    mesh: &'a Triangulation,
264    name: &'static str,
265) -> OgeomResult<Solid<'a>> {
266    let operand = Operand::new(mesh, 0, name)?;
267    operand.check_embedded(0)?;
268    operand.check_wound(name, None, 0)?;
269    Ok(Solid(operand))
270}
271
272impl Solid<'_> {
273    /// The mesh's hierarchy.
274    pub(crate) const fn bvh(&self) -> &Bvh {
275        &self.0.bvh
276    }
277
278    /// [`check_solid`] of the mesh followed by `cavity`, its vertices
279    /// numbered after the mesh's and `first` the mesh's triangle count,
280    /// without checking the mesh again.
281    ///
282    /// The two share no vertex, so the joined mesh is closed, 2-manifold
283    /// and free of degenerate triangles where each is; a cavity triangle
284    /// meets a triangle of the mesh only by meeting it at all; and the
285    /// winding number at a point is the sum of the two meshes'. Messages
286    /// name the cavity's own vertices and triangles where [`Operand::new`]
287    /// refuses it, and the joined mesh's otherwise.
288    ///
289    /// # Errors
290    ///
291    /// As [`check_solid`] for the joined mesh, named `name`.
292    pub(crate) fn check_cavity(
293        &self,
294        cavity: &Triangulation,
295        name: &'static str,
296    ) -> OgeomResult<()> {
297        let part = &self.0;
298        let inner = Operand::new(cavity, 0, name)?;
299        let base = id(part.mesh.positions.len());
300        let first = part.tris.len();
301        let shifted = |t: &Tri, side: Side| Tri {
302            side,
303            v: t.v.map(|v| v + base),
304            ..*t
305        };
306        let chunks: Vec<std::ops::Range<usize>> = (0..inner.tris.len())
307            .step_by(CHUNK)
308            .map(|s| s..(s + CHUNK).min(inner.tris.len()))
309            .collect();
310        let found = parallel::map_ordered(&chunks, |_, range| {
311            for t in range.clone() {
312                let a = shifted(&inner.tris[t], 0);
313                let (lo, hi) = Operand::bounds(&a);
314                for u in part.near(lo, hi) {
315                    let b = Tri {
316                        side: 1,
317                        ..part.tris[u as usize]
318                    };
319                    if !meet_at_shared_only(&a, &b) {
320                        return Some((t + first, u as usize));
321                    }
322                }
323                for u in inner.near(lo, hi) {
324                    if (u as usize) <= t {
325                        continue;
326                    }
327                    if !meet_at_shared_only(&a, &shifted(&inner.tris[u as usize], 1)) {
328                        return Some((t + first, u as usize + first));
329                    }
330                }
331            }
332            None
333        });
334        if let Some((t, u)) = found.into_iter().flatten().next() {
335            ogeom_bail!(
336                Construction,
337                "the {name} mesh crosses itself: triangles {t} and {u} meet away from their shared corners"
338            );
339        }
340        part.check_wound(name, Some(&inner), 0)?;
341        inner.check_wound(name, Some(part), first)
342    }
343}
344
345/// One operand, checked and indexed.
346struct Operand<'a> {
347    side: Side,
348    name: &'static str,
349    mesh: &'a Triangulation,
350    tris: Vec<Tri>,
351    bvh: Bvh,
352    /// For each triangle, the triangle across each side, side `k` running
353    /// from corner `k` to the next.
354    across: Vec<[u32; 3]>,
355}
356
357fn coords(p: Point) -> P3 {
358    // `+ 0.0` turns a negative zero positive, so equal points have equal
359    // bits.
360    [p.x + 0.0, p.y + 0.0, p.z + 0.0]
361}
362
363fn id(i: usize) -> u32 {
364    u32::try_from(i).unwrap_or(u32::MAX)
365}
366
367impl<'a> Operand<'a> {
368    fn new(mesh: &'a Triangulation, side: Side, name: &'static str) -> OgeomResult<Self> {
369        if mesh.triangles.is_empty() {
370            ogeom_bail!(NullObject, "the {name} mesh has no triangles");
371        }
372        if u32::try_from(mesh.positions.len()).is_err() {
373            ogeom_bail!(Range, "the {name} mesh has more than four billion vertices");
374        }
375        let bvh = Bvh::build(mesh)?;
376        let mut tris = Vec::with_capacity(mesh.triangles.len());
377        let mut edges: FastMap<(u32, u32), u32> = FastMap::default();
378        edges.reserve(3 * mesh.triangles.len());
379        for (t, &v) in mesh.triangles.iter().enumerate() {
380            let p = v.map(|k| coords(mesh.positions[k as usize]));
381            let flat = (0..3).all(|k| normal_sign(&p, k).is_zero());
382            if flat {
383                ogeom_bail!(
384                    Construction,
385                    "triangle {t} of the {name} mesh is degenerate: its corners are collinear"
386                );
387            }
388            for k in 0..3 {
389                if edges.insert((v[k], v[(k + 1) % 3]), id(t)).is_some() {
390                    ogeom_bail!(
391                        Construction,
392                        "the {name} mesh is not 2-manifold: edge {}-{} is run the same way twice",
393                        v[k],
394                        v[(k + 1) % 3]
395                    );
396                }
397            }
398            tris.push(Tri {
399                side,
400                id: id(t),
401                v,
402                p,
403            });
404        }
405        let chunks: Vec<std::ops::Range<usize>> = (0..tris.len())
406            .step_by(CHUNK)
407            .map(|s| s..(s + CHUNK).min(tris.len()))
408            .collect();
409        let across: Vec<Vec<Option<[u32; 3]>>> = parallel::map_ordered(&chunks, |_, range| {
410            range
411                .clone()
412                .map(|t| {
413                    let v: [u32; 3] = tris[t].v;
414                    let mut out = [0; 3];
415                    for k in 0..3 {
416                        out[k] = *edges.get(&(v[(k + 1) % 3], v[k]))?;
417                    }
418                    Some(out)
419                })
420                .collect()
421        });
422        let across: Vec<[u32; 3]> = across
423            .into_iter()
424            .flatten()
425            .collect::<Option<_>>()
426            .map_or_else(
427                || {
428                    let (lo, hi) = edges
429                        .keys()
430                        .filter(|&&(u, w)| !edges.contains_key(&(w, u)))
431                        .map(|&(u, w)| (u.min(w), u.max(w)))
432                        .min()
433                        .unwrap_or((0, 0));
434                    Err(ogeom_core::ogeom_err!(
435                        Construction,
436                        "the {name} mesh is open: edge {lo}-{hi} has one triangle"
437                    ))
438                },
439                Ok,
440            )?;
441        drop(edges);
442        let operand = Self {
443            side,
444            name,
445            mesh,
446            tris,
447            bvh,
448            across,
449        };
450        operand.check_fans()?;
451        Ok(operand)
452    }
453
454    /// Every vertex's triangles form one fan.
455    fn check_fans(&self) -> OgeomResult<()> {
456        let mut corners = vec![0_u32; self.mesh.positions.len()];
457        let mut first = vec![u32::MAX; self.mesh.positions.len()];
458        for (t, tri) in self.tris.iter().enumerate() {
459            for &v in &tri.v {
460                corners[v as usize] += 1;
461                if first[v as usize] == u32::MAX {
462                    first[v as usize] = id(t);
463                }
464            }
465        }
466        for (v, &start) in first.iter().enumerate() {
467            if start == u32::MAX {
468                continue;
469            }
470            let v = id(v);
471            let mut at = start;
472            let mut count = 0;
473            loop {
474                count += 1;
475                let t = self.tris[at as usize].v;
476                let k = t.iter().position(|&x| x == v).unwrap_or(0);
477                // The next triangle about `v` is across the side into `v`.
478                let next = self.across[at as usize][(k + 2) % 3];
479                if next == start || count > corners[v as usize] {
480                    break;
481                }
482                at = next;
483            }
484            if count != corners[v as usize] {
485                ogeom_bail!(
486                    Construction,
487                    "the {} mesh is not 2-manifold at vertex {v}: its triangles form more than one fan",
488                    self.name
489                );
490            }
491        }
492        Ok(())
493    }
494
495    fn bounds(t: &Tri) -> (P3, P3) {
496        let mut lo = t.p[0];
497        let mut hi = t.p[0];
498        for q in &t.p[1..] {
499            for k in 0..3 {
500                lo[k] = lo[k].min(q[k]);
501                hi[k] = hi[k].max(q[k]);
502            }
503        }
504        (lo, hi)
505    }
506
507    /// The triangles of this operand whose boxes touch the box `lo..hi`.
508    fn near(&self, lo: P3, hi: P3) -> Vec<u32> {
509        self.bvh.triangles_where(|b: &Aabb| {
510            let (Some(bl), Some(bh)) = (b.low(), b.high()) else {
511                return false;
512            };
513            let (bl, bh) = (coords(bl), coords(bh));
514            (0..3).all(|k| bl[k] <= hi[k] && lo[k] <= bh[k])
515        })
516    }
517
518    /// No two triangles meet but where they share corners: along their
519    /// common edge, or at their common vertex. Only pairs holding a
520    /// triangle numbered `first` or after are tested.
521    fn check_embedded(&self, first: usize) -> OgeomResult<()> {
522        let chunks: Vec<std::ops::Range<usize>> = (first..self.tris.len())
523            .step_by(CHUNK)
524            .map(|s| s..(s + CHUNK).min(self.tris.len()))
525            .collect();
526        let found = parallel::map_ordered(&chunks, |_, range| {
527            for t in range.clone() {
528                let a = &self.tris[t];
529                let (lo, hi) = Self::bounds(a);
530                for u in self.near(lo, hi) {
531                    let u_at = u as usize;
532                    if u_at == t || (u_at >= first && u_at < t) {
533                        continue;
534                    }
535                    let b = Tri {
536                        side: 1 - self.side,
537                        ..self.tris[u as usize]
538                    };
539                    if !meet_at_shared_only(a, &b) {
540                        return Some((t, u));
541                    }
542                }
543            }
544            None
545        });
546        if let Some((t, u)) = found.into_iter().flatten().next() {
547            ogeom_bail!(
548                Construction,
549                "the {} mesh crosses itself: triangles {t} and {u} meet away from their shared corners",
550                self.name
551            );
552        }
553        Ok(())
554    }
555
556    /// Each connected piece faces out of the solid: from the middle of one
557    /// of its triangles, a ray leaving the way it faces finds the winding
558    /// number of the whole mesh 0, and behind it 1.
559    ///
560    /// With `also`, the winding number counts its triangles too; messages
561    /// name the mesh `name` and number its triangles from `base`.
562    fn check_wound(&self, name: &str, also: Option<&Operand<'_>>, base: usize) -> OgeomResult<()> {
563        let mut parent: Vec<u32> = (0..id(self.tris.len())).collect();
564        for (t, near) in self.across.iter().enumerate().take(self.tris.len()) {
565            for &u in near {
566                union(&mut parent, id(t), u);
567            }
568        }
569        let mut seen = FastSet::default();
570        for t in 0..self.tris.len() {
571            if !seen.insert(find(&mut parent, id(t))) {
572                continue;
573            }
574            let tri = &self.tris[t];
575            let axis = by_weight(normal_approx(&tri.p))
576                .into_iter()
577                .find(|&k| !normal_sign(&tri.p, k).is_zero())
578                .unwrap_or(0);
579            let corner = tri.p.map(|q| Pt::new(Def::Vertex(q)));
580            let mid = Mid([&corner[0], &corner[1], &corner[2]]);
581            let out = normal_sign(&tri.p, axis);
582            let mut w = classify::winding(&self.tris, &self.bvh, &mid, axis, out, Some(id(t)))?;
583            if let Some(o) = also {
584                w += classify::winding(&o.tris, &o.bvh, &mid, axis, out, None)?;
585            }
586            if w != 0 {
587                ogeom_bail!(
588                    Construction,
589                    "the {name} mesh is not wound outward: triangle {} faces into the solid",
590                    t + base
591                );
592            }
593        }
594        Ok(())
595    }
596
597    /// For each triangle, a coordinate its plane is not parallel to, the
598    /// one its normal is largest along.
599    fn drops(&self) -> OgeomResult<Vec<usize>> {
600        self.tris
601            .iter()
602            .map(|t| {
603                by_weight(normal_approx(&t.p))
604                    .into_iter()
605                    .find(|&k| !normal_sign(&t.p, k).is_zero())
606                    .ok_or_else(|| ogeom_core::ogeom_err!(Invariant, "a triangle has no normal"))
607            })
608            .collect()
609    }
610}
611
612/// Whether two triangles of one mesh, `b` named as the other side, meet
613/// nowhere but at the corners they share.
614///
615/// The side tests [`apart_but_shared`] and [`pairs::meet`] begin with
616/// settle most pairs, and are taken first without building anything:
617/// sharing an edge, the triangles meet only along it when the fourth
618/// vertex is off the plane of the other three; sharing a corner, when one
619/// triangle's other two corners are strictly on one side of the other's
620/// plane; sharing nothing, when one triangle's corners are all strictly on
621/// one side of the other's plane.
622fn meet_at_shared_only(a: &Tri, b: &Tri) -> bool {
623    // The corners of `y` not shared with `x`, all strictly on one side of
624    // `x`'s plane.
625    let off = |x: &Tri, y: &Tri| {
626        let mut sign = None;
627        for k in 0..3 {
628            if x.v.contains(&y.v[k]) {
629                continue;
630            }
631            let s = Exact::orient3d(x.p[0], x.p[1], x.p[2], y.p[k]);
632            if s.is_zero() || sign.is_some_and(|t| t != s) {
633                return false;
634            }
635            sign = Some(s);
636        }
637        true
638    };
639    let settled = match (0..3).filter(|&k| b.v.contains(&a.v[k])).count() {
640        2 => off(a, b),
641        0 | 1 => off(a, b) || off(b, a),
642        _ => false,
643    };
644    settled || meet_at_shared_only_built(a, b)
645}
646
647/// [`meet_at_shared_only`] in full.
648fn meet_at_shared_only_built(a: &Tri, b: &Tri) -> bool {
649    let shared: Vec<u32> = a.v.iter().copied().filter(|v| b.v.contains(v)).collect();
650    shared.len() < 3
651        && (apart_but_shared(a, b, &shared)
652            || pairs::meet(a, b).is_none_or(|touch| {
653                touch
654                    .points
655                    .iter()
656                    .all(|(key, _)| matches!(key, Key::Vertex(_, v) if shared.contains(v)))
657            }))
658}
659
660/// [`meet_at_shared_only`] for two triangles given by their vertex numbers
661/// and corners: whether they meet nowhere but at the vertices they share,
662/// decided exactly.
663pub(crate) fn apart_but_at_shared(a: ([u32; 3], [Point; 3]), b: ([u32; 3], [Point; 3])) -> bool {
664    let tri = |side: Side, (v, p): ([u32; 3], [Point; 3])| Tri {
665        side,
666        id: u32::from(side),
667        v,
668        p: p.map(coords),
669    };
670    meet_at_shared_only(&tri(0, a), &tri(1, b))
671}
672
673/// Whether two triangles of one mesh sharing the corners `shared` meet
674/// only there, decided from the planes alone where that is enough.
675///
676/// Sharing an edge and not coplanar, they meet in the line their planes
677/// meet in, which each crosses only along that edge. Sharing a corner, they
678/// meet only there when the other two corners of one lie strictly on one
679/// side of the other's plane.
680fn apart_but_shared(a: &Tri, b: &Tri, shared: &[u32]) -> bool {
681    let others = |t: &Tri| -> Vec<P3> {
682        (0..3)
683            .filter(|&k| !shared.contains(&t.v[k]))
684            .map(|k| t.p[k])
685            .collect()
686    };
687    let against = |t: &Tri, q: P3| Exact::orient3d(t.p[0], t.p[1], t.p[2], q);
688    let apart = match shared.len() {
689        2 => others(b).first().is_some_and(|&w| !against(a, w).is_zero()),
690        1 => [(a, b), (b, a)].iter().any(|(x, y)| {
691            let o = others(y);
692            let (s, t) = (against(x, o[0]), against(x, o[1]));
693            !s.is_zero() && s == t
694        }),
695        _ => false,
696    };
697    apart || (others(b).iter().all(|&q| against(a, q).is_zero()) && flat_apart(a, b, shared))
698}
699
700/// As [`apart_but_shared`], for two coplanar triangles: some line through
701/// a shared corner along an edge has one triangle strictly on each side,
702/// but for the shared corners.
703fn flat_apart(a: &Tri, b: &Tri, shared: &[u32]) -> bool {
704    let Some(drop) = by_weight(normal_approx(&a.p))
705        .into_iter()
706        .find(|&k| !normal_sign(&a.p, k).is_zero())
707    else {
708        return false;
709    };
710    let (i, j) = points::axes(drop);
711    let flat = |q: P3| [q[i], q[j]];
712    let others = |t: &Tri| -> Vec<[f64; 2]> {
713        (0..3)
714            .filter(|&k| !shared.contains(&t.v[k]))
715            .map(|k| flat(t.p[k]))
716            .collect()
717    };
718    let at = |t: &Tri, v: u32| t.v.iter().position(|&x| x == v).map(|k| flat(t.p[k]));
719    let splits = |p: [f64; 2], q: [f64; 2], mine: &[[f64; 2]], theirs: &[[f64; 2]]| {
720        let side = |r: [f64; 2]| Exact::orient2d(p, q, r);
721        let Some(&first) = mine.first() else {
722            return false;
723        };
724        let s = side(first);
725        !s.is_zero()
726            && mine.iter().all(|&r| side(r) == s)
727            && theirs.iter().all(|&r| side(r) == s.reversed())
728    };
729    match shared {
730        [u, w] => match (at(a, *u), at(a, *w)) {
731            (Some(p), Some(q)) => splits(p, q, &others(a), &others(b)),
732            _ => false,
733        },
734        [v] => [(a, b), (b, a)].iter().any(|(x, y)| {
735            let Some(p) = at(x, *v) else {
736                return false;
737            };
738            let mine = others(x);
739            (0..2).any(|k| splits(p, mine[k], &mine[1 - k..2 - k], &others(y)))
740        }),
741        _ => false,
742    }
743}
744
745/// How many triangles a parallel task takes.
746const CHUNK: usize = 256;
747
748fn find(parent: &mut [u32], mut x: u32) -> u32 {
749    while parent[x as usize] != x {
750        let up = parent[parent[x as usize] as usize];
751        parent[x as usize] = up;
752        x = up;
753    }
754    x
755}
756
757/// Join two sets, the lower index the root.
758fn union(parent: &mut [u32], a: u32, b: u32) {
759    let (ra, rb) = (find(parent, a), find(parent, b));
760    if ra < rb {
761        parent[rb as usize] = ra;
762    } else if rb < ra {
763        parent[ra as usize] = rb;
764    }
765}
766
767/// Every pair of triangles, one from each operand, that meet, and what
768/// they share, in the order of the first operand's triangles.
769fn meet_all(a: &Operand<'_>, b: &Operand<'_>) -> Vec<(u32, u32, Touch)> {
770    let chunks: Vec<std::ops::Range<usize>> = (0..a.tris.len())
771        .step_by(CHUNK)
772        .map(|s| s..(s + CHUNK).min(a.tris.len()))
773        .collect();
774    parallel::map_ordered(&chunks, |_, range| {
775        let mut out = Vec::new();
776        for t in range.clone() {
777            let ta = &a.tris[t];
778            let (lo, hi) = Operand::bounds(ta);
779            for u in b.near(lo, hi) {
780                if let Some(touch) = pairs::meet(ta, &b.tris[u as usize]) {
781                    out.push((id(t), u, touch));
782                }
783            }
784        }
785        out
786    })
787    .into_iter()
788    .flatten()
789    .collect()
790}
791
792/// What the other operand left on one triangle.
793#[derive(Debug, Clone, Default)]
794struct Mark {
795    /// Points on the triangle, as indices into the table.
796    points: Vec<u32>,
797    segments: Vec<[u32; 2]>,
798    /// The other operand's triangles coplanar with this one that it meets.
799    coplanar: Vec<u32>,
800}
801
802/// Every point, by index: the first operand's vertices, the second's, then
803/// the points made where they meet.
804struct Table {
805    pts: Vec<Pt>,
806    names: FastMap<Key, u32>,
807    /// The name of each point made, in order after the input vertices.
808    keys: Vec<Key>,
809    /// The vertex counts of the two operands.
810    counts: [usize; 2],
811    /// The index each point is merged into: the lowest of the points equal
812    /// to it.
813    rep: Vec<u32>,
814}
815
816impl Table {
817    fn new(ops: &[Operand<'_>; 2]) -> Self {
818        let mut pts = Vec::with_capacity(ops[0].mesh.positions.len() + ops[1].mesh.positions.len());
819        for o in ops {
820            pts.extend(
821                o.mesh
822                    .positions
823                    .iter()
824                    .map(|&p| Pt::new(Def::Vertex(coords(p)))),
825            );
826        }
827        Self {
828            pts,
829            names: FastMap::default(),
830            keys: Vec::new(),
831            counts: [ops[0].mesh.positions.len(), ops[1].mesh.positions.len()],
832            rep: Vec::new(),
833        }
834    }
835
836    fn index(&mut self, key: Key, pt: &Pt) -> u32 {
837        match key {
838            Key::Vertex(0, v) => v,
839            Key::Vertex(_, v) => id(self.counts[0]) + v,
840            _ => {
841                if let Some(&at) = self.names.get(&key) {
842                    return at;
843                }
844                let at = id(self.pts.len());
845                self.pts.push(Pt::new(pt.def));
846                self.names.insert(key, at);
847                self.keys.push(key);
848                at
849            }
850        }
851    }
852
853    fn take(&mut self, marks: &mut [Vec<Mark>; 2], ta: u32, tb: u32, touch: &Touch) {
854        let ids: Vec<u32> = touch
855            .points
856            .iter()
857            .map(|(key, pt)| self.index(*key, pt))
858            .collect();
859        for (side, t, other) in [(0, ta, tb), (1, tb, ta)] {
860            let m = &mut marks[side][t as usize];
861            m.points.extend(&ids);
862            m.segments
863                .extend(touch.segments.iter().map(|s| [ids[s[0]], ids[s[1]]]));
864            if touch.coplanar {
865                m.coplanar.push(other);
866            }
867        }
868    }
869
870    /// Merge every point with the lowest-indexed point equal to it.
871    fn merge(&mut self, ops: &[Operand<'_>; 2]) {
872        let n = self.pts.len();
873        let mut parent: Vec<u32> = (0..id(n)).collect();
874        let explicit = self.counts[0] + self.counts[1];
875        let mut used = vec![false; explicit];
876        for (side, o) in ops.iter().enumerate() {
877            let base = if side == 0 { 0 } else { self.counts[0] };
878            for t in &o.tris {
879                for &v in &t.v {
880                    used[base + v as usize] = true;
881                }
882            }
883        }
884        // Input vertices, equal when their coordinates are.
885        let bits = |p: &P3| p.map(f64::to_bits);
886        let mut at: FastMap<[u64; 3], u32> = FastMap::default();
887        let mut sorted: Vec<(P3, u32)> = Vec::new();
888        for (i, _) in used.iter().enumerate().filter(|(_, u)| **u) {
889            let Def::Vertex(p) = self.pts[i].def else {
890                continue;
891            };
892            match at.get(&bits(&p)) {
893                Some(&j) => union(&mut parent, j, id(i)),
894                None => {
895                    at.insert(bits(&p), id(i));
896                    sorted.push((p, id(i)));
897                }
898            }
899        }
900        sorted.sort_by(|x, y| {
901            x.0[0]
902                .total_cmp(&y.0[0])
903                .then(x.0[1].total_cmp(&y.0[1]))
904                .then(x.0[2].total_cmp(&y.0[2]))
905                .then(x.1.cmp(&y.1))
906        });
907        // Made points against the input vertices inside their boxes.
908        type Bounds = Option<[(f64, f64); 3]>;
909        let made: Vec<(u32, Bounds)> = (explicit..n)
910            .map(|i| {
911                let b = self.pts[i].bounds().map(|b| b.map(|v| (v.lo, v.hi)));
912                (id(i), b)
913            })
914            .collect();
915        let pairs_found: Vec<Vec<(u32, u32)>> = parallel::map_ordered(&made, |_, &(i, b)| {
916            let mut out = Vec::new();
917            let pt = &self.pts[i as usize];
918            match b {
919                Some(b) => {
920                    let start = sorted.partition_point(|e| e.0[0] < b[0].0);
921                    for e in &sorted[start..] {
922                        if e.0[0] > b[0].1 {
923                            break;
924                        }
925                        let inside = (1..3).all(|k| b[k].0 <= e.0[k] && e.0[k] <= b[k].1);
926                        if inside && points::same(pt, &self.pts[e.1 as usize]) {
927                            out.push((e.1, i));
928                        }
929                    }
930                }
931                None => {
932                    for e in &sorted {
933                        if points::same(pt, &self.pts[e.1 as usize]) {
934                            out.push((e.1, i));
935                        }
936                    }
937                }
938            }
939            out
940        });
941        for (x, y) in pairs_found.into_iter().flatten() {
942            union(&mut parent, x, y);
943        }
944        // Made points against each other, swept along x.
945        let mut boxed: Vec<(u32, [(f64, f64); 3])> = Vec::new();
946        let mut loose: Vec<u32> = Vec::new();
947        for &(i, b) in &made {
948            match b {
949                Some(b) => boxed.push((i, b)),
950                None => loose.push(i),
951            }
952        }
953        boxed.sort_by(|x, y| x.1[0].0.total_cmp(&y.1[0].0).then(x.0.cmp(&y.0)));
954        let mut pairs_found = Vec::new();
955        for (k, (i, b)) in boxed.iter().enumerate() {
956            for (j, c) in &boxed[k + 1..] {
957                if c[0].0 > b[0].1 {
958                    break;
959                }
960                let overlap = (1..3).all(|d| c[d].0 <= b[d].1 && b[d].0 <= c[d].1);
961                if overlap && points::same(&self.pts[*i as usize], &self.pts[*j as usize]) {
962                    pairs_found.push((*i, *j));
963                }
964            }
965        }
966        for &i in &loose {
967            for &(j, _) in &made {
968                if j != i && points::same(&self.pts[i as usize], &self.pts[j as usize]) {
969                    pairs_found.push((i, j));
970                }
971            }
972        }
973        for (x, y) in pairs_found {
974            union(&mut parent, x, y);
975        }
976        self.rep = (0..id(n)).map(|i| find(&mut parent, i)).collect();
977    }
978
979    /// Rename every mark's points to their merged index, drop repeats and
980    /// segments merged to a point, and share the points on each edge with
981    /// the triangle across it.
982    fn settle(&self, o: &Operand<'_>, flat: &[usize], marks: &mut [Mark]) {
983        let base = if o.side == 0 { 0 } else { self.counts[0] };
984        let corners = |t: &Tri| t.v.map(|v| self.rep[base + v as usize]);
985        type Settled = (Vec<u32>, Vec<[u32; 2]>, Vec<(u32, u32)>);
986        let which: Vec<usize> = (0..marks.len())
987            .filter(|&t| !(marks[t].points.is_empty() && marks[t].segments.is_empty()))
988            .collect();
989        let settled: Vec<Settled> = parallel::map_ordered(&which, |_, &t| {
990            let m = &marks[t];
991            let tri = &o.tris[t];
992            let c = corners(tri);
993            let mut raw: Vec<(u32, u32)> = m
994                .points
995                .iter()
996                .map(|&x| (self.rep[x as usize], x))
997                .filter(|(r, _)| !c.contains(r))
998                .collect();
999            raw.sort_unstable();
1000            raw.dedup();
1001            let mut points = Vec::new();
1002            let mut shares = Vec::new();
1003            for group in raw.chunk_by(|x, y| x.0 == y.0) {
1004                let r = group[0].0;
1005                points.push(r);
1006                let on = group
1007                    .iter()
1008                    .find_map(|&(_, x)| self.edge_of(o.side, tri, x))
1009                    .or_else(|| {
1010                        (0..3).find(|&k| {
1011                            orient2d(
1012                                flat[t],
1013                                &self.pts[c[k] as usize],
1014                                &self.pts[c[(k + 1) % 3] as usize],
1015                                &self.pts[r as usize],
1016                            )
1017                            .is_zero()
1018                        })
1019                    });
1020                if let Some(k) = on {
1021                    shares.push((o.across[t][k], r));
1022                }
1023            }
1024            let mut segments: Vec<[u32; 2]> = m
1025                .segments
1026                .iter()
1027                .map(|s| {
1028                    let (a, b) = (self.rep[s[0] as usize], self.rep[s[1] as usize]);
1029                    if a < b { [a, b] } else { [b, a] }
1030                })
1031                .filter(|s| s[0] != s[1])
1032                .collect();
1033            segments.sort_unstable();
1034            segments.dedup();
1035            (points, segments, shares)
1036        });
1037        let mut shares = Vec::new();
1038        for (&t, (points, segments, share)) in which.iter().zip(settled) {
1039            let m = &mut marks[t];
1040            m.points = points;
1041            m.segments = segments;
1042            m.coplanar.sort_unstable();
1043            m.coplanar.dedup();
1044            shares.extend(share);
1045        }
1046        for (u, x) in shares {
1047            let m = &mut marks[u as usize];
1048            if let Err(at) = m.points.binary_search(&x) {
1049                m.points.insert(at, x);
1050            }
1051        }
1052    }
1053
1054    /// The side of `tri` that point `x` lies on by its name: where an edge
1055    /// of `tri` crosses the other operand.
1056    fn edge_of(&self, side: Side, tri: &Tri, x: u32) -> Option<usize> {
1057        let made = (x as usize).checked_sub(self.counts[0] + self.counts[1])?;
1058        let edge = match self.keys[made] {
1059            Key::Lpi { side: s, edge, .. } if s == side => edge,
1060            Key::Ssi { a, b } => {
1061                if side == 0 {
1062                    a
1063                } else {
1064                    b
1065                }
1066            }
1067            _ => return None,
1068        };
1069        (0..3).find(|&k| {
1070            let (u, w) = (tri.v[k], tri.v[(k + 1) % 3]);
1071            [u.min(w), u.max(w)] == edge
1072        })
1073    }
1074
1075    /// The pieces of every triangle of one operand: whole where nothing
1076    /// marked it, cut where something did. The constraint edges of the cuts
1077    /// are added to `fixed`.
1078    fn cut(
1079        &self,
1080        o: &Operand<'_>,
1081        flat: &[usize],
1082        marks: &[Mark],
1083        fixed: &mut FastSet<(u32, u32)>,
1084    ) -> OgeomResult<Cut> {
1085        let marked: Vec<bool> = marks
1086            .iter()
1087            .map(|m| !(m.points.is_empty() && m.segments.is_empty()))
1088            .collect();
1089        let which: Vec<usize> = (0..marks.len()).filter(|&t| marked[t]).collect();
1090        let cuts = parallel::map_ordered(&which, |_, &t| {
1091            split::split(
1092                &self.pts,
1093                flat[t],
1094                self.corners(o, t),
1095                &marks[t].points,
1096                &marks[t].segments,
1097            )
1098        });
1099        let mut done = which.iter().zip(cuts).peekable();
1100        let mut pieces = Vec::with_capacity(o.tris.len());
1101        let mut start = Vec::with_capacity(o.tris.len() + 1);
1102        for t in 0..o.tris.len() {
1103            start.push(pieces.len());
1104            if done.peek().is_some_and(|(u, _)| **u == t) {
1105                let Some((_, c)) = done.next() else {
1106                    continue;
1107                };
1108                let c = c?;
1109                fixed.extend(c.fixed.iter().map(|&[a, b]| (a, b)));
1110                let cut = c.triangles.len() > 1;
1111                pieces.extend(c.triangles.into_iter().map(|v| Piece {
1112                    v,
1113                    parent: id(t),
1114                    cut,
1115                }));
1116            } else {
1117                pieces.push(Piece {
1118                    v: self.corners(o, t),
1119                    parent: id(t),
1120                    cut: false,
1121                });
1122            }
1123        }
1124        start.push(pieces.len());
1125        Ok(Cut {
1126            pieces,
1127            start,
1128            marked,
1129        })
1130    }
1131
1132    /// The corners of triangle `t` of operand `o`, as merged points.
1133    fn corners(&self, o: &Operand<'_>, t: usize) -> [u32; 3] {
1134        let base = if o.side == 0 { 0 } else { self.counts[0] };
1135        o.tris[t].v.map(|v| self.rep[base + v as usize])
1136    }
1137
1138    /// Classify each region of one operand's pieces against the other
1139    /// operand, from one piece of it.
1140    fn label(
1141        &self,
1142        ops: &[Operand<'_>; 2],
1143        side: usize,
1144        flat: &[usize],
1145        marks: &[Mark],
1146        pieces: &[Piece],
1147        regions: Vec<Vec<usize>>,
1148    ) -> OgeomResult<Vec<(Vec<usize>, Label)>> {
1149        let (own, other) = (&ops[side], &ops[1 - side]);
1150        let labels = parallel::map_ordered(&regions, |_, region| -> OgeomResult<Label> {
1151            let piece = &pieces[region[0]];
1152            let t = piece.parent as usize;
1153            let drop = flat[t];
1154            let corner = piece.v.map(|v| &self.pts[v as usize]);
1155            let mid = Mid(corner);
1156            let parent = &own.tris[t];
1157            for &u in &marks[t].coplanar {
1158                let q = &other.tris[u as usize];
1159                let qc = q.p.map(|p| Pt::new(Def::Vertex(p)));
1160                let facing = normal_sign(&q.p, drop);
1161                let o = [0, 1, 2].map(|k| orient2d(drop, &qc[k], &qc[(k + 1) % 3], &mid));
1162                // A piece lies wholly inside or wholly outside each coplanar
1163                // triangle, whose edges cut it; its centroid on an edge would
1164                // mean a cut was missed.
1165                if o.contains(&facing.reversed()) {
1166                    continue;
1167                }
1168                if o.contains(&Sign::Zero) {
1169                    ogeom_bail!(Invariant, "a piece straddles a coplanar triangle's edge");
1170                }
1171                return Ok(if facing == normal_sign(&parent.p, drop) {
1172                    Label::OnSame
1173                } else {
1174                    Label::OnOpposite
1175                });
1176            }
1177            let w = classify::winding(&other.tris, &other.bvh, &mid, drop, Sign::Positive, None)?;
1178            match w {
1179                0 => Ok(Label::Out),
1180                1 => Ok(Label::In),
1181                _ => ogeom_bail!(
1182                    Invariant,
1183                    "the {} mesh winds {w} times about a point",
1184                    other.name
1185                ),
1186            }
1187        });
1188        regions
1189            .into_iter()
1190            .zip(labels)
1191            .map(|(r, l)| Ok((r, l?)))
1192            .collect()
1193    }
1194}
1195
1196/// A triangle of a cut operand, by point indices.
1197#[derive(Debug, Clone, Copy)]
1198struct Piece {
1199    v: [u32; 3],
1200    parent: u32,
1201    cut: bool,
1202}
1203
1204/// Where a region of one operand lies against the other.
1205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1206enum Label {
1207    In,
1208    Out,
1209    /// On a face of the other, both facing the same way.
1210    OnSame,
1211    /// On a face of the other, facing opposite ways.
1212    OnOpposite,
1213}
1214
1215/// The pieces of one cut operand.
1216struct Cut {
1217    pieces: Vec<Piece>,
1218    /// Where each triangle's pieces start, and the end of the last.
1219    start: Vec<usize>,
1220    /// Whether each triangle was triangulated afresh.
1221    marked: Vec<bool>,
1222}
1223
1224/// The pieces of one cut operand grouped into regions: joined across every
1225/// edge that is not a constraint, each region's pieces in order and the
1226/// regions in the order of their first piece.
1227///
1228/// A triangle left whole is joined to its neighbours left whole through
1229/// the operand's own edges: no constraint runs along an edge with a whole
1230/// triangle on both sides, since a constraint lies on a triangle that is
1231/// cut. The pieces of cut triangles, and the whole triangles beside them,
1232/// are joined through their edges as merged points.
1233fn regions(
1234    o: &Operand<'_>,
1235    cut: &Cut,
1236    fixed: &FastSet<(u32, u32)>,
1237) -> OgeomResult<Vec<Vec<usize>>> {
1238    let pieces = &cut.pieces;
1239    let near_cut = |t: usize| o.across[t].iter().any(|&u| cut.marked[u as usize]);
1240    let mut edges: FastMap<(u32, u32), u32> = FastMap::default();
1241    for t in 0..o.tris.len() {
1242        if !(cut.marked[t] || near_cut(t)) {
1243            continue;
1244        }
1245        for (i, p) in pieces
1246            .iter()
1247            .enumerate()
1248            .take(cut.start[t + 1])
1249            .skip(cut.start[t])
1250        {
1251            for k in 0..3 {
1252                if edges.insert((p.v[k], p.v[(k + 1) % 3]), id(i)).is_some() {
1253                    ogeom_bail!(Invariant, "the cut {} mesh runs an edge twice", o.name);
1254                }
1255            }
1256        }
1257    }
1258    let mut parent: Vec<u32> = (0..id(pieces.len())).collect();
1259    for t in 0..o.tris.len() {
1260        for (i, p) in pieces
1261            .iter()
1262            .enumerate()
1263            .take(cut.start[t + 1])
1264            .skip(cut.start[t])
1265        {
1266            for k in 0..3 {
1267                if !cut.marked[t] {
1268                    let u = o.across[t][k];
1269                    if !cut.marked[u as usize] {
1270                        union(&mut parent, id(i), id(cut.start[u as usize]));
1271                        continue;
1272                    }
1273                }
1274                let (a, b) = (p.v[k], p.v[(k + 1) % 3]);
1275                let Some(&j) = edges.get(&(b, a)) else {
1276                    ogeom_bail!(Invariant, "the cut {} mesh is open", o.name);
1277                };
1278                if !fixed.contains(&(a.min(b), a.max(b))) {
1279                    union(&mut parent, id(i), j);
1280                }
1281            }
1282        }
1283    }
1284    let mut slot = vec![usize::MAX; pieces.len()];
1285    let mut out: Vec<Vec<usize>> = Vec::new();
1286    for i in 0..pieces.len() {
1287        let r = find(&mut parent, id(i)) as usize;
1288        if slot[r] == usize::MAX {
1289            slot[r] = out.len();
1290            out.push(Vec::new());
1291        }
1292        out[slot[r]].push(i);
1293    }
1294    Ok(out)
1295}
1296
1297/// Whether `op` keeps a region of operand `side` labelled `label`, and
1298/// whether it turns it over.
1299const fn keep(op: BoolKind, side: usize, label: Label) -> Option<bool> {
1300    let kept = matches!(
1301        (op, side, label),
1302        (BoolKind::Union, _, Label::Out)
1303            | (BoolKind::Union | BoolKind::Intersection, 0, Label::OnSame)
1304            | (BoolKind::Intersection, _, Label::In)
1305            | (BoolKind::Difference, 0, Label::Out | Label::OnOpposite)
1306            | (BoolKind::Difference, 1, Label::In)
1307    );
1308    if kept {
1309        Some(matches!(op, BoolKind::Difference) && side == 1)
1310    } else {
1311        None
1312    }
1313}
1314
1315fn assemble(
1316    table: &Table,
1317    cuts: &[Cut; 2],
1318    labels: &[Vec<(Vec<usize>, Label)>; 2],
1319    op: BoolKind,
1320    deflection_met: bool,
1321) -> OgeomResult<MeshBoolean> {
1322    let mut kept: Vec<([u32; 3], bool, (usize, usize))> = Vec::new();
1323    for side in 0..2 {
1324        for (region, label) in &labels[side] {
1325            let Some(flip) = keep(op, side, *label) else {
1326                continue;
1327            };
1328            for &i in region {
1329                let p = &cuts[side].pieces[i];
1330                let v = if flip { [p.v[0], p.v[2], p.v[1]] } else { p.v };
1331                kept.push((v, p.cut, (side, i)));
1332            }
1333        }
1334    }
1335    kept.sort_unstable_by_key(|k| k.2);
1336    // Where the result could fail to be 2-manifold: at the points of cut
1337    // triangles, and where the operands share a point. Elsewhere it is an
1338    // operand's own surface, kept or dropped a whole region at a time.
1339    let mut focus = vec![false; table.pts.len()];
1340    for cut in cuts {
1341        for p in &cut.pieces {
1342            if cut.marked[p.parent as usize] {
1343                for &v in &p.v {
1344                    focus[v as usize] = true;
1345                }
1346            }
1347        }
1348    }
1349    for (i, &r) in table.rep.iter().enumerate() {
1350        if r as usize != i {
1351            focus[r as usize] = true;
1352        }
1353    }
1354    let mut slot = vec![u32::MAX; table.pts.len()];
1355    for k in &kept {
1356        for &v in &k.0 {
1357            slot[v as usize] = 0;
1358        }
1359    }
1360    let explicit = table.counts[0] + table.counts[1];
1361    let mut mesh = Triangulation::new();
1362    let mut rounding = 0.0_f64;
1363    let mut first_new_vertex = 0;
1364    let mut near = Vec::new();
1365    for (v, s) in slot.iter_mut().enumerate() {
1366        if *s == u32::MAX {
1367            continue;
1368        }
1369        *s = id(mesh.positions.len());
1370        if v < explicit {
1371            first_new_vertex += 1;
1372        }
1373        near.push(focus[v]);
1374        let (p, err) = table.pts[v].rounded();
1375        rounding = rounding.max(err);
1376        mesh.positions.push(Point::new(p[0], p[1], p[2]));
1377    }
1378    mesh.triangles = kept.iter().map(|k| k.0.map(|v| slot[v as usize])).collect();
1379    mesh.deflection_met = deflection_met;
1380    check_result(&mesh, &near)?;
1381    Ok(MeshBoolean {
1382        cut: kept.iter().map(|k| k.1).collect(),
1383        mesh,
1384        first_new_vertex,
1385        rounding,
1386    })
1387}
1388
1389/// The result is closed and 2-manifold about the vertices in `focus`: each
1390/// edge at one of them run once each way, one fan about each.
1391fn check_result(mesh: &Triangulation, focus: &[bool]) -> OgeomResult<()> {
1392    let at = |v: u32| focus[v as usize];
1393    let near: Vec<usize> = (0..mesh.triangles.len())
1394        .filter(|&t| mesh.triangles[t].iter().any(|&v| at(v)))
1395        .collect();
1396    let mut edges: FastMap<(u32, u32), u32> = FastMap::default();
1397    for &t in &near {
1398        let v = mesh.triangles[t];
1399        for k in 0..3 {
1400            if edges.insert((v[k], v[(k + 1) % 3]), id(t)).is_some() {
1401                ogeom_bail!(
1402                    Construction,
1403                    "the result would not be 2-manifold: the operands touch along an edge without overlapping there"
1404                );
1405            }
1406        }
1407    }
1408    if edges
1409        .keys()
1410        .any(|&(a, b)| (at(a) || at(b)) && !edges.contains_key(&(b, a)))
1411    {
1412        ogeom_bail!(Invariant, "the result is open");
1413    }
1414    let mut corners = vec![0_u32; mesh.positions.len()];
1415    for &t in &near {
1416        for &v in &mesh.triangles[t] {
1417            corners[v as usize] += 1;
1418        }
1419    }
1420    let mut seen = vec![false; mesh.positions.len()];
1421    for &t in &near {
1422        for &v in &mesh.triangles[t] {
1423            if !at(v) || seen[v as usize] {
1424                continue;
1425            }
1426            seen[v as usize] = true;
1427            let (start, mut here, mut count) = (id(t), id(t), 0);
1428            loop {
1429                count += 1;
1430                let w = mesh.triangles[here as usize];
1431                let k = w.iter().position(|&x| x == v).unwrap_or(0);
1432                match edges.get(&(v, w[(k + 2) % 3])) {
1433                    Some(&next) if next != start && count <= corners[v as usize] => here = next,
1434                    _ => break,
1435                }
1436            }
1437            if count != corners[v as usize] {
1438                ogeom_bail!(
1439                    Construction,
1440                    "the result would not be 2-manifold: the operands touch at a point without overlapping there"
1441                );
1442            }
1443        }
1444    }
1445    Ok(())
1446}
1447
1448#[cfg(test)]
1449mod tests;