Skip to main content

teamcraft/mesh/creo/
tess.rs

1//! Tessellation of the Creo B-rep: contours → regions → ear clipping in the
2//! parameter plane → 3D, with every edge evaluated once on its most exact
3//! face so that neighbouring faces share identical vertices, then refined
4//! with PN triangles along the analytic normals.
5
6use std::collections::{BTreeMap, HashMap, HashSet};
7
8use anyhow::{anyhow, bail, Result};
9
10use super::brep::{
11    candidates, cross, dot, eval, grow, norm, sub, Brep, Conv, Curve, Kind, Surface, P3,
12};
13use crate::mesh::{Mesh, MAX_TRIANGLES};
14
15/// Largest number of edges followed around one contour.
16const MAX_LOOP: usize = 100_000;
17/// Weld distance relative to the model size.
18const WELD_REL: f64 = 1e-6;
19/// Edges whose two sides differ by more than this (relative to the model
20/// size) keep their own vertices per face: the disagreement stays visible.
21const SNAP_REL: f64 = 1e-3;
22/// Step of the finite differences for the normals (parameter units).
23const H: f64 = 1e-4;
24
25fn is_constant(vals: impl Iterator<Item = f64>) -> bool {
26    let mut first = None;
27    for v in vals {
28        match first {
29            None => first = Some(v),
30            Some(f) if (f - v).abs() > 1e-9 => return false,
31            _ => {}
32        }
33    }
34    first.is_some()
35}
36
37/// Faces referenced by edges but without a record of their own: the second
38/// half of a split periodic face. It shares the primitive of the known face
39/// behind a straight seam edge (type 1 or 8, constant u on both sides).
40pub fn add_twins(brep: &mut Brep) {
41    let ids: HashSet<i64> = brep.surfaces.iter().map(|s| s.id).collect();
42    let mut missing: Vec<i64> = brep
43        .curves
44        .iter()
45        .flat_map(|c| c.faces)
46        .filter(|g| !ids.contains(g))
47        .collect::<HashSet<_>>()
48        .into_iter()
49        .collect();
50    missing.sort_unstable();
51    let by_id: HashMap<i64, usize> = brep
52        .surfaces
53        .iter()
54        .enumerate()
55        .map(|(i, s)| (s.id, i))
56        .collect();
57    let mut twins = Vec::new();
58    for g in missing {
59        let mut partner: Option<usize> = None;
60        for c in &brep.curves {
61            if !c.faces.contains(&g) || !(c.ty == 1 || c.ty == 8) {
62                continue;
63            }
64            let side = usize::from(c.faces[0] != g);
65            let other = c.face(1 - side);
66            let Some(&oi) = by_id.get(&other) else {
67                continue;
68            };
69            if brep.surfaces.get(oi).is_some_and(|s| {
70                matches!(s.kind, Kind::Plane | Kind::Other) || s.kind.is_freeform()
71            }) {
72                continue;
73            }
74            if is_constant(c.pts.iter().map(|r| if side == 0 { r[0] } else { r[2] })) {
75                partner = Some(oi);
76                break;
77            }
78        }
79        let Some(pi) = partner else { continue };
80        let Some(p) = brep.surfaces.get(pi) else {
81            continue;
82        };
83        let mut tw = p.clone();
84        tw.id = g;
85        tw.twin_of = Some(p.id);
86        tw.contours = twin_contours(brep, g);
87        twins.push(tw);
88    }
89    brep.surfaces.extend(twins);
90}
91
92/// Contours of a twin: chains of the edges that reference it.
93fn twin_contours(brep: &Brep, g: i64) -> Vec<(i64, i64)> {
94    let on_g: HashMap<i64, (usize, usize)> = brep
95        .curves
96        .iter()
97        .enumerate()
98        .filter_map(|(ci, c)| {
99            let side = if c.faces[0] == g {
100                0
101            } else if c.faces[1] == g {
102                1
103            } else {
104                return None;
105            };
106            Some((c.id, (ci, side)))
107        })
108        .collect();
109    let mut starts = Vec::new();
110    let mut seen = HashSet::new();
111    let mut ids_sorted: Vec<i64> = on_g.keys().copied().collect();
112    ids_sorted.sort_unstable();
113    for cid0 in ids_sorted {
114        if seen.contains(&cid0) {
115            continue;
116        }
117        let mut cid = cid0;
118        for _ in 0..MAX_LOOP {
119            let Some(&(ci, side)) = on_g.get(&cid) else {
120                break;
121            };
122            if !seen.insert(cid) {
123                break;
124            }
125            cid = brep.curves.get(ci).map_or(cid, |c| c.next_on(side));
126        }
127        starts.push(cid0);
128    }
129    starts
130        .iter()
131        .enumerate()
132        .map(|(k, &s)| (s, if k == 0 { 1 } else { -1 }))
133        .collect()
134}
135
136/// Twins whose convention stayed unresolved got the wrong partner: the
137/// curved record whose best candidate fits the 3D points known from the
138/// resolved neighbours becomes the donor. Returns how many were changed.
139pub fn repair_twins(brep: &mut Brep, fixed: &HashMap<i64, Conv>) -> usize {
140    let donors: Vec<usize> = brep
141        .surfaces
142        .iter()
143        .enumerate()
144        .filter(|(_, s)| {
145            s.twin_of.is_none() && matches!(s.kind, Kind::Cylinder | Kind::Cone | Kind::Torus)
146        })
147        .map(|(i, _)| i)
148        .collect();
149    let by_id: HashMap<i64, usize> = brep
150        .surfaces
151        .iter()
152        .enumerate()
153        .map(|(i, s)| (s.id, i))
154        .collect();
155    let mut changes: Vec<(usize, usize)> = Vec::new();
156    for (ti, tw) in brep.surfaces.iter().enumerate() {
157        if tw.twin_of.is_none() || fixed.contains_key(&tw.id) {
158            continue;
159        }
160        let g = tw.id;
161        let mut samples: Vec<([f64; 2], P3)> = Vec::new();
162        for c in &brep.curves {
163            for sd in 0..2 {
164                if c.face(sd) != g {
165                    continue;
166                }
167                let other = c.face(1 - sd);
168                let (Some(oconv), Some(&oi)) = (fixed.get(&other), by_id.get(&other)) else {
169                    continue;
170                };
171                let Some(os) = brep.surfaces.get(oi) else {
172                    continue;
173                };
174                for r in &c.pts {
175                    if r.iter().all(|x| x.is_finite()) {
176                        let (um, vm, uo, vo) = if sd == 0 {
177                            (r[0], r[1], r[2], r[3])
178                        } else {
179                            (r[2], r[3], r[0], r[1])
180                        };
181                        let p = eval(os, oconv, uo, vo);
182                        if p.iter().all(|x| x.is_finite()) {
183                            samples.push(([um, vm], p));
184                        }
185                    }
186                }
187            }
188        }
189        if samples.is_empty() {
190            continue;
191        }
192        let mut best: Option<(f64, usize)> = None;
193        for &di in &donors {
194            let Some(d) = brep.surfaces.get(di) else {
195                continue;
196            };
197            for cand in candidates(d.kind) {
198                let mut errs: Vec<f64> = samples
199                    .iter()
200                    .map(|(uv, p)| norm(sub(eval(d, &cand, uv[0], uv[1]), *p)))
201                    .filter(|e| e.is_finite())
202                    .collect();
203                if errs.is_empty() {
204                    continue;
205                }
206                errs.sort_by(|a, b| a.total_cmp(b));
207                let med = errs.get(errs.len() / 2).copied().unwrap_or(f64::INFINITY);
208                if best.is_none_or(|(e, _)| med < e) {
209                    best = Some((med, di));
210                }
211            }
212        }
213        if let Some((_, di)) = best {
214            if brep.surfaces.get(di).map(|d| d.id) != tw.twin_of {
215                changes.push((ti, di));
216            }
217        }
218    }
219    for &(ti, di) in &changes {
220        let donor = brep.surfaces.get(di).cloned();
221        if let (Some(d), Some(tw)) = (donor, brep.surfaces.get_mut(ti)) {
222            tw.kind = d.kind;
223            tw.kind_name = d.kind_name.clone();
224            tw.orient = d.orient;
225            tw.frame = d.frame;
226            tw.params = d.params;
227            tw.freeform = d.freeform.clone();
228            tw.twin_of = Some(d.id);
229        }
230    }
231    changes.len()
232}
233
234/// A loop point: parameters on the face and the shared 3D point (NaN when
235/// the edge is not snapped).
236#[derive(Clone, Copy)]
237struct LoopPt {
238    uv: [f64; 2],
239    xyz: P3,
240}
241
242/// One 3D polyline per edge, evaluated on its most exact face (when both
243/// sides agree to `agree`), and the B-rep vertices unified across the
244/// contours (union-find over the loop successors).
245fn edge_points(
246    brep: &Brep,
247    fixed: &HashMap<i64, Conv>,
248    by_id: &HashMap<i64, usize>,
249    agree: f64,
250) -> HashMap<i64, Vec<P3>> {
251    let mut out: HashMap<i64, Vec<P3>> = HashMap::new();
252    let mut rank: HashMap<i64, u8> = HashMap::new();
253    for c in &brep.curves {
254        let sides: Vec<(usize, &Surface)> = (0..2)
255            .filter_map(|sd| {
256                by_id
257                    .get(&c.face(sd))
258                    .and_then(|&i| brep.surfaces.get(i))
259                    .map(|s| (sd, s))
260            })
261            .collect();
262        let Some(&(sd, s)) = sides.iter().min_by_key(|(sd, s)| (s.kind.rank(), *sd)) else {
263            continue;
264        };
265        let Some(conv) = fixed.get(&s.id) else {
266            continue;
267        };
268        let pts: Vec<P3> = c
269            .pts
270            .iter()
271            .map(|r| {
272                if sd == 0 {
273                    eval(s, conv, r[0], r[1])
274                } else {
275                    eval(s, conv, r[2], r[3])
276                }
277            })
278            .collect();
279        if sides.len() == 2 {
280            let Some(&(osd, os)) = sides.get(1 - sd) else {
281                continue;
282            };
283            if let Some(oconv) = fixed.get(&os.id) {
284                let mut disagree = false;
285                for (r, p) in c.pts.iter().zip(&pts) {
286                    let q = if osd == 0 {
287                        eval(os, oconv, r[0], r[1])
288                    } else {
289                        eval(os, oconv, r[2], r[3])
290                    };
291                    let d = norm(sub(*p, q));
292                    if d.is_nan() || d > agree {
293                        disagree = true;
294                        break;
295                    }
296                }
297                if disagree {
298                    continue;
299                }
300            }
301        }
302        rank.insert(c.id, s.kind.rank());
303        out.insert(c.id, pts);
304    }
305    // union-find over edge ends that meet in a contour
306    let cmap: HashMap<i64, &Curve> = brep.curves.iter().map(|c| (c.id, c)).collect();
307    let mut parent: HashMap<(i64, bool), (i64, bool)> = HashMap::new();
308    fn find(parent: &mut HashMap<(i64, bool), (i64, bool)>, mut x: (i64, bool)) -> (i64, bool) {
309        let mut path = Vec::new();
310        while let Some(&p) = parent.get(&x) {
311            if p == x {
312                break;
313            }
314            path.push(x);
315            x = p;
316        }
317        for q in path {
318            parent.insert(q, x);
319        }
320        x
321    }
322    for s in &brep.surfaces {
323        for &(start, _) in &s.contours {
324            let mut cid = start;
325            let mut prev: Option<(i64, bool)> = None;
326            let mut first: Option<(i64, bool)> = None;
327            for _ in 0..MAX_LOOP {
328                let Some(c) = cmap.get(&cid) else { break };
329                let side = if c.faces[0] == s.id {
330                    0
331                } else if c.faces[1] == s.id {
332                    1
333                } else {
334                    break;
335                };
336                if !out.contains_key(&cid) {
337                    break;
338                }
339                // (edge, end): false = first sample, true = last sample
340                let (head, tail) = if c.dir_on(side) == -1 {
341                    ((cid, true), (cid, false))
342                } else {
343                    ((cid, false), (cid, true))
344                };
345                parent.entry(head).or_insert(head);
346                parent.entry(tail).or_insert(tail);
347                if let Some(p) = prev {
348                    let (ra, rb) = (find(&mut parent, p), find(&mut parent, head));
349                    parent.insert(ra, rb);
350                }
351                first.get_or_insert(head);
352                prev = Some(tail);
353                cid = c.next_on(side);
354                if cid == start {
355                    if let (Some(p), Some(f)) = (prev, first) {
356                        let (ra, rb) = (find(&mut parent, p), find(&mut parent, f));
357                        parent.insert(ra, rb);
358                    }
359                    break;
360                }
361            }
362        }
363    }
364    let keys: Vec<(i64, bool)> = parent.keys().copied().collect();
365    let mut groups: HashMap<(i64, bool), Vec<(i64, bool)>> = HashMap::new();
366    for k in keys {
367        let r = find(&mut parent, k);
368        groups.entry(r).or_default().push(k);
369    }
370    for members in groups.values() {
371        let best = members
372            .iter()
373            .filter_map(|&(cid, end)| {
374                let pts = out.get(&cid)?;
375                let p = if end { *pts.last()? } else { *pts.first()? };
376                p.iter().all(|x| x.is_finite()).then_some((
377                    rank.get(&cid).copied().unwrap_or(9),
378                    cid,
379                    p,
380                ))
381            })
382            .min_by(|a, b| (a.0, a.1).cmp(&(b.0, b.1)));
383        let Some((_, _, p)) = best else { continue };
384        for &(cid, end) in members {
385            if let Some(pts) = out.get_mut(&cid) {
386                if end {
387                    if let Some(l) = pts.last_mut() {
388                        *l = p;
389                    }
390                } else if let Some(f) = pts.first_mut() {
391                    *f = p;
392                }
393            }
394        }
395    }
396    out
397}
398
399/// The contours of a face as closed point lists in its parameter plane.
400fn loops(
401    s: &Surface,
402    curves: &HashMap<i64, &Curve>,
403    e3: &HashMap<i64, Vec<P3>>,
404) -> Vec<(Vec<LoopPt>, i64)> {
405    let mut out = Vec::new();
406    for &(start, trv) in &s.contours {
407        let mut pts: Vec<LoopPt> = Vec::new();
408        let mut cid = start;
409        let mut ok = true;
410        for _ in 0..MAX_LOOP {
411            let Some(c) = curves.get(&cid) else {
412                ok = false;
413                break;
414            };
415            let side = if c.faces[0] == s.id {
416                0
417            } else if c.faces[1] == s.id {
418                1
419            } else {
420                ok = false;
421                break;
422            };
423            let snapped = e3.get(&cid);
424            let mut seg: Vec<LoopPt> = c
425                .pts
426                .iter()
427                .enumerate()
428                .map(|(k, r)| LoopPt {
429                    uv: if side == 0 {
430                        [r[0], r[1]]
431                    } else {
432                        [r[2], r[3]]
433                    },
434                    xyz: snapped
435                        .and_then(|v| v.get(k))
436                        .copied()
437                        .unwrap_or([f64::NAN; 3]),
438                })
439                .filter(|p| p.uv[0].is_finite() && p.uv[1].is_finite())
440                .collect();
441            if c.dir_on(side) == -1 {
442                seg.reverse();
443            }
444            pts.extend(seg);
445            cid = c.next_on(side);
446            if cid == start {
447                break;
448            }
449        }
450        if !ok || pts.len() < 3 {
451            continue;
452        }
453        let mut dedup: Vec<LoopPt> = Vec::with_capacity(pts.len());
454        for p in pts {
455            if dedup
456                .last()
457                .is_none_or(|q| (q.uv[0] - p.uv[0]).hypot(q.uv[1] - p.uv[1]) > 1e-9)
458            {
459                dedup.push(p);
460            }
461        }
462        if dedup.len() > 1 {
463            let closed = match (dedup.first(), dedup.last()) {
464                (Some(f), Some(l)) => (f.uv[0] - l.uv[0]).hypot(f.uv[1] - l.uv[1]) < 1e-9,
465                _ => false,
466            };
467            if closed {
468                dedup.pop();
469            }
470        }
471        if dedup.len() >= 3 {
472            out.push((dedup, trv));
473        }
474    }
475    out
476}
477
478fn area2(poly: &[[f64; 2]], scale: [f64; 2]) -> f64 {
479    let n = poly.len();
480    let mut s = 0.0;
481    for i in 0..n {
482        let a = poly.get(i).copied().unwrap_or_default();
483        let b = poly.get((i + 1) % n).copied().unwrap_or_default();
484        s += a[0] * scale[0] * b[1] * scale[1] - b[0] * scale[0] * a[1] * scale[1];
485    }
486    0.5 * s
487}
488
489fn point_in_poly(p: [f64; 2], poly: &[[f64; 2]]) -> bool {
490    let n = poly.len();
491    let mut inside = false;
492    for i in 0..n {
493        let a = poly.get(i).copied().unwrap_or_default();
494        let b = poly.get((i + 1) % n).copied().unwrap_or_default();
495        if (a[1] > p[1]) != (b[1] > p[1]) {
496            let x = a[0] + (p[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]);
497            if x > p[0] {
498                inside = !inside;
499            }
500        }
501    }
502    inside
503}
504
505/// The coarse mesh: per-face vertices (not yet welded) with their analytic
506/// normals, and triangles oriented outwards.
507pub struct Coarse {
508    pub points: Vec<P3>,
509    pub normals: Vec<P3>,
510    pub triangles: Vec<[u32; 3]>,
511}
512
513/// Tessellates every face of a solid body.
514pub fn tessellate(brep: &Brep, fixed: &HashMap<i64, Conv>) -> Result<Coarse> {
515    let curves: HashMap<i64, &Curve> = brep.curves.iter().map(|c| (c.id, c)).collect();
516    let by_id: HashMap<i64, usize> = brep
517        .surfaces
518        .iter()
519        .enumerate()
520        .map(|(i, s)| (s.id, i))
521        .collect();
522    // model size from a first pass over the edges, then the shared polylines
523    let rough = edge_points(brep, fixed, &by_id, f64::INFINITY);
524    let mut lo = [f64::INFINITY; 3];
525    let mut hi = [f64::NEG_INFINITY; 3];
526    for p in rough.values().flatten() {
527        if p.iter().all(|x| x.is_finite()) {
528            grow(&mut lo, &mut hi, *p);
529        }
530    }
531    let size = if lo[0].is_finite() {
532        norm(sub(hi, lo)).max(1e-9)
533    } else {
534        1.0
535    };
536    let e3 = edge_points(brep, fixed, &by_id, SNAP_REL * size);
537    let mut out = Coarse {
538        points: Vec::new(),
539        normals: Vec::new(),
540        triangles: Vec::new(),
541    };
542    for s in &brep.surfaces {
543        if s.kind == Kind::Other {
544            continue;
545        }
546        let conv = fixed
547            .get(&s.id)
548            .copied()
549            .or_else(|| candidates(s.kind).first().copied())
550            .unwrap_or(Conv::IDENTITY);
551        let polys = loops(s, &curves, &e3);
552        if polys.is_empty() {
553            continue;
554        }
555        // metric parameter plane: angles scaled by their radius
556        let scale = match s.kind {
557            Kind::Cylinder => [s.params[0].abs().max(1e-9), 1.0],
558            Kind::Torus => [
559                (s.params[0].abs() + s.params[1].abs()).max(1e-9),
560                s.params[1].abs().max(1e-9),
561            ],
562            Kind::Cone => [(s.params[0].tan().abs() * 0.5).max(1e-9), 1.0],
563            _ => [1.0, 1.0],
564        };
565        let uv_of = |p: &Vec<LoopPt>| -> Vec<[f64; 2]> { p.iter().map(|q| q.uv).collect() };
566        let mut outers: Vec<&Vec<LoopPt>> = polys
567            .iter()
568            .filter(|(_, t)| *t == 1)
569            .map(|(p, _)| p)
570            .collect();
571        let mut holes: Vec<&Vec<LoopPt>> = polys
572            .iter()
573            .filter(|(_, t)| *t != 1)
574            .map(|(p, _)| p)
575            .collect();
576        if outers.is_empty() {
577            let Some((k, _)) = polys.iter().enumerate().max_by(|a, b| {
578                area2(&uv_of(&a.1 .0), scale)
579                    .abs()
580                    .total_cmp(&area2(&uv_of(&b.1 .0), scale).abs())
581            }) else {
582                continue;
583            };
584            outers = polys.get(k).map(|(p, _)| vec![p]).unwrap_or_default();
585            holes = polys
586                .iter()
587                .enumerate()
588                .filter(|(i, _)| *i != k)
589                .map(|(_, (p, _))| p)
590                .collect();
591        }
592        let flip = s.orient == -1;
593        for outer in outers {
594            let mut ring: Vec<LoopPt> = outer.clone();
595            if area2(&uv_of(&ring), scale) < 0.0 {
596                ring.reverse();
597            }
598            let ring_uv = uv_of(&ring);
599            let mut coords: Vec<f64> = Vec::new();
600            let mut pts: Vec<LoopPt> = Vec::new();
601            let mut hole_starts: Vec<usize> = Vec::new();
602            for p in &ring {
603                coords.extend([p.uv[0] * scale[0], p.uv[1] * scale[1]]);
604                pts.push(*p);
605            }
606            for h in &holes {
607                let Some(first) = h.first() else { continue };
608                if !point_in_poly(first.uv, &ring_uv) {
609                    continue;
610                }
611                let mut hr: Vec<LoopPt> = (*h).clone();
612                if area2(&uv_of(&hr), scale) > 0.0 {
613                    hr.reverse();
614                }
615                hole_starts.push(pts.len());
616                for p in &hr {
617                    coords.extend([p.uv[0] * scale[0], p.uv[1] * scale[1]]);
618                    pts.push(*p);
619                }
620            }
621            let pairs: Vec<[f64; 2]> = coords
622                .chunks_exact(2)
623                .filter_map(|c| <[f64; 2]>::try_from(c).ok())
624                .collect();
625            let tris: Vec<usize> = crate::mesh::earcut::earcut(&pairs, &hole_starts)
626                .into_iter()
627                .flatten()
628                .collect();
629            if tris.len() < 3 {
630                continue;
631            }
632            let tris = delaunay_flips(tris, &coords);
633            let signed: f64 = tris
634                .chunks(3)
635                .map(|t| {
636                    let g = |k: usize| {
637                        pts.get(t.get(k).copied().unwrap_or(usize::MAX))
638                            .map(|p| p.uv)
639                            .unwrap_or_default()
640                    };
641                    area2(&[g(0), g(1), g(2)], scale)
642                })
643                .sum();
644            let ccw = signed >= 0.0;
645            let base = u32::try_from(out.points.len()).map_err(|_| anyhow!("too many vertices"))?;
646            for p in &pts {
647                let [u, v] = p.uv;
648                let xyz = if p.xyz.iter().all(|x| x.is_finite()) {
649                    p.xyz
650                } else {
651                    eval(s, &conv, u, v)
652                };
653                let du = sub(eval(s, &conv, u + H, v), eval(s, &conv, u - H, v));
654                let dv = sub(eval(s, &conv, u, v + H), eval(s, &conv, u, v - H));
655                let mut n = cross(du, dv);
656                let l = norm(n);
657                n = if l > 1e-18 && n.iter().all(|x| x.is_finite()) {
658                    n.map(|x| x / l)
659                } else {
660                    [0.0; 3]
661                };
662                if flip {
663                    n = n.map(|x| -x);
664                }
665                if !xyz.iter().all(|x| x.is_finite()) {
666                    bail!("face {} evaluates to NaN", s.id);
667                }
668                out.points.push(xyz);
669                out.normals.push(n);
670            }
671            for t in tris.chunks(3) {
672                let (Some(&a), Some(&b), Some(&c)) = (t.first(), t.get(1), t.get(2)) else {
673                    continue;
674                };
675                let (a, b, c) = (u32::try_from(a)?, u32::try_from(b)?, u32::try_from(c)?);
676                let tri = if ccw != flip {
677                    [base + a, base + b, base + c]
678                } else {
679                    [base + a, base + c, base + b]
680                };
681                out.triangles.push(tri);
682                if out.triangles.len() > MAX_TRIANGLES {
683                    bail!("The model has more than {MAX_TRIANGLES} triangles – more than the preview shows.");
684                }
685            }
686        }
687    }
688    if out.triangles.is_empty() {
689        bail!("The Creo part contains no faces that could be tessellated.");
690    }
691    Ok(out)
692}
693
694/// Lawson flips towards a Delaunay triangulation in the metric parameter
695/// plane: ear clipping leaves long thin triangles that, mapped to a curved
696/// face, cut through the material. Boundary edges are never flipped.
697fn delaunay_flips(tris: Vec<usize>, coords: &[f64]) -> Vec<usize> {
698    let n = tris.len() / 3;
699    let mut t: Vec<[usize; 3]> = tris
700        .chunks_exact(3)
701        .filter_map(|c| <[usize; 3]>::try_from(c).ok())
702        .collect();
703    let pt = |i: usize| -> [f64; 2] {
704        [
705            coords.get(2 * i).copied().unwrap_or(0.0),
706            coords.get(2 * i + 1).copied().unwrap_or(0.0),
707        ]
708    };
709    let in_circle = |a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]| -> bool {
710        let (ax, ay) = (a[0] - d[0], a[1] - d[1]);
711        let (bx, by) = (b[0] - d[0], b[1] - d[1]);
712        let (cx, cy) = (c[0] - d[0], c[1] - d[1]);
713        let det = (ax * ax + ay * ay) * (bx * cy - cx * by)
714            - (bx * bx + by * by) * (ax * cy - cx * ay)
715            + (cx * cx + cy * cy) * (ax * by - bx * ay);
716        let orient = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
717        if orient > 0.0 {
718            det > 1e-12
719        } else {
720            det < -1e-12
721        }
722    };
723    // Lawson passes; each pass flips many edges, so a few dozen suffice.
724    // A BTreeMap keeps the result the same from run to run.
725    for _ in 0..n.clamp(1, 64) {
726        let mut edges: BTreeMap<(usize, usize), Vec<usize>> = BTreeMap::new();
727        for (ti, &[x, y, z]) in t.iter().enumerate() {
728            for (a, b) in [(x, y), (y, z), (z, x)] {
729                edges.entry((a.min(b), a.max(b))).or_default().push(ti);
730            }
731        }
732        let mut flipped = false;
733        let mut done: HashSet<usize> = HashSet::new();
734        for (&(a, b), owners) in &edges {
735            let [ti, tj] = owners.as_slice() else {
736                continue;
737            };
738            if done.contains(ti) || done.contains(tj) {
739                continue;
740            }
741            let (Some(&x), Some(&y)) = (t.get(*ti), t.get(*tj)) else {
742                continue;
743            };
744            let Some(&c) = x.iter().find(|&&v| v != a && v != b) else {
745                continue;
746            };
747            let Some(&d) = y.iter().find(|&&v| v != a && v != b) else {
748                continue;
749            };
750            if !in_circle(pt(a), pt(b), pt(c), pt(d)) {
751                continue;
752            }
753            // the quad must be convex: both new triangles keep the orientation
754            let orient = |p: [f64; 2], q: [f64; 2], r: [f64; 2]| {
755                (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0])
756            };
757            let o_old = orient(pt(x[0]), pt(x[1]), pt(x[2]));
758            let new1 = [c, d, b];
759            let new2 = [d, c, a];
760            let (n1, n2) = (orient(pt(c), pt(d), pt(b)), orient(pt(d), pt(c), pt(a)));
761            if n1 * o_old <= 0.0 || n2 * o_old <= 0.0 {
762                continue;
763            }
764            if let Some(v) = t.get_mut(*ti) {
765                *v = new1;
766            }
767            if let Some(v) = t.get_mut(*tj) {
768                *v = new2;
769            }
770            done.insert(*ti);
771            done.insert(*tj);
772            flipped = true;
773        }
774        if !flipped {
775            break;
776        }
777    }
778    t.into_iter().flatten().collect()
779}
780
781/// Welds the per-face vertices, refines along the normals and builds the
782/// preview mesh (model units are kept).
783pub fn build(coarse: &Coarse) -> Result<Mesh> {
784    let mut lo = [f64::INFINITY; 3];
785    let mut hi = [f64::NEG_INFINITY; 3];
786    for p in &coarse.points {
787        grow(&mut lo, &mut hi, *p);
788    }
789    let size = norm(sub(hi, lo)).max(1e-9);
790    let weld = WELD_REL * size;
791    let mut points: Vec<P3> = Vec::new();
792    let mut welded: HashMap<[i64; 3], u32> = HashMap::new();
793    let mut index: Vec<u32> = Vec::with_capacity(coarse.points.len());
794    for p in &coarse.points {
795        let grid = p.map(|c| (c / weld).round() as i64);
796        let next = u32::try_from(points.len()).map_err(|_| anyhow!("too many vertices"))?;
797        index.push(*welded.entry(grid).or_insert_with(|| {
798            points.push(*p);
799            next
800        }));
801    }
802    let mut triangles: Vec<[u32; 3]> = Vec::with_capacity(coarse.triangles.len());
803    let mut normal_triangles: Vec<[u32; 3]> = Vec::with_capacity(coarse.triangles.len());
804    for t in &coarse.triangles {
805        let w = t.map(|k| index.get(k as usize).copied().unwrap_or(u32::MAX));
806        if w[0] == w[1] || w[1] == w[2] || w[0] == w[2] || w.contains(&u32::MAX) {
807            continue;
808        }
809        triangles.push(w);
810        normal_triangles.push(*t);
811    }
812    let volume: f64 = triangles
813        .iter()
814        .map(|t| {
815            let g = |k: u32| points.get(k as usize).copied().unwrap_or_default();
816            dot(g(t[0]), cross(g(t[1]), g(t[2])))
817        })
818        .sum::<f64>()
819        / 6.0;
820    let mut normals = coarse.normals.clone();
821    if volume < 0.0 {
822        for t in &mut triangles {
823            t.swap(1, 2);
824        }
825        for t in &mut normal_triangles {
826            t.swap(1, 2);
827        }
828        for n in &mut normals {
829            *n = n.map(|x| -x);
830        }
831    }
832    let level = crate::mesh::pn::refine_level(triangles.len());
833    let (points, triangles) = if level > 1 {
834        crate::mesh::pn::refine(&points, &triangles, &normals, &normal_triangles, level)
835    } else {
836        (points, triangles)
837    };
838    if triangles.len() > MAX_TRIANGLES {
839        bail!("The model has more than {MAX_TRIANGLES} triangles – more than the preview shows.");
840    }
841    let mut mesh = Mesh {
842        parts: 1,
843        ..Mesh::default()
844    };
845    for p in &points {
846        mesh.vertex(*p);
847    }
848    mesh.indices.extend(triangles.iter().flatten());
849    Ok(mesh)
850}