Skip to main content

brep_reconstruction/import_session/
nurbs_face.rs

1//! A region the reconstruction left as triangles, rebuilt as ONE NURBS face
2//! of the solid — exactly bounded by the edges its neighbours already have.
3//!
4//! The fitted surface must contain the region's boundary EXACTLY, or the
5//! shell's edges would not lie on it and the solid could not validate. The
6//! region's boundary in the built body is a chain of straight edges between
7//! mesh vertices. Split at four corners, the four sides are polylines; a
8//! cubic B-spline curve with a triple knot at every polyline vertex IS that
9//! polyline, parametrized linearly on each segment. So the patch is a bicubic
10//! tensor surface on the union of the sides' knots (triple interior knots),
11//! whose four boundary rows of control points are the four sides: every
12//! boundary edge of the region lies on the patch's domain edge, and its pcurve
13//! is a straight parameter line. The interior control points are a smoothed
14//! least-squares fit to the region's vertices, parametrized by a Tutte
15//! (barycentric) embedding of the region on the unit square — valid for any
16//! disk-shaped region, so the parametrization never folds.
17//!
18//! The patch replaces the region's facet faces in the solid; its interior
19//! edges and vertices go with them. Nothing else in the solid changes. The
20//! result must pass [`BrepSolid::validate`] like every other body, and is
21//! refused with the reason otherwise.
22
23use brep_kernel::{BrepSolid, CoedgeRecord, FaceRecord, LoopRecord, NurbsSurface, Vec3, Vec4};
24use serde::{Deserialize, Serialize};
25use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
26
27/// One region to rebuild as a NURBS face.
28#[derive(Clone, Debug, Deserialize, Serialize)]
29pub struct NurbsFaceChoice {
30    /// The region's source triangles (indices into the welded mesh).
31    pub triangles: Vec<u32>,
32    /// Largest vertex distance the patch may have from the mesh, mm.
33    pub tolerance: f64,
34}
35
36/// What became of one [`NurbsFaceChoice`].
37#[derive(Clone, Debug, Deserialize, Serialize)]
38pub struct NurbsFaceOutcome {
39    pub applied: bool,
40    /// The patch's largest vertex deviation (applied), mm.
41    pub max_deviation: Option<f64>,
42    /// Control net size (applied).
43    pub control_points: Option<[usize; 2]>,
44    /// Why it was refused, in words.
45    pub reason: Option<String>,
46}
47
48impl NurbsFaceOutcome {
49    fn refused(reason: impl Into<String>) -> Self {
50        Self { applied: false, max_deviation: None, control_points: None, reason: Some(reason.into()) }
51    }
52}
53
54fn v3(p: &[f64]) -> Vec3 {
55    Vec3::new(p[0], p[1], p[2])
56}
57
58/// Distance from `p` to triangle `t`.
59fn point_triangle_distance(p: Vec3, t: [Vec3; 3]) -> f64 {
60    let [a, b, c] = t;
61    let n = b.sub(a).cross(c.sub(a));
62    let area2 = n.length();
63    if area2 > 0.0 {
64        let n = n.scale(1.0 / area2);
65        let q = p.sub(n.scale(p.sub(a).dot(n)));
66        let inside = [(a, b), (b, c), (c, a)].iter().all(|&(u, v)| v.sub(u).cross(q.sub(u)).dot(n) >= -1e-12 * area2);
67        if inside {
68            return p.sub(q).length();
69        }
70    }
71    [(a, b), (b, c), (c, a)].iter().map(|&(u, v)| p.distance_to_segment(u, v, 0.0)).fold(f64::INFINITY, f64::min)
72}
73
74/// A coedge's (start, end) solid vertex ids.
75fn coedge_ends(solid: &BrepSolid, coedge: &CoedgeRecord) -> Option<(u64, u64)> {
76    let edge = solid.edges.iter().find(|e| e.id == coedge.edge_id)?;
77    Some(if coedge.forward { (edge.start_vertex_id, edge.end_vertex_id) } else { (edge.end_vertex_id, edge.start_vertex_id) })
78}
79
80/// Rebuild each choice's region as one NURBS face of `solid` (in place),
81/// returning one outcome per choice. A refused choice leaves the solid as it
82/// was.
83pub fn apply_nurbs_faces(
84    solid: &mut BrepSolid,
85    positions: &[f64],
86    indices: &[u32],
87    choices: &[NurbsFaceChoice],
88) -> Vec<NurbsFaceOutcome> {
89    choices
90        .iter()
91        .map(|choice| {
92            let mut trial = solid.clone();
93            match replace_region(&mut trial, positions, indices, choice) {
94                Ok(outcome) => {
95                    let issues = trial.validate();
96                    if issues.is_empty() {
97                        *solid = trial;
98                        outcome
99                    } else {
100                        NurbsFaceOutcome::refused(format!(
101                            "the solid with the NURBS face does not validate: {}",
102                            issues.iter().take(2).map(|issue| format!("{issue:?}")).collect::<Vec<_>>().join("; ")
103                        ))
104                    }
105                }
106                Err(reason) => NurbsFaceOutcome::refused(reason),
107            }
108        })
109        .collect()
110}
111
112fn replace_region(
113    solid: &mut BrepSolid,
114    positions: &[f64],
115    indices: &[u32],
116    choice: &NurbsFaceChoice,
117) -> Result<NurbsFaceOutcome, String> {
118    if !(choice.tolerance.is_finite() && choice.tolerance > 0.0) {
119        return Err("the tolerance must be a positive distance".into());
120    }
121    let vertex_count = positions.len() / 3;
122    let triangles: Vec<[u32; 3]> = choice
123        .triangles
124        .iter()
125        .map(|&t| {
126            let t = t as usize;
127            if 3 * t + 2 >= indices.len() {
128                return Err(format!("triangle {t} is outside the mesh"));
129            }
130            let tri = [indices[3 * t], indices[3 * t + 1], indices[3 * t + 2]];
131            if tri.iter().any(|&v| v as usize >= vertex_count) {
132                return Err(format!("triangle {t} names a vertex outside the mesh"));
133            }
134            Ok(tri)
135        })
136        .collect::<Result<_, _>>()?;
137    if triangles.is_empty() {
138        return Err("the region has no triangles".into());
139    }
140    let tri_points: Vec<[Vec3; 3]> = triangles.iter().map(|t| t.map(|v| v3(&positions[3 * v as usize..]))).collect();
141    let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
142    let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
143    for p in positions.chunks_exact(3) {
144        low = Vec3::new(low.x.min(p[0]), low.y.min(p[1]), low.z.min(p[2]));
145        high = Vec3::new(high.x.max(p[0]), high.y.max(p[1]), high.z.max(p[2]));
146    }
147    let radius = (high.sub(low).length() * 1e-9).max(1e-12);
148    // On the region: within a weld radius of one of its triangles.
149    let on_region = |p: Vec3| tri_points.iter().any(|t| point_triangle_distance(p, *t) <= radius * 1e3);
150
151    // The region's faces in the body: planar facets lying on its triangles
152    // (the builder may have cut a triangle where it meets a fitted wall, so
153    // a facet need not be a source triangle — but it lies on one).
154    if solid.shells.len() != 1 {
155        return Err(format!("the body has {} shells; a NURBS face is placed on single-shell bodies", solid.shells.len()));
156    }
157    let shell = &solid.shells[0];
158    let point_of = |id: u64| solid.vertices.iter().find(|v| v.id == id).map(|v| v.point);
159    let region_faces: Vec<usize> = shell
160        .faces
161        .iter()
162        .enumerate()
163        .filter(|(_, face)| {
164            if face.surface.degree_u != 1 || face.surface.degree_v != 1 {
165                return false;
166            }
167            let points: Vec<Vec3> = face
168                .loops
169                .iter()
170                .flat_map(|l| &l.coedges)
171                .filter_map(|c| coedge_ends(solid, c))
172                .filter_map(|(a, _)| point_of(a))
173                .collect();
174            if points.is_empty() || !points.iter().all(|&p| on_region(p)) {
175                return false;
176            }
177            let centre = points.iter().fold(Vec3::new(0.0, 0.0, 0.0), |acc, &p| acc.add(p)).scale(1.0 / points.len() as f64);
178            on_region(centre)
179        })
180        .map(|(index, _)| index)
181        .collect();
182    if region_faces.is_empty() {
183        return Err("the region's triangles are not faces of the built body (it was fitted, or merged with a neighbour)".into());
184    }
185    let region_face_set: HashSet<usize> = region_faces.iter().copied().collect();
186    // The region's vertices and edges in the body.
187    let mut region_edges: HashMap<u64, (u64, u64)> = HashMap::new();
188    for &i in &region_faces {
189        for coedge in shell.faces[i].loops.iter().flat_map(|l| &l.coedges) {
190            let (a, b) = coedge_ends(solid, coedge).ok_or("a coedge names a missing edge")?;
191            region_edges.insert(coedge.edge_id, (a.min(b), a.max(b)));
192        }
193    }
194    let region_vertices: BTreeSet<u64> = region_edges.values().flat_map(|&(a, b)| [a, b]).collect();
195    // Disk check: V - E + F == 1.
196    if region_vertices.len() as i64 - region_edges.len() as i64 + region_faces.len() as i64 != 1 {
197        return Err("the region is not a single disk-shaped patch (it has a hole or a handle); one NURBS face needs one boundary loop".into());
198    }
199    // Edge use: an edge used once by the region's faces is boundary.
200    let mut uses: HashMap<u64, usize> = HashMap::new();
201    for &i in &region_faces {
202        for coedge in shell.faces[i].loops.iter().flat_map(|l| &l.coedges) {
203            *uses.entry(coedge.edge_id).or_default() += 1;
204        }
205    }
206    // Boundary coedges, chained by start vertex.
207    let mut next: HashMap<u64, CoedgeRecord> = HashMap::new();
208    for &i in &region_faces {
209        for coedge in shell.faces[i].loops.iter().flat_map(|l| &l.coedges) {
210            if uses[&coedge.edge_id] == 1 {
211                let (a, _) = coedge_ends(solid, coedge).ok_or("a boundary coedge names a missing edge")?;
212                if next.insert(a, coedge.clone()).is_some() {
213                    return Err("the region's boundary touches itself at a vertex; one NURBS face needs a simple boundary loop".into());
214                }
215            }
216        }
217    }
218    let Some(&start) = next.keys().min() else { return Err("the region has no boundary (a closed surface)".into()) };
219    let mut loop_coedges = Vec::new();
220    let mut at = start;
221    loop {
222        let coedge = next.get(&at).ok_or("the region's boundary does not close")?.clone();
223        let (_, b) = coedge_ends(solid, &coedge).unwrap();
224        loop_coedges.push(coedge);
225        at = b;
226        if at == start {
227            break;
228        }
229        if loop_coedges.len() > next.len() {
230            return Err("the region's boundary does not close".into());
231        }
232    }
233    if loop_coedges.len() != next.len() {
234        return Err("the region's boundary is more than one loop (the region has a hole)".into());
235    }
236    if loop_coedges.len() < 4 {
237        return Err(format!("the region's boundary has {} edges; a four-sided patch needs at least four", loop_coedges.len()));
238    }
239    // Every boundary edge must be a straight line (a facet's edge is).
240    let edge_of = |id: u64| solid.edges.iter().find(|e| e.id == id).unwrap();
241    for coedge in &loop_coedges {
242        let edge = edge_of(coedge.edge_id);
243        let start = solid.vertices.iter().find(|v| v.id == edge.start_vertex_id).unwrap().point;
244        let end = solid.vertices.iter().find(|v| v.id == edge.end_vertex_id).unwrap().point;
245        let mid = edge.curve.evaluate(0.5 * (edge.t0 + edge.t1)).map_err(|e| format!("boundary edge: {e}"))?;
246        let chord = start.add(end).scale(0.5);
247        if mid.sub(chord).length() > radius.max(1e-9 * end.sub(start).length()) * 10.0 {
248            return Err("a boundary edge of the region is curved; the NURBS patch is bounded by straight facet edges only".into());
249        }
250    }
251    // Boundary vertex positions, in loop order.
252    let loop_vertices: Vec<u64> = loop_coedges.iter().map(|c| coedge_ends(solid, c).unwrap().0).collect();
253    let boundary_points: Vec<Vec3> = loop_vertices.iter().map(|&id| point_of(id).unwrap()).collect();
254    let n = boundary_points.len();
255
256    // Corners: the four boundary vertices nearest the corners of the
257    // region's principal-axis box, in loop order.
258    let region_points: Vec<Vec3> = region_vertices.iter().filter_map(|&v| point_of(v)).collect();
259    let (centroid, axes) = super::principal_frame_kernel(&region_points);
260    let project = |p: Vec3| {
261        let d = p.sub(centroid);
262        [d.dot(axes[0]), d.dot(axes[1])]
263    };
264    let uv: Vec<[f64; 2]> = boundary_points.iter().map(|&p| project(p)).collect();
265    let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
266    for p in &uv {
267        for k in 0..2 {
268            lo[k] = lo[k].min(p[k]);
269            hi[k] = hi[k].max(p[k]);
270        }
271    }
272    let box_corners = [[lo[0], lo[1]], [hi[0], lo[1]], [hi[0], hi[1]], [lo[0], hi[1]]];
273    let mut corners: Vec<usize> = Vec::new();
274    for corner in box_corners {
275        let best = (0..n)
276            .filter(|i| !corners.contains(i))
277            .min_by(|&a, &b| {
278                let da = (uv[a][0] - corner[0]).powi(2) + (uv[a][1] - corner[1]).powi(2);
279                let db = (uv[b][0] - corner[0]).powi(2) + (uv[b][1] - corner[1]).powi(2);
280                da.total_cmp(&db)
281            })
282            .unwrap();
283        corners.push(best);
284    }
285    corners.sort_unstable();
286    corners.dedup();
287    if corners.len() != 4 {
288        return Err("could not split the region's boundary into four sides".into());
289    }
290    // Sides as runs of boundary vertex indices (inclusive), loop order.
291    let side = |s: usize| -> Vec<usize> {
292        let (a, b) = (corners[s], corners[(s + 1) % 4]);
293        let mut run = vec![a];
294        let mut i = a;
295        while i != b {
296            i = (i + 1) % n;
297            run.push(i);
298        }
299        run
300    };
301    let sides: Vec<Vec<usize>> = (0..4).map(side).collect();
302    // Chord-length parameters along each side, 0..1.
303    let params = |run: &[usize]| -> Result<Vec<f64>, String> {
304        let mut t = vec![0.0];
305        for w in run.windows(2) {
306            t.push(t.last().unwrap() + boundary_points[w[1]].sub(boundary_points[w[0]]).length());
307        }
308        let total = *t.last().unwrap();
309        if total <= 0.0 {
310            return Err("a side of the region has no length".into());
311        }
312        Ok(t.into_iter().map(|x| x / total).collect())
313    };
314    // Side 0: (0,0)->(1,0); side 1: (1,0)->(1,1); side 2: (1,1)->(0,1);
315    // side 3: (0,1)->(0,0). Square-boundary position of every boundary vertex.
316    let p0 = params(&sides[0])?;
317    let p1 = params(&sides[1])?;
318    let p2 = params(&sides[2])?;
319    let p3 = params(&sides[3])?;
320    let mut square: HashMap<usize, [f64; 2]> = HashMap::new();
321    for (k, &i) in sides[0].iter().enumerate() {
322        square.insert(i, [p0[k], 0.0]);
323    }
324    for (k, &i) in sides[1].iter().enumerate() {
325        square.insert(i, [1.0, p1[k]]);
326    }
327    for (k, &i) in sides[2].iter().enumerate() {
328        square.insert(i, [1.0 - p2[k], 1.0]);
329    }
330    for (k, &i) in sides[3].iter().enumerate() {
331        square.insert(i, [0.0, 1.0 - p3[k]]);
332    }
333    // Knots: the union of both opposite sides' vertex parameters.
334    let merge = |a: &[f64], b: &[f64]| -> Vec<f64> {
335        let mut all: Vec<f64> = a.iter().chain(b).copied().collect();
336        all.sort_by(f64::total_cmp);
337        let mut out: Vec<f64> = Vec::new();
338        for x in all {
339            if out.last().is_none_or(|&last| x - last > 1e-9) {
340                out.push(x);
341            } else if x == 1.0 {
342                *out.last_mut().unwrap() = 1.0;
343            }
344        }
345        *out.first_mut().unwrap() = 0.0;
346        *out.last_mut().unwrap() = 1.0;
347        out
348    };
349    let u_breaks = merge(&p0, &p2.iter().map(|t| 1.0 - t).collect::<Vec<_>>());
350    let v_breaks = merge(&p1, &p3.iter().map(|t| 1.0 - t).collect::<Vec<_>>());
351    let cu = 3 * (u_breaks.len() - 1) + 1;
352    let cv = 3 * (v_breaks.len() - 1) + 1;
353    if cu * cv > 40_000 {
354        return Err(format!("the region's boundary needs a {cu} × {cv} control net; split the region"));
355    }
356    let knots = |breaks: &[f64]| {
357        let mut k = vec![0.0; 4];
358        for &b in &breaks[1..breaks.len() - 1] {
359            k.extend([b, b, b]);
360        }
361        k.extend([1.0; 4]);
362        k
363    };
364    let knots_u = knots(&u_breaks);
365    let knots_v = knots(&v_breaks);
366    // Control parameter of control index i (span s, offset r of 0..3).
367    let control_param = |breaks: &[f64], i: usize| {
368        let s = (i / 3).min(breaks.len() - 2);
369        let r = i - 3 * s;
370        breaks[s] + (breaks[s + 1] - breaks[s]) * r as f64 / 3.0
371    };
372    // A side polyline evaluated at its parameter t (linear per segment).
373    let along = |run: &[usize], t_side: &[f64], t: f64| -> Vec3 {
374        let k = t_side.partition_point(|&x| x <= t).clamp(1, run.len() - 1);
375        let (t0, t1) = (t_side[k - 1], t_side[k]);
376        let f = if t1 > t0 { ((t - t0) / (t1 - t0)).clamp(0.0, 1.0) } else { 0.0 };
377        boundary_points[run[k - 1]].scale(1.0 - f).add(boundary_points[run[k]].scale(f))
378    };
379    let mut net: Vec<Option<Vec3>> = vec![None; cu * cv];
380    let at = |i: usize, j: usize| i * cv + j;
381    for i in 0..cu {
382        let u = control_param(&u_breaks, i);
383        net[at(i, 0)] = Some(along(&sides[0], &p0, u));
384        net[at(i, cv - 1)] = Some(along(&sides[2], &p2, 1.0 - u));
385    }
386    for j in 0..cv {
387        let v = control_param(&v_breaks, j);
388        net[at(cu - 1, j)] = Some(along(&sides[1], &p1, v));
389        net[at(0, j)] = Some(along(&sides[3], &p3, 1.0 - v));
390    }
391
392    // Tutte embedding of the region's vertices on the square.
393    let boundary_mesh: HashMap<u64, [f64; 2]> = loop_vertices
394        .iter()
395        .enumerate()
396        .map(|(k, &id)| (id, square[&k]))
397        .collect();
398    let mut neighbours: BTreeMap<u64, BTreeSet<u64>> = BTreeMap::new();
399    for &(a, b) in region_edges.values() {
400        neighbours.entry(a).or_default().insert(b);
401        neighbours.entry(b).or_default().insert(a);
402    }
403    let mut param: HashMap<u64, [f64; 2]> = region_vertices.iter().map(|&v| (v, boundary_mesh.get(&v).copied().unwrap_or([0.5, 0.5]))).collect();
404    for &v in &region_vertices {
405        if !boundary_mesh.contains_key(&v) && !neighbours.contains_key(&v) {
406            return Err("an isolated vertex in the region".into());
407        }
408    }
409    let interior: Vec<u64> = region_vertices.iter().copied().filter(|v| !boundary_mesh.contains_key(v)).collect();
410    for _ in 0..20_000 {
411        let mut change = 0.0f64;
412        for &v in &interior {
413            let around = &neighbours[&v];
414            let mut sum = [0.0; 2];
415            for w in around {
416                let p = param[w];
417                sum[0] += p[0];
418                sum[1] += p[1];
419            }
420            let new = [sum[0] / around.len() as f64, sum[1] / around.len() as f64];
421            let old = param[&v];
422            let relaxed = [old[0] + 1.8 * (new[0] - old[0]), old[1] + 1.8 * (new[1] - old[1])];
423            change = change.max((relaxed[0] - old[0]).abs()).max((relaxed[1] - old[1]).abs());
424            param.insert(v, relaxed);
425        }
426        if change < 1e-12 {
427            break;
428        }
429    }
430
431    // Least squares for the free control points, smoothed; the smoothest
432    // weight that meets the tolerance is kept.
433    let free: Vec<usize> = (0..cu * cv).filter(|&k| net[k].is_none()).collect();
434    let free_index: HashMap<usize, usize> = free.iter().enumerate().map(|(k, &c)| (c, k)).collect();
435    let samples: Vec<(Vec3, [f64; 2])> = region_vertices.iter().map(|&v| (point_of(v).unwrap(), param[&v])).collect();
436    let rows: Vec<Vec<(usize, f64)>> = samples
437        .iter()
438        .map(|(_, uv)| {
439            let (su, bu) = super::basis(&knots_u, 3, cu, uv[0]);
440            let (sv, bv) = super::basis(&knots_v, 3, cv, uv[1]);
441            let mut row = Vec::with_capacity(16);
442            for (a, wu) in bu.iter().enumerate() {
443                for (b, wv) in bv.iter().enumerate() {
444                    if wu * wv != 0.0 {
445                        row.push((at(su - 3 + a, sv - 3 + b), wu * wv));
446                    }
447                }
448            }
449            row
450        })
451        .collect();
452    let trace = std::env::var("BREP_DEBUG_NURBS_FACE").is_ok();
453    if trace {
454        eprintln!("[nurbs-face] net {cu}x{cv} free {} samples {} boundary {n}", (0..cu * cv).filter(|&k| net[k].is_none()).count(), region_vertices.len());
455    }
456    // The Coons net of the four boundary rows, blended on the control
457    // points' own (Greville) parameters: the patch the smoothing relaxes to.
458    // Smoothing the displacement from it rather than the raw net keeps the
459    // boundary's kinks fading inward; smoothing raw positions by index pulled
460    // the first interior row straight across the staircase's reflex corners
461    // and folded the patch there (the domed block: 28 reversed normals, a
462    // second sheet 2.2e-3 mm from a boundary edge, which the importer's
463    // projector then landed on — 21 s to read the STEP back).
464    let pu: Vec<f64> = (0..cu).map(|i| control_param(&u_breaks, i)).collect();
465    let pv: Vec<f64> = (0..cv).map(|j| control_param(&v_breaks, j)).collect();
466    let coons: Vec<Vec3> = (0..cu * cv)
467        .map(|c| {
468            let (i, j) = (c / cv, c % cv);
469            if let Some(p) = net[c] {
470                return p;
471            }
472            let (u, v) = (pu[i], pv[j]);
473            let g = |ii: usize, jj: usize| net[at(ii, jj)].unwrap();
474            g(i, 0)
475                .scale(1.0 - v)
476                .add(g(i, cv - 1).scale(v))
477                .add(g(0, j).scale(1.0 - u))
478                .add(g(cu - 1, j).scale(u))
479                .sub(g(0, 0).scale((1.0 - u) * (1.0 - v)))
480                .sub(g(cu - 1, 0).scale(u * (1.0 - v)))
481                .sub(g(0, cv - 1).scale((1.0 - u) * v))
482                .sub(g(cu - 1, cv - 1).scale(u * v))
483        })
484        .collect();
485    let ladder_started = web_time::Instant::now();
486    let mut best: Option<(NurbsSurface, f64, f64)> = None;
487    let mut folds = 0usize;
488    // Each weight starts from the last one's displacements (the ladder only
489    // loosens the smoothing, so the next solution is near).
490    let mut warm = vec![Vec3::new(0.0, 0.0, 0.0); free.len()];
491    for weight in [1e0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8] {
492        let solved = if free.is_empty() {
493            Vec::new()
494        } else {
495            solve_net(&coons, &free, &free_index, &rows, &samples, cu, cv, &pu, &pv, weight, &warm)
496        };
497        warm.clone_from(&solved);
498        let mut full = coons.clone();
499        for (k, &c) in free.iter().enumerate() {
500            full[c] = coons[c].add(solved[k]);
501        }
502        let mut max = 0.0f64;
503        let mut sum = 0.0f64;
504        for ((point, _), row) in samples.iter().zip(&rows) {
505            let s = row.iter().fold(Vec3::new(0.0, 0.0, 0.0), |acc, &(c, w)| acc.add(full[c].scale(w)));
506            let d = s.sub(*point).length();
507            max = max.max(d);
508            sum += d * d;
509        }
510        let rms = (sum / samples.len() as f64).sqrt();
511        let control_points: Vec<Vec<Vec4>> = (0..cu).map(|i| (0..cv).map(|j| Vec4::from_point(full[at(i, j)], 1.0)).collect()).collect();
512        let surface = NurbsSurface::new(3, 3, knots_u.clone(), knots_v.clone(), control_points).map_err(|e| format!("NURBS surface: {e}"))?;
513        let folded = reversed_normals(&surface, &u_breaks, &v_breaks, &tri_points);
514        if trace {
515            eprintln!("[nurbs-face] weight {weight:e} max {max:.3e} rms {rms:.3e} reversed normals {folded} at {:.2}s", ladder_started.elapsed().as_secs_f64());
516        }
517        if folded > 0 {
518            folds += 1;
519            continue;
520        }
521        let done = max <= choice.tolerance;
522        if best.as_ref().is_none_or(|(_, m, _)| max < *m) || done {
523            best = Some((surface, max, rms));
524        }
525        if done {
526            break;
527        }
528    }
529    let Some((surface, max_deviation, _)) = best else {
530        return Err(format!("every NURBS face with this boundary folds over itself ({folds} tried); split the region or choose its corners differently"));
531    };
532    if max_deviation > choice.tolerance {
533        return Err(format!(
534            "the closest NURBS face with this boundary that does not fold is {max_deviation:.3e} mm from the mesh, over the {:.3e} mm tolerance",
535            choice.tolerance
536        ));
537    }
538
539    // The face: one loop of the region's boundary coedges, each with its
540    // straight parameter line along the square.
541    let mut next_id = 0u64;
542    for vertex in &solid.vertices {
543        next_id = next_id.max(vertex.id);
544    }
545    for edge in &solid.edges {
546        next_id = next_id.max(edge.id);
547    }
548    for face in solid.shells.iter().flat_map(|s| &s.faces) {
549        next_id = next_id.max(face.id);
550        for l in &face.loops {
551            next_id = next_id.max(l.id);
552            for c in &l.coedges {
553                next_id = next_id.max(c.id);
554            }
555        }
556    }
557    next_id += 1;
558    let mut fresh = || {
559        next_id += 1;
560        next_id - 1
561    };
562    let mut coedges = Vec::new();
563    for (k, coedge) in loop_coedges.iter().enumerate() {
564        let a = square[&k];
565        let b = square[&((k + 1) % n)];
566        let pcurve = brep_kernel::make_line(Vec3::new(a[0], a[1], 0.0), Vec3::new(b[0], b[1], 0.0))?;
567        coedges.push(CoedgeRecord { id: fresh(), edge_id: coedge.edge_id, forward: coedge.forward, pcurve });
568    }
569    // Orientation: the loop runs counter-clockwise on the square, so the
570    // surface normal Su × Sv is the side the region's facets face when the
571    // loop runs counter-clockwise around their outward normal — check it.
572    let facet_normal = region_faces.iter().fold(Vec3::new(0.0, 0.0, 0.0), |acc, &i| {
573        let face = &shell.faces[i];
574        let sign = if face.same_sense { 1.0 } else { -1.0 };
575        let normal = face.surface.normal(0.5 * (face.surface.knots_u[0] + face.surface.knots_u.last().unwrap()), 0.5 * (face.surface.knots_v[0] + face.surface.knots_v.last().unwrap()));
576        acc.add(normal.map(|n| n.scale(sign)).unwrap_or(Vec3::new(0.0, 0.0, 0.0)))
577    });
578    let mid = surface.normal(0.5, 0.5).map_err(|e| format!("NURBS normal: {e}"))?;
579    let same_sense = mid.dot(facet_normal) >= 0.0;
580    let name = shell.faces[region_faces[0]].name.clone();
581    let face = FaceRecord { id: fresh(), surface, same_sense, loops: vec![LoopRecord { id: fresh(), coedges }], name };
582
583    // Drop the region's faces, interior edges and interior vertices.
584    let interior_edges: HashSet<u64> = uses.iter().filter(|&(_, &n)| n == 2).map(|(&e, _)| e).collect();
585    let kept_vertices: HashSet<u64> = loop_vertices.iter().copied().collect();
586    let region_vertex_ids: HashSet<u64> = region_faces
587        .iter()
588        .flat_map(|&i| shell.faces[i].loops.iter().flat_map(|l| &l.coedges))
589        .filter_map(|c| coedge_ends(solid, c))
590        .flat_map(|(a, b)| [a, b])
591        .collect();
592    let first = region_faces[0];
593    let shell = &mut solid.shells[0];
594    let mut kept = Vec::with_capacity(shell.faces.len());
595    for (index, old) in std::mem::take(&mut shell.faces).into_iter().enumerate() {
596        if index == first {
597            kept.push(face.clone());
598        } else if !region_face_set.contains(&index) {
599            kept.push(old);
600        }
601    }
602    shell.faces = kept;
603    solid.edges.retain(|e| !interior_edges.contains(&e.id));
604    solid.vertices.retain(|v| !region_vertex_ids.contains(&v.id) || kept_vertices.contains(&v.id));
605    solid.mass_properties_cache = Default::default();
606    Ok(NurbsFaceOutcome { applied: true, max_deviation: Some(max_deviation), control_points: Some([cu, cv]), reason: None })
607}
608
609/// Samples of `surface` whose normal turns against the region's nearest
610/// source triangle (by more than a right angle, against the orientation most
611/// samples agree on): where the patch folds over itself. Sampled inside every
612/// knot span, off its creases. A folded patch has a second sheet beside its
613/// own boundary, which the importer's projector can land on.
614fn reversed_normals(surface: &NurbsSurface, u_breaks: &[f64], v_breaks: &[f64], tri_points: &[[Vec3; 3]]) -> usize {
615    let triangles: Vec<(Vec3, Vec3)> = tri_points
616        .iter()
617        .filter_map(|t| {
618            let n = t[1].sub(t[0]).cross(t[2].sub(t[0]));
619            let length = n.length();
620            (length > 0.0).then(|| (t[0].add(t[1]).add(t[2]).scale(1.0 / 3.0), n.scale(1.0 / length)))
621        })
622        .collect();
623    let mut dots = Vec::new();
624    for su in u_breaks.windows(2) {
625        for sv in v_breaks.windows(2) {
626            for a in [0.1, 0.5, 0.9] {
627                for b in [0.1, 0.5, 0.9] {
628                    let (u, v) = (su[0] + a * (su[1] - su[0]), sv[0] + b * (sv[1] - sv[0]));
629                    let (Ok(point), Ok(normal)) = (surface.evaluate(u, v), surface.normal(u, v)) else { continue };
630                    let Some((_, n)) = triangles.iter().min_by(|x, y| x.0.sub(point).length().total_cmp(&y.0.sub(point).length())) else { continue };
631                    dots.push(normal.dot(*n));
632                }
633            }
634        }
635    }
636    let sign = if dots.iter().sum::<f64>() >= 0.0 { 1.0 } else { -1.0 };
637    dots.iter().filter(|&&d| d * sign < 0.0).count()
638}
639
640/// The most conjugate-gradient steps one coordinate's solve takes.
641const CG_ITERATIONS: usize = 1500;
642
643/// Preconditioned conjugate gradients on the smoothed normal equations,
644/// matrix-free. The unknowns are the free control points' DISPLACEMENTS
645/// from `base` (the Coons net; the boundary's are zero), smoothed by their
646/// second divided differences in the control parameters `pu`/`pv`, each
647/// stencil weighted by its parameter extent (a discrete bending energy that
648/// does not depend on how unevenly the knots are spaced).
649#[allow(clippy::too_many_arguments)]
650fn solve_net(
651    base: &[Vec3],
652    free: &[usize],
653    free_index: &HashMap<usize, usize>,
654    rows: &[Vec<(usize, f64)>],
655    samples: &[(Vec3, [f64; 2])],
656    cu: usize,
657    cv: usize,
658    pu: &[f64],
659    pv: &[f64],
660    weight: f64,
661    start: &[Vec3],
662) -> Vec<Vec3> {
663    let m = free.len();
664    let w = weight.sqrt();
665    // Second divided differences of the net, both directions.
666    let divided = |t: &[f64], k: usize| -> Option<[f64; 3]> {
667        let (h1, h2) = (t[k] - t[k - 1], t[k + 1] - t[k]);
668        if !(h1 > 0.0 && h2 > 0.0) {
669            return None;
670        }
671        let extent = (0.5 * (h1 + h2)).sqrt();
672        Some([2.0 / (h1 * (h1 + h2)), -2.0 / (h1 * h2), 2.0 / (h2 * (h1 + h2))].map(|c| c * extent * w))
673    };
674    let mut stencils: Vec<([usize; 3], [f64; 3])> = Vec::new();
675    for i in 0..cu {
676        for j in 1..cv - 1 {
677            if let Some(co) = divided(pv, j) {
678                stencils.push(([i * cv + j - 1, i * cv + j, i * cv + j + 1], co));
679            }
680        }
681    }
682    for j in 0..cv {
683        for i in 1..cu - 1 {
684            if let Some(co) = divided(pu, i) {
685                stencils.push(([(i - 1) * cv + j, i * cv + j, (i + 1) * cv + j], co));
686            }
687        }
688    }
689    // The system's rows over the free unknowns only (samples, then
690    // stencils), resolved once: the iterations below never look an index up.
691    let free_rows: Vec<Vec<(usize, f64)>> = rows
692        .iter()
693        .map(|row| row.iter().filter_map(|&(c, wt)| free_index.get(&c).map(|&k| (k, wt))).collect())
694        .chain(stencils.iter().map(|(st, coefficients)| {
695            st.iter().zip(coefficients).filter_map(|(&c, &co)| free_index.get(&c).map(|&k| (k, co))).collect()
696        }))
697        .filter(|row: &Vec<(usize, f64)>| !row.is_empty())
698        .collect();
699    // A x for the free unknowns (one coordinate), A^T A applied. Fixed
700    // displacements are zero, so the stencils add nothing to the right side.
701    let apply = |x: &[f64]| -> Vec<f64> {
702        let mut out = vec![0.0; m];
703        for row in &free_rows {
704            let s: f64 = row.iter().map(|&(k, wt)| wt * x[k]).sum();
705            for &(k, wt) in row {
706                out[k] += wt * s;
707            }
708        }
709        out
710    };
711    // Jacobi preconditioner: the diagonal of the normal matrix.
712    let mut diagonal = vec![0.0; m];
713    for row in &free_rows {
714        for &(k, wt) in row {
715            diagonal[k] += wt * wt;
716        }
717    }
718    let inverse: Vec<f64> = diagonal.iter().map(|&d| if d > 0.0 { 1.0 / d } else { 1.0 }).collect();
719    let mut result = vec![Vec3::new(0.0, 0.0, 0.0); m];
720    for axis in 0..3 {
721        let coord = |p: Vec3| match axis {
722            0 => p.x,
723            1 => p.y,
724            _ => p.z,
725        };
726        // Right-hand side A^T (b - A base): what the samples ask beyond the
727        // Coons net.
728        let mut rhs = vec![0.0; m];
729        for ((point, _), row) in samples.iter().zip(rows) {
730            let fixed: f64 = row.iter().map(|&(c, wt)| wt * coord(base[c])).sum();
731            let r = coord(*point) - fixed;
732            for &(c, wt) in row {
733                if let Some(&k) = free_index.get(&c) {
734                    rhs[k] += wt * r;
735                }
736            }
737        }
738        // From `start` (zero: the Coons net itself).
739        let mut x: Vec<f64> = start.iter().map(|&p| coord(p)).collect();
740        let ax = apply(&x);
741        let mut r: Vec<f64> = rhs.iter().zip(&ax).map(|(b, a)| b - a).collect();
742        let mut z: Vec<f64> = r.iter().zip(&inverse).map(|(a, b)| a * b).collect();
743        let mut p = z.clone();
744        let mut rz: f64 = r.iter().zip(&z).map(|(a, b)| a * b).sum();
745        let scale: f64 = rhs.iter().map(|v| v * v).sum::<f64>().max(1e-300);
746        for _ in 0..CG_ITERATIONS.min(4 * m + 50) {
747            let rr: f64 = r.iter().map(|v| v * v).sum();
748            if rr <= 1e-24 * scale {
749                break;
750            }
751            let ap = apply(&p);
752            let pap: f64 = p.iter().zip(&ap).map(|(a, b)| a * b).sum();
753            if pap <= 0.0 {
754                break;
755            }
756            let alpha = rz / pap;
757            for k in 0..m {
758                x[k] += alpha * p[k];
759                r[k] -= alpha * ap[k];
760            }
761            for k in 0..m {
762                z[k] = r[k] * inverse[k];
763            }
764            let next: f64 = r.iter().zip(&z).map(|(a, b)| a * b).sum();
765            let beta = next / rz;
766            rz = next;
767            for k in 0..m {
768                p[k] = z[k] + beta * p[k];
769            }
770        }
771        for k in 0..m {
772            match axis {
773                0 => result[k].x = x[k],
774                1 => result[k].y = x[k],
775                _ => result[k].z = x[k],
776            }
777        }
778    }
779    result
780}