Skip to main content

brep_render/sheets/
hlr.rs

1//! The ANALYTIC hidden-line pass: what a sheet draws of the model, computed
2//! from the B-rep rather than from the display mesh.
3//!
4//! # The algorithm
5//!
6//! 1. **Candidate curves.** Every non-degenerate B-rep edge, sampled on its
7//!    own curve, and the SILHOUETTE of every curved face — the locus on the
8//!    exact surface where the viewing direction is tangent to it: two
9//!    generator lines of a cylinder or cone (closed form), one great circle of
10//!    a sphere (closed form), and for a torus, a general revolution or a
11//!    fitted surface the zero set of `n(u, v) · d` traced over the face's
12//!    parameter box and solved onto the surface through its own evaluator.
13//!    Each silhouette is trimmed to its face. An edge is INKED when its two
14//!    faces meet at a crease past [`EDGE_ANGLE_DEG`]
15//!    (the rule the mesh pass used, read here off the exact normals), when it
16//!    is a TANGENT LINE — a smooth edge between two surfaces, a round's or a
17//!    blend's, told from a split of one surface by the jump in curvature
18//!    across it (`one_surface`) — when it bounds only one face, or when more
19//!    than two share it; a smooth edge between two fragments of one surface
20//!    is kept as a SPLITTER only, and a seam (one face on both sides) is
21//!    neither.
22//! 2. **Splitting.** Every candidate is split where its projection crosses
23//!    another's, and where another's END lands on it — the only places the
24//!    number of surfaces in front of a line can change. The crossing test is
25//!    the kernel's own `segment_intersection` over the sampled paper chords.
26//! 3. **Visibility.** Each piece is decided at its MIDPOINT, evaluated on the
27//!    exact curve (a chord's midpoint sits inside a curved surface, and the
28//!    ray from there to the eye can cross the very surface it bounds): the
29//!    view ray from that point toward the eye is intersected with every face
30//!    whose paper box holds the point — a plane in closed form, anything else
31//!    through the kernel's `intersect_curve_surface` on the exact carrier —
32//!    and the piece is hidden when a hit inside that face's trim is nearer the
33//!    eye than the piece by more than the depth floor.
34//! 4. **Runs.** Consecutive visible pieces of one candidate are one run, so a
35//!    straight edge a hidden line crosses still comes back as its two ends,
36//!    and edge runs chain through a vertex exactly two inked edges share.
37//!
38//! # The two floors
39//!
40//! Both are the B-rep's own agreement bar, `KernelTolerances::pcurve_consistency`
41//! (the largest disagreement a valid in-memory B-rep allows between an edge's
42//! curve and the carriers it lies on), carried to the paper at the view's
43//! scale:
44//!
45//! - the **depth floor** — a face must be nearer the eye than the piece by
46//!   more than that bar to hide it, because an edge's own curve may sit that
47//!   far off the face it bounds;
48//! - the **crossing floor** — two projected curves closer than that bar touch,
49//!   and two split points closer than it along a curve are one. A touch is
50//!   measured against a CHORD, so it also admits the chord's own sag,
51//!   [`CHORD_MM`].
52//!
53//! Visibility is exact to those floors: no raster cell, no bias, no dropped
54//! fringe. What it does not see is what the SAMPLING does not: a trim notch
55//! or a silhouette loop smaller than one chord.
56//!
57//! # Before the B-rep arrives
58//!
59//! Steps 2 to 4 also run on a DISPLAY ([`visible_mesh_lines`]): the display
60//! edges and the mesh's silhouettes as candidates, the display triangles as
61//! what hides them. It is what a placement draws while its solids' topology is
62//! not in the scene, and the drawing says so — a stated approximation, the
63//! tessellation's chords in place of the exact curves.
64
65use std::collections::HashMap;
66
67use brep_kernel::{
68    containment_lane, intersect_analytic_pair, intersect_curve_surface, intersect_surfaces,
69    make_line, make_plane, parameter_point_in_face, project_point_to_curve,
70    project_point_to_surface, segment_intersection, trim_sample_count, trim_station,
71    AnalyticSurface, BrepSolid, ContainmentLane, FaceRecord, KernelTolerances, NurbsCurve,
72    NurbsSurface, PolygonClass, SurfaceIntersectionOptions, Vec2, Vec3,
73};
74
75use super::project::{ViewFrame, EDGE_ANGLE_DEG};
76use super::PlacedView;
77
78/// How far a sampled paper polyline may sit off its exact curve, paper
79/// millimetres. The comparand is the model line's own stroke,
80/// `EDGE_WIDTH` = 0.35 mm: seventy times finer, so
81/// no printed or zoomed drawing can show the chord.
82pub const CHORD_MM: f64 = 0.005;
83
84/// Deepest a sampled interval is halved in search of [`CHORD_MM`].
85const MAX_SUBDIVISION: u32 = 14;
86
87/// The kernel's tangency bar on a curve-surface hit (`intersect_curve_surface`
88/// flags `|n · t| <= 1e-3`), used for the closed-form plane too: a face the
89/// view ray only grazes has no area in front of the piece to hide it with.
90const GRAZING_COS: f64 = 1e-3;
91
92/// The fewest stations a silhouette is classified at against its face's trim,
93/// whatever its chord sampling: a trim notch shorter than 1/64 of the curve
94/// between two chord samples is what this pass does not see of a silhouette.
95const TRIM_STATIONS: usize = 64;
96
97/// Bisection steps when a curve's parameter is pinned where it leaves a face,
98/// crosses the cutting plane or crosses the detail circle — 2^-40 of one
99/// sampled interval, far under every floor.
100const BISECTION_STEPS: u32 = 40;
101
102
103fn v3(p: [f64; 3]) -> Vec3 {
104    Vec3::new(p[0], p[1], p[2])
105}
106
107fn a3(p: Vec3) -> [f64; 3] {
108    [p.x, p.y, p.z]
109}
110
111// ---------------------------------------------------------------------------
112// Inputs
113// ---------------------------------------------------------------------------
114
115/// What the drawn model is restricted to before anything is decided.
116pub enum Clip<'a> {
117    /// A plain placement: the whole model.
118    Whole,
119    /// A SECTION: the half-space at or beyond the cutting plane, which is the
120    /// view's own target plane (depth zero, the camera looking along its
121    /// normal). `cap` is the cut region on the paper — the exact section
122    /// curves — which hides everything behind it. `whole`, per solid (by
123    /// index; absent is false), marks a solid the section leaves whole — a
124    /// shaft, a fastener: drawn on both sides of the plane, hiding from both,
125    /// and contributing nothing to the cut. `half`, a HALF section's second
126    /// plane, keeps everything on its side whole as well (and `cap` is then
127    /// closed along its line).
128    Beyond { cap: &'a [Vec<[f64; 2]>], whole: &'a [bool], half: Option<HalfLine> },
129    /// A DETAIL: the cylinder of MODEL radius `radius` along the viewing
130    /// direction through the view's target.
131    Circle { radius: f64 },
132}
133
134impl Clip<'_> {
135    /// Whether solid `index` is drawn whole under this clip.
136    fn whole(&self, index: usize) -> bool {
137        matches!(self, Clip::Beyond { whole, .. } if whole.get(index) == Some(&true))
138    }
139
140    /// A section's margin at view point `view`, model units: negative where
141    /// the cut removes material — in front of the cutting plane and, for a
142    /// half section, on the removed side of its second plane too.
143    fn margin(&self, view: [f64; 3]) -> f64 {
144        match self {
145            Clip::Beyond { half: Some(half), .. } => view[2].max(half.at(view)),
146            _ => view[2],
147        }
148    }
149}
150
151/// A HALF section's second plane, perpendicular to the cutting plane and so
152/// edge-on to the section view: a line on the paper. In view units, `normal`
153/// is its unit normal in the view's `(right, up)` and `offset` its distance
154/// from the view's target; the side the normal points to is kept whole.
155#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
156pub struct HalfLine {
157    pub normal: [f64; 2],
158    pub offset: f64,
159}
160
161impl HalfLine {
162    /// Signed distance of view point `view` from the line, positive on the
163    /// side kept whole.
164    pub fn at(&self, view: [f64; 3]) -> f64 {
165        view[0] * self.normal[0] + view[1] * self.normal[1] - self.offset
166    }
167}
168
169/// The floors of one placement, see the module docs.
170#[derive(Debug, Clone, Copy)]
171pub struct Floors {
172    /// The B-rep's agreement bar, model units.
173    pub bar: f64,
174    /// The depth floor, paper millimetres (`bar × scale`).
175    pub depth_mm: f64,
176    /// The crossing floor, paper millimetres (`bar × scale`).
177    pub crossing_mm: f64,
178}
179
180impl Floors {
181    pub fn of(scale: f64) -> Floors {
182        let bar = KernelTolerances::default().pcurve_consistency;
183        Floors { bar, depth_mm: bar * scale, crossing_mm: bar * scale }
184    }
185}
186
187/// The exact result of one placement: the visible runs on the paper, plus
188/// what the A/B comparison and the tests read.
189#[derive(Debug, Clone, Default)]
190pub struct HlrResult {
191    pub runs: Vec<Vec<[f64; 2]>>,
192    /// Inked candidate curves before splitting (edges and silhouette pieces).
193    pub inked: usize,
194    /// Silhouette pieces among them.
195    pub silhouettes: usize,
196    /// Pieces the splitting produced and visibility decided.
197    pub pieces: usize,
198    /// How many times the pass asked the kernel whether a point is inside a
199    /// face's trim (`parameter_point_in_face`) — the question the projected and
200    /// parameter trims exist to answer first.
201    pub trim_queries: usize,
202    /// How many view rays the pass intersected with a curved face's carrier
203    /// (`intersect_curve_surface`).
204    pub ray_queries: usize,
205}
206
207thread_local! {
208    /// The kernel questions of the pass running on this thread, for
209    /// [`HlrResult::trim_queries`] and [`HlrResult::ray_queries`].
210    static TRIM_QUERIES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
211    static RAY_QUERIES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
212}
213
214
215
216// ---------------------------------------------------------------------------
217// Curves
218// ---------------------------------------------------------------------------
219
220/// A candidate's exact carrier, evaluated at its own parameter.
221#[derive(Clone)]
222enum Carrier<'a> {
223    /// A B-rep edge's curve.
224    Edge(&'a NurbsCurve),
225    /// A curve the kernel handed back (an exact section of a carrier).
226    Curve(NurbsCurve),
227    /// A straight generator from `a` (s = 0) to `b` (s = 1).
228    Line { a: Vec3, b: Vec3 },
229    /// `centre + e1 cos s + e2 sin s` — `e1`, `e2` carry the radius.
230    Circle { centre: Vec3, e1: Vec3, e2: Vec3 },
231    /// A silhouette TRACED on a general surface: `uv[floor(s)]` toward
232    /// `uv[floor(s) + 1]`, re-solved onto `n · d = 0` at every evaluation.
233    Traced { surface: &'a NurbsSurface, uv: Vec<[f64; 2]>, dir: Vec3 },
234    /// A marched section polyline (`intersect_surfaces`): `s` indexes the
235    /// points, which the marcher already put on both carriers.
236    Polyline(Vec<Vec3>),
237}
238
239impl Carrier<'_> {
240    fn at(&self, s: f64) -> Option<Vec3> {
241        match self {
242            Carrier::Edge(curve) => curve.evaluate(s).ok(),
243            Carrier::Curve(curve) => curve.evaluate(s).ok(),
244            Carrier::Line { a, b } => Some(a.add(b.sub(*a).scale(s))),
245            Carrier::Circle { centre, e1, e2 } => Some(centre.add(e1.scale(s.cos())).add(e2.scale(s.sin()))),
246            Carrier::Traced { surface, uv, dir } => {
247                let (point, _) = traced_at(surface, uv, *dir, s)?;
248                Some(point)
249            }
250            Carrier::Polyline(points) => {
251                let last = points.len().checked_sub(1)?;
252                let i = (s.floor().max(0.0) as usize).min(last.saturating_sub(1));
253                let f = (s - i as f64).clamp(0.0, 1.0);
254                let (a, b) = (points[i], points[(i + 1).min(last)]);
255                Some(a.add(b.sub(a).scale(f)))
256            }
257        }
258    }
259
260    /// Whether the carrier is a straight line, so sampling needs no interior
261    /// point to be exact.
262    fn straight(&self) -> bool {
263        match self {
264            Carrier::Line { .. } => true,
265            Carrier::Edge(curve) => curve.degree == 1 && curve.control_points.len() == 2,
266            Carrier::Curve(curve) => curve.degree == 1 && curve.control_points.len() == 2,
267            _ => false,
268        }
269    }
270}
271
272/// `n(u, v) · d` with the normal normalised — the silhouette's defining
273/// function, sign-blind (the locus is the same for either orientation).
274fn grazing(surface: &NurbsSurface, dir: Vec3, u: f64, v: f64) -> Option<f64> {
275    surface.normal(u, v).ok().map(|n| n.dot(dir))
276}
277
278/// A traced silhouette at fractional station `s`: the straight uv step
279/// between the two bracketing stations, then a root of `n · d` along the uv
280/// direction PERPENDICULAR to that step, bracketed within one step's length.
281fn traced_at(surface: &NurbsSurface, uv: &[[f64; 2]], dir: Vec3, s: f64) -> Option<(Vec3, [f64; 2])> {
282    let last = uv.len().checked_sub(1)?;
283    if last == 0 {
284        let p = surface.evaluate(uv[0][0], uv[0][1]).ok()?;
285        return Some((p, uv[0]));
286    }
287    let i = (s.floor().max(0.0) as usize).min(last - 1);
288    let f = (s - i as f64).clamp(0.0, 1.0);
289    let (a, b) = (uv[i], uv[i + 1]);
290    let base = [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f];
291    let step = [b[0] - a[0], b[1] - a[1]];
292    let length = (step[0] * step[0] + step[1] * step[1]).sqrt();
293    let solved = if f <= 0.0 || f >= 1.0 || length <= 0.0 {
294        base
295    } else {
296        let across = [-step[1], step[0]];
297        root_along(surface, dir, base, across).unwrap_or(base)
298    };
299    let p = surface.evaluate(solved[0], solved[1]).ok()?;
300    Some((p, solved))
301}
302
303/// A root of `n · d` on the uv segment `base ± across`, by bisection from the
304/// first sign change outward from `base`.
305fn root_along(surface: &NurbsSurface, dir: Vec3, base: [f64; 2], across: [f64; 2]) -> Option<[f64; 2]> {
306    let at = |t: f64| [base[0] + across[0] * t, base[1] + across[1] * t];
307    let g0 = {
308        let p = at(0.0);
309        grazing(surface, dir, p[0], p[1])?
310    };
311    if g0 == 0.0 {
312        return Some(base);
313    }
314    for (lo, hi) in [(0.0, 0.5), (0.0, -0.5), (0.5, 1.0), (-0.5, -1.0)] {
315        let (p_lo, p_hi) = (at(lo), at(hi));
316        let g_lo = grazing(surface, dir, p_lo[0], p_lo[1])?;
317        let g_hi = grazing(surface, dir, p_hi[0], p_hi[1])?;
318        if g_lo.signum() == g_hi.signum() {
319            continue;
320        }
321        let (mut lo, mut hi, mut g_lo) = (lo, hi, g_lo);
322        for _ in 0..BISECTION_STEPS {
323            let mid = 0.5 * (lo + hi);
324            let p = at(mid);
325            let g = grazing(surface, dir, p[0], p[1])?;
326            if g.signum() == g_lo.signum() {
327                lo = mid;
328                g_lo = g;
329            } else {
330                hi = mid;
331            }
332        }
333        return Some(at(0.5 * (lo + hi)));
334    }
335    None
336}
337
338/// The paper image of a model point, and its view depth in MODEL units.
339fn project(frame: &ViewFrame, placed: &PlacedView, p: Vec3) -> ([f64; 2], [f64; 3]) {
340    let view = frame.to_view(a3(p));
341    ([placed.position[0] + view[0] * placed.scale, placed.position[1] - view[1] * placed.scale], view)
342}
343
344fn paper_distance(a: [f64; 2], b: [f64; 2]) -> f64 {
345    ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt()
346}
347
348/// Distance from `p` to the paper segment `a`–`b`, and the segment parameter
349/// of the foot.
350fn segment_foot(p: [f64; 2], a: [f64; 2], b: [f64; 2]) -> (f64, f64) {
351    let d = [b[0] - a[0], b[1] - a[1]];
352    let length2 = d[0] * d[0] + d[1] * d[1];
353    let t = if length2 > 0.0 {
354        (((p[0] - a[0]) * d[0] + (p[1] - a[1]) * d[1]) / length2).clamp(0.0, 1.0)
355    } else {
356        0.0
357    };
358    let foot = [a[0] + d[0] * t, a[1] + d[1] * t];
359    (paper_distance(p, foot), t)
360}
361
362/// One sampled curve: parameters, model points and paper points, in order.
363#[derive(Debug, Clone, Default)]
364struct Samples {
365    params: Vec<f64>,
366    points: Vec<Vec3>,
367    paper: Vec<[f64; 2]>,
368    view: Vec<[f64; 3]>,
369}
370
371impl Samples {
372    fn push(&mut self, s: f64, p: Vec3, frame: &ViewFrame, placed: &PlacedView) {
373        let (paper, view) = project(frame, placed, p);
374        self.params.push(s);
375        self.points.push(p);
376        self.paper.push(paper);
377        self.view.push(view);
378    }
379
380    fn len(&self) -> usize {
381        self.params.len()
382    }
383
384    fn paper_length(&self) -> f64 {
385        self.paper.windows(2).map(|w| paper_distance(w[0], w[1])).sum()
386    }
387}
388
389/// Sample `carrier` over `[s0, s1]` so every chord is within [`CHORD_MM`] of
390/// the exact curve ON THE PAPER, starting from `initial` equal intervals. The
391/// test is against the chord as a SEGMENT, so a curve that doubles back in
392/// projection (a circle seen edge-on) is still subdivided at its turn.
393fn sample(
394    carrier: &Carrier,
395    s0: f64,
396    s1: f64,
397    initial: usize,
398    frame: &ViewFrame,
399    placed: &PlacedView,
400) -> Samples {
401    let mut out = Samples::default();
402    let initial = if carrier.straight() { 1 } else { initial.max(2) };
403    let Some(first) = carrier.at(s0) else { return out };
404    out.push(s0, first, frame, placed);
405    for k in 0..initial {
406        let a = s0 + (s1 - s0) * k as f64 / initial as f64;
407        let b = s0 + (s1 - s0) * (k + 1) as f64 / initial as f64;
408        let (Some(pa), Some(pb)) = (carrier.at(a), carrier.at(b)) else { continue };
409        subdivide(carrier, (a, pa), (b, pb), 0, frame, placed, &mut out);
410    }
411    out
412}
413
414fn subdivide(
415    carrier: &Carrier,
416    (a, pa): (f64, Vec3),
417    (b, pb): (f64, Vec3),
418    depth: u32,
419    frame: &ViewFrame,
420    placed: &PlacedView,
421    out: &mut Samples,
422) {
423    if depth < MAX_SUBDIVISION && !carrier.straight() {
424        let mid = 0.5 * (a + b);
425        if let Some(pm) = carrier.at(mid) {
426            let (paper_a, _) = project(frame, placed, pa);
427            let (paper_b, _) = project(frame, placed, pb);
428            let (paper_m, _) = project(frame, placed, pm);
429            if segment_foot(paper_m, paper_a, paper_b).0 > CHORD_MM {
430                subdivide(carrier, (a, pa), (mid, pm), depth + 1, frame, placed, out);
431                subdivide(carrier, (mid, pm), (b, pb), depth + 1, frame, placed, out);
432                return;
433            }
434        }
435    }
436    out.push(b, pb, frame, placed);
437}
438
439// ---------------------------------------------------------------------------
440// Candidates
441// ---------------------------------------------------------------------------
442
443/// Which B-rep vertex an edge candidate starts or ends on, for chaining.
444type VertexKey = (usize, u64);
445
446struct Candidate<'a> {
447    carrier: Carrier<'a>,
448    samples: Samples,
449    /// Drawn (an inked edge or a silhouette) or only a SPLITTER.
450    inked: bool,
451    silhouette: bool,
452    /// The B-rep edge an edge candidate samples.
453    edge: Option<(usize, u64)>,
454    ends: Option<(VertexKey, VertexKey)>,
455    /// Paper box of the samples.
456    bounds: [f64; 4],
457    /// Its solid is left whole by the section ([`Clip::Beyond`]).
458    whole: bool,
459}
460
461fn bounds_of(paper: &[[f64; 2]]) -> [f64; 4] {
462    let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
463    for p in paper {
464        b[0] = b[0].min(p[0]);
465        b[1] = b[1].min(p[1]);
466        b[2] = b[2].max(p[0]);
467        b[3] = b[3].max(p[1]);
468    }
469    b
470}
471
472/// Initial sampling density of a NURBS curve: a few intervals per span.
473fn curve_initial(curve: &NurbsCurve) -> usize {
474    let spans = curve.control_points.len().saturating_sub(curve.degree).max(1);
475    (spans * (curve.degree + 1)).clamp(2, 256)
476}
477
478/// The outward normal of `face` at model point `p` (the carrier's normal,
479/// turned by `same_sense`), through the carrier's own projection.
480fn outward_normal(face: &FaceRecord, p: Vec3) -> Option<Vec3> {
481    let foot = project_point_to_surface(&face.surface, p).ok()?;
482    let n = face.surface.normal(foot.u, foot.v).ok()?;
483    Some(if face.same_sense { n } else { n.scale(-1.0) })
484}
485
486/// Whether an edge between `a` and `b` is a CREASE anywhere along it: the two
487/// outward normals more than [`EDGE_ANGLE_DEG`] apart at any of five interior
488/// stations. A normal that cannot be evaluated counts as a crease — drawn
489/// rather than guessed away.
490fn is_crease(curve: &NurbsCurve, t0: f64, t1: f64, a: &FaceRecord, b: &FaceRecord) -> bool {
491    let bar = EDGE_ANGLE_DEG.to_radians().cos();
492    for k in 1..=5 {
493        let t = t0 + (t1 - t0) * k as f64 / 6.0;
494        let Ok(p) = curve.evaluate(t) else { return true };
495        let (Some(na), Some(nb)) = (outward_normal(a, p), outward_normal(b, p)) else { return true };
496        if na.dot(nb) < bar {
497            return true;
498        }
499    }
500    false
501}
502
503/// Every face's index by the edges its loops use: `edge id -> [(face index)]`.
504fn edge_faces(solid: &BrepSolid) -> HashMap<u64, Vec<(usize, usize)>> {
505    let mut map: HashMap<u64, Vec<(usize, usize)>> = HashMap::new();
506    for (shell_index, shell) in solid.shells.iter().enumerate() {
507        for (face_index, face) in shell.faces.iter().enumerate() {
508            for lp in &face.loops {
509                for coedge in &lp.coedges {
510                    map.entry(coedge.edge_id).or_default().push((shell_index, face_index));
511                }
512            }
513        }
514    }
515    map
516}
517
518/// The diagonal of the box two faces' BOUNDARIES span — both ends and the
519/// middle of every edge either face's loops use: how far, to the scale
520/// [`one_surface`] needs, the pair reaches from an edge it shares. A face that
521/// bulges past its own boundary reaches farther than this.
522fn pair_extent(solid: &BrepSolid, a: &FaceRecord, b: &FaceRecord, edge_index: &HashMap<u64, usize>) -> f64 {
523    let mut lo = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
524    let mut hi = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
525    for coedge in [a, b].into_iter().flat_map(|face| &face.loops).flat_map(|lp| &lp.coedges) {
526        let Some(edge) = edge_index.get(&coedge.edge_id).map(|&i| &solid.edges[i]) else { continue };
527        for t in [edge.t0, 0.5 * (edge.t0 + edge.t1), edge.t1] {
528            if let Ok(q) = edge.curve.evaluate(t) {
529                lo = Vec3::new(lo.x.min(q.x), lo.y.min(q.y), lo.z.min(q.z));
530                hi = Vec3::new(hi.x.max(q.x), hi.y.max(q.y), hi.z.max(q.z));
531            }
532        }
533    }
534    if lo.x <= hi.x {
535        hi.sub(lo).length()
536    } else {
537        0.0
538    }
539}
540
541/// The normal curvature of `face` at `p` ACROSS the direction `tangent`,
542/// signed against the face's outward normal, read off the carrier's second
543/// derivatives at the foot of `p`.
544fn curvature_across(face: &FaceRecord, p: Vec3, tangent: Vec3) -> Option<f64> {
545    let foot = project_point_to_surface(&face.surface, p).ok()?;
546    let d = face.surface.derivatives(foot.u, foot.v, 2).ok()?;
547    let (su, sv) = (d[1][0], d[0][1]);
548    let normal = su.cross(sv).normalized().ok()?;
549    let normal = if face.same_sense { normal } else { normal.scale(-1.0) };
550    let across = normal.cross(tangent).normalized().ok()?;
551    // `across` in the carrier's own tangent basis, then II / I along it.
552    let (e, f, g) = (su.dot(su), su.dot(sv), sv.dot(sv));
553    let det = e * g - f * f;
554    if det.abs() <= f64::EPSILON * e * g {
555        return None;
556    }
557    let (cu, cv) = (across.dot(su), across.dot(sv));
558    let (a, b) = ((g * cu - f * cv) / det, (e * cv - f * cu) / det);
559    let second = d[2][0].dot(normal) * a * a + 2.0 * d[1][1].dot(normal) * a * b + d[0][2].dot(normal) * b * b;
560    let first = e * a * a + 2.0 * f * a * b + g * b * b;
561    (first > 0.0).then(|| second / first)
562}
563
564/// Whether the two faces of a SMOOTH edge are ONE surface — fragments of a
565/// split no merge removed, whose shared edge is not a line of the part —
566/// rather than two surfaces meeting tangentially, where the edge is a TANGENT
567/// LINE (a round's, a blend's) and is drawn.
568///
569/// Two carriers tangent along an edge whose normal curvatures across it
570/// differ by Δκ part by Δκ·w²/2 at a distance w from it. They are one surface
571/// when that parting, over `extent` ([`pair_extent`]), is within the B-rep bar at every one of five interior stations —
572/// the same stations and the same doubt rule as [`is_crease`]: a curvature
573/// that cannot be evaluated draws the line. Read at the edge, where both
574/// carriers are inside their own domains, so a fragment's carrier being
575/// bounded to its fragment does not matter. What it does not see is a
576/// curvature-CONTINUOUS (G2) joint between two surfaces: its edge is drawn
577/// as one surface's.
578fn one_surface(curve: &NurbsCurve, t0: f64, t1: f64, a: &FaceRecord, b: &FaceRecord, extent: f64, bar: f64) -> bool {
579    for k in 1..=5 {
580        let t = t0 + (t1 - t0) * k as f64 / 6.0;
581        let (Ok(p), Ok(d)) = (curve.evaluate(t), curve.derivatives(t, 1)) else { return false };
582        let (Some(ka), Some(kb)) = (curvature_across(a, p, d[1]), curvature_across(b, p, d[1])) else { return false };
583        if (ka - kb).abs() * extent * extent * 0.5 > bar {
584            return false;
585        }
586    }
587    true
588}
589
590fn edge_candidates<'a>(
591    solid_index: usize,
592    solid: &'a BrepSolid,
593    frame: &ViewFrame,
594    placed: &PlacedView,
595    floors: &Floors,
596) -> Vec<Candidate<'a>> {
597    let faces = edge_faces(solid);
598    let edge_index: HashMap<u64, usize> = solid.edges.iter().enumerate().map(|(i, edge)| (edge.id, i)).collect();
599    // How far each smooth pair of faces reaches, read once per pair.
600    let mut extents: HashMap<[(usize, usize); 2], f64> = HashMap::new();
601    let mut out = Vec::new();
602    for edge in &solid.edges {
603        if edge.degenerate {
604            continue;
605        }
606        let owners = faces.get(&edge.id).cloned().unwrap_or_default();
607        let mut distinct = owners.clone();
608        distinct.sort_unstable();
609        distinct.dedup();
610        let inked = match distinct.len() {
611            // A seam: the face meets itself. Not a line of the drawing, and
612            // not a place visibility can change (the surface is one surface
613            // across it), so it is not even a splitter.
614            1 if owners.len() >= 2 => continue,
615            // A crease, or a tangent line between two surfaces; an edge
616            // between two fragments of one surface is a splitter only.
617            2 => {
618                let face = |(s, f): (usize, usize)| &solid.shells[s].faces[f];
619                let (a, b) = (face(distinct[0]), face(distinct[1]));
620                is_crease(&edge.curve, edge.t0, edge.t1, a, b) || {
621                    let extent = *extents
622                        .entry([distinct[0], distinct[1]])
623                        .or_insert_with(|| pair_extent(solid, a, b, &edge_index));
624                    !one_surface(&edge.curve, edge.t0, edge.t1, a, b, extent, floors.bar)
625                }
626            }
627            _ => true,
628        };
629        let carrier = Carrier::Edge(&edge.curve);
630        let samples = sample(&carrier, edge.t0, edge.t1, curve_initial(&edge.curve), frame, placed);
631        if samples.len() < 2 {
632            continue;
633        }
634        let bounds = bounds_of(&samples.paper);
635        out.push(Candidate {
636            carrier,
637            samples,
638            inked,
639            silhouette: false,
640            edge: Some((solid_index, edge.id)),
641            ends: Some(((solid_index, edge.start_vertex_id), (solid_index, edge.end_vertex_id))),
642            bounds,
643            whole: false,
644        });
645    }
646    out
647}
648
649/// Whether a face is flat: a recognized plane, or an affine bilinear patch.
650fn planar(surface: &NurbsSurface) -> bool {
651    matches!(surface.analytic(), Some(AnalyticSurface::Plane { .. })) || surface.is_affine().unwrap_or(false)
652}
653
654/// The uv box of a face's trim: the control points of every loop's pcurves,
655/// clamped to the carrier's domain (a pcurve lies in its own control hull).
656fn trim_box(face: &FaceRecord) -> Option<[f64; 4]> {
657    let du = face.surface.domain_u().ok()?;
658    let dv = face.surface.domain_v().ok()?;
659    let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
660    for lp in &face.loops {
661        for coedge in &lp.coedges {
662            for cp in &coedge.pcurve.control_points {
663                let w = if cp.w != 0.0 { cp.w } else { 1.0 };
664                b[0] = b[0].min(cp.x / w);
665                b[1] = b[1].min(cp.y / w);
666                b[2] = b[2].max(cp.x / w);
667                b[3] = b[3].max(cp.y / w);
668            }
669        }
670    }
671    if !(b[0] <= b[2]) {
672        return Some([du[0], dv[0], du[1], dv[1]]);
673    }
674    Some([b[0].max(du[0]), b[1].max(dv[0]), b[2].min(du[1]), b[3].min(dv[1])])
675}
676
677/// What a pass learns about every trim of one solid that no camera, scale or
678/// clip changes: each face's `TrimTests`, `[shell][face]`. The projection
679/// cache keeps one per solid it draws ([`crate::sheets::project`]), so a
680/// re-projection — another scale, a section or a detail of the same part —
681/// asks nothing a previous one built.
682pub struct SolidTrims {
683    faces: Vec<Vec<TrimTests>>,
684}
685
686impl SolidTrims {
687    pub fn of(solid: &BrepSolid) -> SolidTrims {
688        SolidTrims { faces: solid.shells.iter().map(|shell| shell.faces.iter().map(|_| TrimTests::new()).collect()).collect() }
689    }
690}
691
692/// What a pass learns about a face's trim, so the kernel is asked only what
693/// nothing cheaper can answer: which of the kernel's containment lanes the face
694/// is in ([`containment_lane`]), its [`UvReach`], and — the first time a
695/// question gets past that — its [`UvTrim`]. All are read off the face alone,
696/// so they hold for any view of it, and each is learned the first time a
697/// question about the face needs it.
698struct TrimTests {
699    lane: std::cell::OnceCell<Option<ContainmentLane>>,
700    reach: std::cell::OnceCell<Option<UvReach>>,
701    uv: std::cell::OnceCell<Option<UvTrim>>,
702}
703
704impl TrimTests {
705    fn new() -> TrimTests {
706        TrimTests { lane: std::cell::OnceCell::new(), reach: std::cell::OnceCell::new(), uv: std::cell::OnceCell::new() }
707    }
708
709    /// The kernel's lane for the face; `None` where preparing it fails, where
710    /// the kernel's own answer is an error and nothing answers ahead of it.
711    fn lane(&self, face: &FaceRecord) -> Option<&ContainmentLane> {
712        self.lane.get_or_init(|| containment_lane(face).ok()).as_ref()
713    }
714
715    /// The face's [`UvReach`].
716    fn reach(&self, face: &FaceRecord) -> Option<&UvReach> {
717        self.reach.get_or_init(|| self.lane(face).and_then(|lane| UvReach::of(face, lane))).as_ref()
718    }
719
720    /// The face's [`UvTrim`], built the first time the kernel would be asked.
721    fn uv_trim(&self, face: &FaceRecord) -> Option<&UvTrim> {
722        self.uv
723            .get_or_init(|| {
724                let reach = self.reach(face)?;
725                UvTrim::of(face, reach, self.lane(face)?)
726            })
727            .as_ref()
728    }
729}
730
731/// A face's trim in its carrier's parameters, as the kernel's containment
732/// test reads it, so that a point clear of its chords is classified here and
733/// only a point near them is asked of the kernel. Two of the kernel's lanes
734/// are mirrored; any other face has none.
735///
736/// **The plain lane** — the kernel's generic scan, for a face
737/// [`containment_lane`] puts there without the sphere chart — samples each
738/// pcurve at the kernel's [`trim_sample_count`] stations, [`trim_station`]
739/// apart (`max(2, (k + 1)(p + 1)·4)`, `k` the distinct interior knots), and
740/// answers `Boundary` (inside) within its band
741/// of a chord, counts crossings of that polygon farther than one chord's length
742/// from the nearest chord, and takes the side of the pcurve itself nearer than
743/// that. This polygon's vertices are a subset of those stations — one per
744/// power-of-two stride, halved on the kernel's own stations wherever the
745/// chord's middle sits off it by more than the kernel's band at the trim's
746/// middle — and `margin` is the largest such middle's distance off its chord
747/// (and the gaps where one pcurve's end meets the next one's start). The
748/// kernel's polygon and the pcurves both lie within that of this one, so a
749/// point farther than the kernel's band plus `margin` from every chord is
750/// outside that band of the kernel's polygon and on the same side of it as of
751/// this one and of the pcurves: whichever rule the kernel uses there, its
752/// answer is this polygon's even-odd count. The side rule assumes the loops
753/// run with the material to the left of each pcurve on a same-sense face (to
754/// the right otherwise); a face whose loops do not is left to the kernel.
755///
756/// **The seam-horizon lane**, for a face [`containment_lane`] puts in the
757/// kernel's wrapped-horizon lane: the loops the kernel published, unwrapped
758/// sample to sample, `Boundary` within the band of a chord at any of the
759/// query's three period images, otherwise each loop's even-odd count summed
760/// over the images, mod 2. Here the polygon IS the kernel's, every station
761/// kept, and a point is asked of the kernel within twice its band of a chord at
762/// any image. With the kernel's per-image fallback switched on
763/// ([`brep_kernel::horizon_cross_frame`] false) it is not built.
764///
765/// Not built for a face in any other lane: the seam band, the sphere cap, the
766/// covering rim strip, or the generic scan where the sphere chart substitutes
767/// for its parity count.
768struct UvTrim {
769    /// `(from, to, loop)`.
770    segments: Vec<([f64; 2], [f64; 2], usize)>,
771    loops: usize,
772    grid: Grid,
773    /// How far beyond the kernel's band a point must be from every chord.
774    margin: Margin,
775    /// The period images the query is counted at.
776    images: Vec<f64>,
777}
778
779#[derive(Debug, Clone, Copy)]
780enum Margin {
781    /// Plus this, in the carrier's parameters.
782    Plus(f64),
783    /// The kernel's band again.
784    Twice,
785}
786
787impl UvTrim {
788    fn of(face: &FaceRecord, reach: &UvReach, lane: &ContainmentLane) -> Option<UvTrim> {
789        match lane {
790            ContainmentLane::WrappedHorizon { loops, u_period, cross_frame: true } => {
791                let loops = loops.iter().map(|lp| lp.iter().map(|p| [p.x, p.y]).collect()).collect();
792                return Some(UvTrim::grid_of(loops, Margin::Twice, vec![-u_period, 0.0, *u_period]));
793            }
794            ContainmentLane::Generic { sphere_chart: false } => {}
795            _ => return None,
796        }
797        let surface = &face.surface;
798        // Chords no coarser than the band the kernel asks with at the trim's
799        // middle, where the curve allows it.
800        let centre = [0.5 * (reach.lo[0] + reach.hi[0]), 0.5 * (reach.lo[1] + reach.hi[1])];
801        let fine = trim_band(surface, centre, KernelTolerances::default().pcurve_consistency);
802        let mut rings: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
803        let mut sag: f64 = 0.0;
804        for lp in &face.loops {
805            let mut ring = Vec::new();
806            for coedge in &lp.coedges {
807                let curve = &coedge.pcurve;
808                let [start, end] = curve.domain().ok()?;
809                let count = trim_sample_count(curve);
810                let station = |i: usize| if i < count { trim_station(start, end, i, count) } else { end };
811                let at = |i: usize| -> Option<[f64; 2]> { curve.evaluate(station(i)).ok().map(|p| [p.x, p.y]) };
812                // Every station ours is one of the kernel's, the coarsest a
813                // power-of-two stride that still gives two chords.
814                let mut stride = 1;
815                while count % (stride * 2) == 0 && count / (stride * 2) >= 2 {
816                    stride *= 2;
817                }
818                let mut a = at(0)?;
819                // The kernel's polygon runs from a pcurve's last station
820                // straight to the next one's first; both polygons meet the
821                // pcurves' own ends only to the gap between them.
822                if let Some(previous) = ring.last() {
823                    sag = sag.max(paper_distance(*previous, a));
824                }
825                for from in (0..count).step_by(stride) {
826                    let to = from + stride;
827                    let b = at(to)?;
828                    // Halve on the kernel's stations while the middle sits
829                    // off the chord by more than `fine`; a single kernel
830                    // chord is measured at its parameter middle.
831                    let mut pending = vec![(from, a, to, b)];
832                    while let Some((i, pi, j, pj)) = pending.pop() {
833                        if j - i >= 2 {
834                            let m = (i + j) / 2;
835                            let pm = at(m)?;
836                            let off = segment_foot(pm, pi, pj).0;
837                            if off > fine {
838                                pending.push((m, pm, j, pj));
839                                pending.push((i, pi, m, pm));
840                                continue;
841                            }
842                            sag = sag.max(off);
843                        } else {
844                            let pm = curve.evaluate(0.5 * (station(i) + station(j))).ok()?;
845                            sag = sag.max(segment_foot([pm.x, pm.y], pi, pj).0);
846                        }
847                        ring.push(pi);
848                    }
849                    a = b;
850                }
851                ring.push(a);
852            }
853            if let (Some(first), Some(last)) = (ring.first(), ring.last()) {
854                sag = sag.max(paper_distance(*first, *last));
855            }
856            if ring.len() >= 2 {
857                rings.push(ring);
858            }
859        }
860        let area = |ring: &[[f64; 2]]| -> f64 {
861            (0..ring.len()).map(|i| {
862                let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
863                a[0] * b[1] - b[0] * a[1]
864            }).sum()
865        };
866        let areas: Vec<f64> = rings.iter().map(|ring| area(ring)).collect();
867        let outer = (0..rings.len()).max_by(|&i, &j| areas[i].abs().partial_cmp(&areas[j].abs()).unwrap_or(std::cmp::Ordering::Equal))?;
868        let left = if face.same_sense { 1.0 } else { -1.0 };
869        for (index, a) in areas.iter().enumerate() {
870            let expected = if index == outer { left } else { -left };
871            if !(a * expected > 0.0) {
872                return None;
873            }
874        }
875        Some(UvTrim::grid_of(rings, Margin::Plus(sag), vec![0.0]))
876    }
877
878    fn grid_of(rings: Vec<Vec<[f64; 2]>>, margin: Margin, images: Vec<f64>) -> UvTrim {
879        let mut segments = Vec::new();
880        for (index, ring) in rings.iter().enumerate() {
881            for i in 0..ring.len() {
882                segments.push((ring[i], ring[(i + 1) % ring.len()], index));
883            }
884        }
885        let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
886        let boxed = |(a, b, _): &([f64; 2], [f64; 2], usize)| [a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])];
887        for segment in &segments {
888            let g = boxed(segment);
889            extent = [extent[0].min(g[0]), extent[1].min(g[1]), extent[2].max(g[2]), extent[3].max(g[3])];
890        }
891        let mut grid = Grid::new(extent, segments.len().max(1));
892        for (index, segment) in segments.iter().enumerate() {
893            grid.insert(boxed(segment), index);
894        }
895        UvTrim { segments, loops: rings.len(), grid, margin, images }
896    }
897
898    /// Inside the trim, or `None` near enough a chord that only the kernel
899    /// can say, `band` being the band the kernel is asked with.
900    fn covers(&self, uv: [f64; 2], band: f64) -> Option<bool> {
901        let near = match self.margin {
902            Margin::Plus(sag) => band + sag,
903            Margin::Twice => 2.0 * band,
904        };
905        let mut odd = vec![0u32; self.loops];
906        for shift in &self.images {
907            let image = [uv[0] + shift, uv[1]];
908            let (x0, y0, x1, y1) = self.grid.range([image[0] - near, image[1] - near, image[0] + near, image[1] + near]);
909            for y in y0..=y1 {
910                for x in x0..=x1 {
911                    for &index in &self.grid.buckets[y * self.grid.width + x] {
912                        let (a, b, _) = self.segments[index];
913                        if segment_foot(image, a, b).0 <= near {
914                            return None;
915                        }
916                    }
917                }
918            }
919            // The lane's own count: crossings of +u, half-open in v, per loop.
920            let (column, row, _, _) = self.grid.range([image[0], image[1], image[0], image[1]]);
921            let mut inside = vec![false; self.loops];
922            for x in column..self.grid.width {
923                for &index in &self.grid.buckets[row * self.grid.width + x] {
924                    let (a, b, ring) = self.segments[index];
925                    if (a[1] > image[1]) == (b[1] > image[1]) {
926                        continue;
927                    }
928                    let crossing = a[0] + (image[1] - a[1]) / (b[1] - a[1]) * (b[0] - a[0]);
929                    if crossing > image[0] && self.grid.range([crossing, image[1], crossing, image[1]]).0 == x {
930                        inside[ring] = !inside[ring];
931                    }
932                }
933            }
934            for (ring, inside) in inside.into_iter().enumerate() {
935                odd[ring] += inside as u32;
936            }
937        }
938        Some(odd.iter().map(|hits| hits % 2).sum::<u32>() % 2 == 1)
939    }
940}
941
942/// A flat face's trim on the paper: its loops closed from the paper samples of
943/// its boundary edges, each segment signed by its loop's orientation (the
944/// loop of largest projected area is the outer one, the rest are holes) and
945/// bucketed on a grid.
946///
947/// A flat face that is not edge-on maps onto the paper one to one, so the view
948/// ray through a paper point meets it inside its trim exactly when the
949/// projected loops wind round the point. The projected loops are not the trim,
950/// and differ from it by at most the band [`PaperTrim::covers`] keeps clear of
951/// them: [`CHORD_MM`], the samples' chords against the exact edge curves, plus
952/// the crossing floor twice — once for an edge curve sitting the B-rep bar off
953/// the trim its pcurves draw (and a loop closed across two edges' ends that
954/// agree only to that bar), once for the band the kernel's trim test counts as
955/// inside.
956///
957/// It answers ONE way. A point the loops wind round by more than the band is
958/// inside the trim, and the kernel's test says so too: within its band of its
959/// chord polygon it answers inside, near the pcurve it takes the pcurve's own
960/// side, and farther out its polygon's count agrees with the pcurve's. A point
961/// the loops do not wind round is NOT reliably outside for the kernel: it
962/// counts anything within its band of its own chords as inside, and a chord
963/// can sit well inside a rim it samples coarsely — measured on `booleanIssue`,
964/// where a hole's circle is sampled at twelve stations and a point 0.023 paper
965/// mm into the hole reads `Boundary`. So "not covered" goes to the trim test in
966/// the carrier's parameters ([`UvTrim`], then the kernel).
967///
968/// This is what made the pass fast: one flat face of `gear-hex-bore-push`
969/// carries 562 boundary edges and 36 530 pcurve control points, and the
970/// kernel's test scans every sample of that trim for every point it is asked
971/// about — 37 of the pass's 43 seconds.
972#[derive(Debug, Clone)]
973struct PaperTrim {
974    /// `(from, to, sign)`: the sign turns every loop to the outer-positive,
975    /// holes-negative orientation.
976    segments: Vec<([f64; 2], [f64; 2], i32)>,
977    /// Each segment, in every cell its box grown by the band touches.
978    grid: Grid,
979    band: f64,
980}
981
982impl PaperTrim {
983    /// `None` when a boundary edge has no samples (a seam has none), or a loop
984    /// does not close on the paper within the bar the B-rep keeps its edges'
985    /// ends to.
986    fn of(solid: &BrepSolid, face: &FaceRecord, paper_of_edge: &HashMap<u64, &[[f64; 2]]>, floors: &Floors) -> Option<PaperTrim> {
987        let band = CHORD_MM + 2.0 * floors.crossing_mm;
988        let gap = 2.0 * floors.crossing_mm + CHORD_MM;
989        let mut rings: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
990        for lp in &face.loops {
991            let mut pieces: Vec<&[[f64; 2]]> = Vec::with_capacity(lp.coedges.len());
992            for coedge in &lp.coedges {
993                match paper_of_edge.get(&coedge.edge_id) {
994                    Some(paper) if paper.len() >= 2 => pieces.push(paper),
995                    _ if solid.edges.iter().any(|edge| edge.id == coedge.edge_id && edge.degenerate) => {}
996                    _ => return None,
997                }
998            }
999            let Some(first) = pieces.first() else { continue };
1000            // Walk the loop, each edge's samples the way round that starts
1001            // nearest the previous one's end; the first is turned to end
1002            // nearest the second.
1003            let ends = |piece: &[[f64; 2]]| (piece[0], piece[piece.len() - 1]);
1004            let first_forward = pieces.get(1).is_none_or(|next| {
1005                let (n0, n1) = ends(next);
1006                let near = |p: [f64; 2]| paper_distance(p, n0).min(paper_distance(p, n1));
1007                near(ends(first).1) <= near(ends(first).0)
1008            });
1009            let mut ring: Vec<[f64; 2]> = if first_forward { first.to_vec() } else { first.iter().rev().copied().collect() };
1010            for piece in &pieces[1..] {
1011                let last = ring[ring.len() - 1];
1012                let (p0, p1) = ends(piece);
1013                let forward = paper_distance(last, p0) <= paper_distance(last, p1);
1014                if paper_distance(last, if forward { p0 } else { p1 }) > gap {
1015                    return None;
1016                }
1017                if forward {
1018                    ring.extend_from_slice(piece);
1019                } else {
1020                    ring.extend(piece.iter().rev());
1021                }
1022            }
1023            if paper_distance(ring[0], ring[ring.len() - 1]) > gap {
1024                return None;
1025            }
1026            rings.push(ring);
1027        }
1028        // Twice the signed area of each ring, and the outer one.
1029        let areas: Vec<f64> = rings
1030            .iter()
1031            .map(|ring| (0..ring.len()).map(|i| {
1032                let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
1033                a[0] * b[1] - b[0] * a[1]
1034            }).sum())
1035            .collect();
1036        let outer = (0..rings.len()).max_by(|&i, &j| areas[i].abs().partial_cmp(&areas[j].abs()).unwrap_or(std::cmp::Ordering::Equal))?;
1037        let mut segments: Vec<([f64; 2], [f64; 2], i32)> = Vec::new();
1038        for (index, ring) in rings.iter().enumerate() {
1039            let turned = if areas[index] < 0.0 { -1 } else { 1 };
1040            let sign = if index == outer { turned } else { -turned };
1041            segments.extend((0..ring.len()).map(|i| (ring[i], ring[(i + 1) % ring.len()], sign)));
1042        }
1043        let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1044        let grown = |(a, b, _): &([f64; 2], [f64; 2], i32)| {
1045            [a[0].min(b[0]) - band, a[1].min(b[1]) - band, a[0].max(b[0]) + band, a[1].max(b[1]) + band]
1046        };
1047        for segment in &segments {
1048            let g = grown(segment);
1049            extent = [extent[0].min(g[0]), extent[1].min(g[1]), extent[2].max(g[2]), extent[3].max(g[3])];
1050        }
1051        let mut grid = Grid::new(extent, segments.len());
1052        for (index, segment) in segments.iter().enumerate() {
1053            grid.insert(grown(segment), index);
1054        }
1055        Some(PaperTrim { segments, grid, band })
1056    }
1057
1058    /// Whether the face covers paper point `p` — its projected loops wind
1059    /// round it — or `None` within the band of them, where only the kernel
1060    /// can say.
1061    fn covers(&self, p: [f64; 2]) -> Option<bool> {
1062        // A segment within the band has its grown box round `p`, so it is in
1063        // `p`'s own cell.
1064        if self.grid.at(p).iter().any(|&index| {
1065            let (a, b, _) = self.segments[index];
1066            segment_foot(p, a, b).0 <= self.band
1067        }) {
1068            return None;
1069        }
1070        // Signed crossings along +x, half-open in y. A segment the ray
1071        // crosses is in the cell of the crossing, and is counted there only.
1072        let (column, row, _, _) = self.grid.range([p[0], p[1], p[0], p[1]]);
1073        let mut winding = 0;
1074        for x in column..self.grid.width {
1075            for &index in &self.grid.buckets[row * self.grid.width + x] {
1076                let (a, b, sign) = self.segments[index];
1077                if (a[1] > p[1]) == (b[1] > p[1]) {
1078                    continue;
1079                }
1080                let crossing = a[0] + (p[1] - a[1]) / (b[1] - a[1]) * (b[0] - a[0]);
1081                if crossing > p[0] && self.grid.range([crossing, p[1], crossing, p[1]]).0 == x {
1082                    winding += if b[1] > p[1] { sign } else { -sign };
1083                }
1084            }
1085        }
1086        Some(winding != 0)
1087    }
1088}
1089
1090/// The uv coincidence band a trim is asked with: the B-rep bar carried into
1091/// the carrier's parameters by its own speed at `uv`.
1092fn trim_band(surface: &NurbsSurface, uv: [f64; 2], bar: f64) -> f64 {
1093    let speed = surface
1094        .derivatives(uv[0], uv[1], 1)
1095        .ok()
1096        .map(|d| d[1][0].length().max(d[0][1].length()))
1097        .unwrap_or(0.0);
1098    if speed > 1e-12 {
1099        bar / speed
1100    } else {
1101        1e-9
1102    }
1103}
1104
1105/// Inside (or on) a face's trim: the kernel's `parameter_point_in_face`,
1106/// unless what the pass has learned about the trim ([`TrimTests`]) answers
1107/// first.
1108fn in_face(face: &FaceRecord, tests: Option<&TrimTests>, uv: [f64; 2], bar: f64) -> bool {
1109    let band = trim_band(&face.surface, uv, bar);
1110    if let Some(tests) = tests {
1111        if tests.reach(face).is_some_and(|reach| reach.outside(uv, band)) {
1112            return false;
1113        }
1114        if let Some(inside) = tests.uv_trim(face).and_then(|trim| trim.covers(uv, band)) {
1115            return inside;
1116        }
1117    }
1118    TRIM_QUERIES.with(|count| count.set(count.get() + 1));
1119    !matches!(
1120        parameter_point_in_face(face, Vec2 { x: uv[0], y: uv[1] }, band),
1121        Ok(PolygonClass::Outside) | Err(_)
1122    )
1123}
1124
1125/// Where in its carrier's parameters a face's trim can put material: the box
1126/// of every loop's pcurve control points — a pcurve with positive weights
1127/// lies in the hull of its own — read modulo the period along a closed
1128/// direction. A point outside that box by more than the band the trim is
1129/// asked with is outside every loop and within the band of none, so the
1130/// kernel's answer there is `Outside` and is not asked for.
1131///
1132/// It is the containment question's cheap half: a silhouette generator of a
1133/// small cylinder patch, or a view ray through its carrier far from the
1134/// patch, is decided without sampling the trim.
1135///
1136/// The box holds for the lanes that answer inside the loops' own polygons: the
1137/// generic scan without the sphere chart and the wrapped horizon.
1138#[derive(Debug, Clone)]
1139struct UvReach {
1140    lo: [f64; 2],
1141    hi: [f64; 2],
1142    period: [Option<f64>; 2],
1143}
1144
1145impl UvReach {
1146    /// `None` where the kernel's lane ([`containment_lane`]) can put material
1147    /// outside the pcurve box: the sphere cap, which holds a pole the loop does
1148    /// not enclose in uv; the sphere chart, which decides on the ball; the seam
1149    /// band, which reads a band of punctures as all material; and the covering
1150    /// rim strip, whose rims need only span the period to within 2%, so a
1151    /// point below both can count one crossing.
1152    fn of(face: &FaceRecord, lane: &ContainmentLane) -> Option<UvReach> {
1153        if !matches!(lane, ContainmentLane::Generic { sphere_chart: false } | ContainmentLane::WrappedHorizon { .. }) {
1154            return None;
1155        }
1156        let surface = &face.surface;
1157        let closed = surface.closed_directions().ok()?;
1158        let (du, dv) = (surface.domain_u().ok()?, surface.domain_v().ok()?);
1159        let mut lo = [f64::INFINITY; 2];
1160        let mut hi = [f64::NEG_INFINITY; 2];
1161        for cp in face.loops.iter().flat_map(|lp| &lp.coedges).flat_map(|coedge| &coedge.pcurve.control_points) {
1162            if !(cp.w > 0.0) {
1163                return None;
1164            }
1165            let (u, v) = (cp.x / cp.w, cp.y / cp.w);
1166            lo = [lo[0].min(u), lo[1].min(v)];
1167            hi = [hi[0].max(u), hi[1].max(v)];
1168        }
1169        if !(lo[0] <= hi[0] && lo[1] <= hi[1]) {
1170            return None;
1171        }
1172        let period = [closed.0.then_some(du[1] - du[0]), closed.1.then_some(dv[1] - dv[0])];
1173        Some(UvReach { lo, hi, period })
1174    }
1175
1176    fn outside(&self, uv: [f64; 2], band: f64) -> bool {
1177        (0..2).any(|k| {
1178            let (lo, hi) = (self.lo[k] - band, self.hi[k] + band);
1179            match self.period[k] {
1180                None => uv[k] < lo || uv[k] > hi,
1181                Some(period) if period > 0.0 && hi - lo < period => {
1182                    uv[k] - ((uv[k] - lo) / period).floor() * period > hi
1183                }
1184                Some(_) => false,
1185            }
1186        })
1187    }
1188}
1189
1190/// The uv of a model point that lies ON `face`'s carrier (within `bar`).
1191fn uv_on(face: &FaceRecord, p: Vec3, bar: f64) -> Option<[f64; 2]> {
1192    let foot = project_point_to_surface(&face.surface, p).ok()?;
1193    (foot.distance <= bar).then_some([foot.u, foot.v])
1194}
1195
1196/// The silhouette curves of one face, trimmed to it.
1197/// The silhouette curves of one face, trimmed to it.
1198fn silhouette_candidates<'a>(
1199    face: &'a FaceRecord,
1200    tests: &TrimTests,
1201    frame: &ViewFrame,
1202    placed: &PlacedView,
1203    floors: &Floors,
1204) -> Vec<Candidate<'a>> {
1205    let surface = &face.surface;
1206    if planar(surface) {
1207        return Vec::new();
1208    }
1209    let dir = v3(frame.forward);
1210    let mut carriers: Vec<(Carrier<'a>, f64, f64, usize)> = Vec::new();
1211    match surface.analytic() {
1212        Some(AnalyticSurface::RuledRevolution { frame: rev, rho0, rho1, height }) => {
1213            // n ∝ h·r(θ) − (ρ1 − ρ0)·axis, so n·d = 0 is A cos θ + B sin θ = C.
1214            let a = height * dir.dot(rev.x_axis);
1215            let b = height * dir.dot(rev.y_axis);
1216            let c = (rho1 - rho0) * dir.dot(rev.axis);
1217            let r = (a * a + b * b).sqrt();
1218            if r > 1e-12 * (height.abs() + 1.0) && c.abs() <= r {
1219                let base = b.atan2(a);
1220                let spread = (c / r).clamp(-1.0, 1.0).acos();
1221                let mut angles = vec![base + spread];
1222                if spread > 1e-12 {
1223                    angles.push(base - spread);
1224                }
1225                for theta in angles {
1226                    let radial = rev.x_axis.scale(theta.cos()).add(rev.y_axis.scale(theta.sin()));
1227                    let start = rev.origin.add(radial.scale(*rho0));
1228                    let end = rev.origin.add(rev.axis.scale(*height)).add(radial.scale(*rho1));
1229                    carriers.push((Carrier::Line { a: start, b: end }, 0.0, 1.0, 1));
1230                }
1231            }
1232        }
1233        Some(AnalyticSurface::Sphere { frame: rev, radius }) => {
1234            let Ok(e1) = dir.perpendicular().and_then(|e| e.normalized()) else { return Vec::new() };
1235            let e2 = dir.cross(e1);
1236            carriers.push((
1237                Carrier::Circle { centre: rev.origin, e1: e1.scale(*radius), e2: e2.scale(*radius) },
1238                0.0,
1239                std::f64::consts::TAU,
1240                16,
1241            ));
1242        }
1243        _ => {
1244            let traced = trace_silhouette(face, dir);
1245            for uv in traced {
1246                let n = uv.len();
1247                if n < 2 {
1248                    continue;
1249                }
1250                carriers.push((Carrier::Traced { surface, uv, dir }, 0.0, (n - 1) as f64, n - 1));
1251            }
1252        }
1253    }
1254    let mut out = Vec::new();
1255    for (carrier, s0, s1, initial) in carriers {
1256        let samples = sample(&carrier, s0, s1, initial, frame, placed);
1257        let spans_found = trimmed_spans(&carrier, &samples, face, tests, floors.bar);
1258        for (lo, hi) in spans_found {
1259            let piece = sample(&carrier, lo, hi, initial_between(&samples, lo, hi), frame, placed);
1260            if piece.len() < 2 || piece.paper_length() < floors.crossing_mm {
1261                continue;
1262            }
1263            let bounds = bounds_of(&piece.paper);
1264            out.push(Candidate {
1265                carrier: carrier.clone(),
1266                samples: piece,
1267                inked: true,
1268                silhouette: true,
1269                edge: None,
1270                ends: None,
1271                bounds,
1272                whole: false,
1273            });
1274        }
1275    }
1276    out
1277}
1278
1279/// How many initial intervals a re-sample of `[lo, hi]` needs: as many as the
1280/// full sampling had there, so the chord test starts no coarser.
1281fn initial_between(samples: &Samples, lo: f64, hi: f64) -> usize {
1282    samples.params.iter().filter(|s| **s > lo && **s < hi).count() + 1
1283}
1284
1285/// The parameter spans of a sampled silhouette that lie on its face, each
1286/// transition pinned by bisection on the exact curve.
1287fn trimmed_spans(carrier: &Carrier, samples: &Samples, face: &FaceRecord, tests: &TrimTests, bar: f64) -> Vec<(f64, f64)> {
1288    // The chord samples, and never fewer than TRIM_STATIONS: a straight
1289    // generator is two chord samples, and a trim it enters and leaves between
1290    // them would be invisible to its ends.
1291    let (s0, s1) = (samples.params[0], *samples.params.last().unwrap());
1292    let mut stations: Vec<f64> = (0..=TRIM_STATIONS).map(|k| s0 + (s1 - s0) * k as f64 / TRIM_STATIONS as f64).collect();
1293    stations.extend(samples.params.iter().copied());
1294    stations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1295    stations.dedup();
1296    let inside = |s: f64| -> bool {
1297        let uv = match carrier {
1298            Carrier::Traced { surface, uv, dir } => traced_at(surface, uv, *dir, s).map(|(_, uv)| uv),
1299            _ => carrier.at(s).and_then(|p| uv_on(face, p, bar)),
1300        };
1301        uv.is_some_and(|uv| in_face(face, Some(tests), uv, bar))
1302    };
1303    let flags: Vec<bool> = stations.iter().map(|s| inside(*s)).collect();
1304    let mut spans = Vec::new();
1305    let mut start: Option<f64> = None;
1306    for i in 0..flags.len() {
1307        if i > 0 && flags[i] != flags[i - 1] {
1308            let (mut lo, mut hi) = (stations[i - 1], stations[i]);
1309            let lo_flag = flags[i - 1];
1310            for _ in 0..BISECTION_STEPS {
1311                let mid = 0.5 * (lo + hi);
1312                if inside(mid) == lo_flag {
1313                    lo = mid;
1314                } else {
1315                    hi = mid;
1316                }
1317            }
1318            let cut = 0.5 * (lo + hi);
1319            if flags[i] {
1320                start = Some(cut);
1321            } else if let Some(s) = start.take() {
1322                spans.push((s, cut));
1323            }
1324        } else if i == 0 && flags[0] {
1325            start = Some(stations[0]);
1326        }
1327    }
1328    if let (Some(s), Some(last)) = (start, stations.last()) {
1329        spans.push((s, *last));
1330    }
1331    spans
1332}
1333
1334/// The zero set of `n · d` over a face's uv box, as uv polylines: marching
1335/// squares on a grid of the carrier's own normals, each crossing pinned on
1336/// its grid edge by bisection, the crossings chained cell to cell.
1337fn trace_silhouette(face: &FaceRecord, dir: Vec3) -> Vec<Vec<[f64; 2]>> {
1338    let surface = &face.surface;
1339    let Some(uv_box) = trim_box(face) else { return Vec::new() };
1340    let spans = |knots: &[f64], degree: usize| knots.len().saturating_sub(2 * degree + 1).max(1);
1341    let nu = (spans(&surface.knots_u, surface.degree_u) * (surface.degree_u + 1) * 2).clamp(12, 96);
1342    let nv = (spans(&surface.knots_v, surface.degree_v) * (surface.degree_v + 1) * 2).clamp(12, 96);
1343    let at = |i: usize, j: usize| {
1344        [
1345            uv_box[0] + (uv_box[2] - uv_box[0]) * i as f64 / nu as f64,
1346            uv_box[1] + (uv_box[3] - uv_box[1]) * j as f64 / nv as f64,
1347        ]
1348    };
1349    let mut values = vec![None; (nu + 1) * (nv + 1)];
1350    for j in 0..=nv {
1351        for i in 0..=nu {
1352            let p = at(i, j);
1353            values[j * (nu + 1) + i] = grazing(surface, dir, p[0], p[1]);
1354        }
1355    }
1356    let value = |i: usize, j: usize| values[j * (nu + 1) + i];
1357    // A crossing on a grid edge, keyed by the edge: (i, j, horizontal).
1358    let mut nodes: HashMap<(usize, usize, bool), [f64; 2]> = HashMap::new();
1359    let mut crossing = |i: usize, j: usize, horizontal: bool| -> Option<(usize, usize, bool)> {
1360        let (i2, j2) = if horizontal { (i + 1, j) } else { (i, j + 1) };
1361        let (g0, g1) = (value(i, j)?, value(i2, j2)?);
1362        if (g0 < 0.0) == (g1 < 0.0) {
1363            return None;
1364        }
1365        let key = (i, j, horizontal);
1366        if !nodes.contains_key(&key) {
1367            let (a, b) = (at(i, j), at(i2, j2));
1368            let (mut lo, mut hi, mut g_lo) = (0.0, 1.0, g0);
1369            for _ in 0..BISECTION_STEPS {
1370                let mid = 0.5 * (lo + hi);
1371                let p = [a[0] + (b[0] - a[0]) * mid, a[1] + (b[1] - a[1]) * mid];
1372                let Some(g) = grazing(surface, dir, p[0], p[1]) else { break };
1373                if (g < 0.0) == (g_lo < 0.0) {
1374                    lo = mid;
1375                    g_lo = g;
1376                } else {
1377                    hi = mid;
1378                }
1379            }
1380            let t = 0.5 * (lo + hi);
1381            nodes.insert(key, [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
1382        }
1383        Some(key)
1384    };
1385    let mut links: Vec<[(usize, usize, bool); 2]> = Vec::new();
1386    for j in 0..nv {
1387        for i in 0..nu {
1388            let edges = [
1389                crossing(i, j, true),
1390                crossing(i + 1, j, false),
1391                crossing(i, j + 1, true),
1392                crossing(i, j, false),
1393            ];
1394            let hits: Vec<_> = edges.iter().flatten().copied().collect();
1395            match hits.len() {
1396                2 => links.push([hits[0], hits[1]]),
1397                4 => {
1398                    // A saddle: pair by the centre's sign, the usual rule.
1399                    let centre = at(i, j);
1400                    let half = [
1401                        centre[0] + (uv_box[2] - uv_box[0]) * 0.5 / nu as f64,
1402                        centre[1] + (uv_box[3] - uv_box[1]) * 0.5 / nv as f64,
1403                    ];
1404                    let g = grazing(surface, dir, half[0], half[1]).unwrap_or(0.0);
1405                    let corner = value(i, j).unwrap_or(0.0);
1406                    if (g < 0.0) == (corner < 0.0) {
1407                        links.push([hits[0], hits[1]]);
1408                        links.push([hits[2], hits[3]]);
1409                    } else {
1410                        links.push([hits[0], hits[3]]);
1411                        links.push([hits[1], hits[2]]);
1412                    }
1413                }
1414                _ => {}
1415            }
1416        }
1417    }
1418    // Chain the links through shared crossings.
1419    let mut incident: HashMap<(usize, usize, bool), Vec<usize>> = HashMap::new();
1420    for (index, link) in links.iter().enumerate() {
1421        incident.entry(link[0]).or_default().push(index);
1422        incident.entry(link[1]).or_default().push(index);
1423    }
1424    let mut used = vec![false; links.len()];
1425    let mut order: Vec<usize> = (0..links.len()).collect();
1426    // Start from an END (a crossing one link touches) so open curves keep
1427    // their ends; what is left is closed.
1428    order.sort_by_key(|&i| {
1429        let open = incident[&links[i][0]].len() == 1 || incident[&links[i][1]].len() == 1;
1430        (!open, i)
1431    });
1432    let mut out = Vec::new();
1433    for start in order {
1434        if used[start] {
1435            continue;
1436        }
1437        used[start] = true;
1438        let [a, b] = links[start];
1439        let (head, mut tail) = if incident[&a].len() == 1 { (a, b) } else { (b, a) };
1440        let mut path = vec![head, tail];
1441        while let Some(next) = incident[&tail].iter().copied().find(|i| !used[*i]) {
1442            used[next] = true;
1443            tail = if links[next][0] == tail { links[next][1] } else { links[next][0] };
1444            path.push(tail);
1445            if tail == head {
1446                break;
1447            }
1448        }
1449        out.push(path.into_iter().map(|key| nodes[&key]).collect());
1450    }
1451    out
1452}
1453
1454// ---------------------------------------------------------------------------
1455// Occluders
1456// ---------------------------------------------------------------------------
1457
1458/// Something that can hide a piece, and its box on the paper.
1459struct Occluder<'a> {
1460    cover: Cover<'a>,
1461    bounds: [f64; 4],
1462    /// Its solid is left whole by the section: it hides from the removed side too.
1463    whole: bool,
1464}
1465
1466/// What an [`Occluder`] is.
1467enum Cover<'a> {
1468    /// A B-rep face: the exact pass.
1469    Face {
1470        face: &'a FaceRecord,
1471        /// `(point, unit normal)` of a flat face, for the closed-form ray hit.
1472        plane: Option<(Vec3, Vec3)>,
1473        /// The carrier's per-SPAN control hulls that meet the trim's uv box,
1474        /// each as its paper box, its projected convex hull and its nearest
1475        /// view depth: a NURBS patch lies inside the hull of the control points
1476        /// of each of its knot spans, and a projection of a convex hull is the
1477        /// convex hull of the projections, so a point outside every hull — or
1478        /// in front of every hull that holds it — is not covered by the face.
1479        /// Much tighter than one hull for a lofted face, and than a box for a
1480        /// slanted one.
1481        spans: Vec<SpanHull>,
1482        /// What the pass has learned about the trim.
1483        tests: &'a TrimTests,
1484        /// A flat face's trim on the paper, when its loops close there.
1485        trim: Option<PaperTrim>,
1486    },
1487    /// A triangle of a display mesh: the mesh approximation
1488    /// ([`visible_mesh_lines`]). Its corners and unit normal.
1489    Triangle { corners: [Vec3; 3], normal: Vec3 },
1490}
1491
1492/// The paper box a face can cover: its boundary and silhouettes when those
1493/// were computed in closed form, otherwise the carrier's control hull (a
1494/// NURBS patch lies inside it, so the box is never short).
1495fn face_bounds(
1496    face: &FaceRecord,
1497    boundary: &[[f64; 4]],
1498    silhouettes: &[[f64; 4]],
1499    frame: &ViewFrame,
1500    placed: &PlacedView,
1501) -> [f64; 4] {
1502    let exact_outline = planar(&face.surface)
1503        || matches!(face.surface.analytic(), Some(AnalyticSurface::RuledRevolution { .. } | AnalyticSurface::Sphere { .. }));
1504    let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1505    let mut grow = |c: [f64; 4]| {
1506        b[0] = b[0].min(c[0]);
1507        b[1] = b[1].min(c[1]);
1508        b[2] = b[2].max(c[2]);
1509        b[3] = b[3].max(c[3]);
1510    };
1511    if exact_outline && !boundary.is_empty() {
1512        for c in boundary.iter().chain(silhouettes) {
1513            grow(*c);
1514        }
1515    } else {
1516        for row in &face.surface.control_points {
1517            for cp in row {
1518                let w = if cp.w != 0.0 { cp.w } else { 1.0 };
1519                let (paper, _) = project(frame, placed, Vec3::new(cp.x / w, cp.y / w, cp.z / w));
1520                grow([paper[0], paper[1], paper[0], paper[1]]);
1521            }
1522        }
1523    }
1524    b
1525}
1526
1527/// One knot span's control hull on the paper.
1528struct SpanHull {
1529    bounds: [f64; 4],
1530    /// The projected control points' convex hull, counter-clockwise.
1531    hull: Vec<[f64; 2]>,
1532    /// The nearest view depth of its control points.
1533    near: f64,
1534}
1535
1536impl SpanHull {
1537    /// Whether `p` is inside the hull or within `pad` of it.
1538    fn holds(&self, p: [f64; 2], pad: f64) -> bool {
1539        let b = self.bounds;
1540        if p[0] < b[0] - pad || p[0] > b[2] + pad || p[1] < b[1] - pad || p[1] > b[3] + pad {
1541            return false;
1542        }
1543        let n = self.hull.len();
1544        if n >= 3 && (0..n).all(|k| {
1545            let (a, c) = (self.hull[k], self.hull[(k + 1) % n]);
1546            (c[0] - a[0]) * (p[1] - a[1]) - (c[1] - a[1]) * (p[0] - a[0]) >= 0.0
1547        }) {
1548            return true;
1549        }
1550        (0..n.max(1)).any(|k| segment_foot(p, self.hull[k], self.hull[(k + 1) % n]).0 <= pad)
1551    }
1552}
1553
1554/// The convex hull of paper points, counter-clockwise (monotone chain).
1555fn convex_hull(mut points: Vec<[f64; 2]>) -> Vec<[f64; 2]> {
1556    points.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1557    points.dedup();
1558    if points.len() < 3 {
1559        return points;
1560    }
1561    let turn = |o: [f64; 2], a: [f64; 2], b: [f64; 2]| (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
1562    let mut hull: Vec<[f64; 2]> = Vec::with_capacity(points.len() + 1);
1563    for pass in 0..2 {
1564        let start = hull.len();
1565        let ordered: Box<dyn Iterator<Item = &[f64; 2]>> =
1566            if pass == 0 { Box::new(points.iter()) } else { Box::new(points.iter().rev()) };
1567        for &p in ordered {
1568            while hull.len() >= start + 2 && turn(hull[hull.len() - 2], hull[hull.len() - 1], p) <= 0.0 {
1569                hull.pop();
1570            }
1571            hull.push(p);
1572        }
1573        hull.pop();
1574    }
1575    hull
1576}
1577
1578/// The control hull on the paper of each knot span of `face`'s carrier, for
1579/// the spans whose parameter interval meets the trim's uv box.
1580fn span_hulls(face: &FaceRecord, frame: &ViewFrame, placed: &PlacedView) -> Vec<SpanHull> {
1581    let surface = &face.surface;
1582    let uv_box = trim_box(face).unwrap_or([f64::NEG_INFINITY, f64::NEG_INFINITY, f64::INFINITY, f64::INFINITY]);
1583    let spans = |knots: &[f64], degree: usize, count: usize, lo: f64, hi: f64| -> Vec<usize> {
1584        // Span i covers [knots[i], knots[i + 1]] and is shaped by control
1585        // points i − degree ..= i.
1586        (degree..count)
1587            .filter(|&i| knots[i + 1] > knots[i] && knots[i + 1] >= lo && knots[i] <= hi)
1588            .collect()
1589    };
1590    let rows = surface.control_points.len();
1591    let columns = surface.control_points.first().map_or(0, Vec::len);
1592    let su = spans(&surface.knots_u, surface.degree_u, rows, uv_box[0], uv_box[2]);
1593    let sv = spans(&surface.knots_v, surface.degree_v, columns, uv_box[1], uv_box[3]);
1594    let mut out = Vec::with_capacity(su.len() * sv.len());
1595    for &i in &su {
1596        for &j in &sv {
1597            let mut b = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1598            let mut near = f64::INFINITY;
1599            let mut points = Vec::with_capacity((surface.degree_u + 1) * (surface.degree_v + 1));
1600            for row in &surface.control_points[i - surface.degree_u..=i] {
1601                for cp in &row[j - surface.degree_v..=j] {
1602                    let w = if cp.w != 0.0 { cp.w } else { 1.0 };
1603                    let (paper, view) = project(frame, placed, Vec3::new(cp.x / w, cp.y / w, cp.z / w));
1604                    b = [b[0].min(paper[0]), b[1].min(paper[1]), b[2].max(paper[0]), b[3].max(paper[1])];
1605                    near = near.min(view[2]);
1606                    points.push(paper);
1607                }
1608            }
1609            out.push(SpanHull { bounds: b, hull: convex_hull(points), near });
1610        }
1611    }
1612    out
1613}
1614
1615/// A uniform bucket grid over paper boxes.
1616#[derive(Debug, Clone)]
1617struct Grid {
1618    origin: [f64; 2],
1619    cell: f64,
1620    width: usize,
1621    height: usize,
1622    buckets: Vec<Vec<usize>>,
1623}
1624
1625impl Grid {
1626    fn new(extent: [f64; 4], count: usize) -> Grid {
1627        let span = (extent[2] - extent[0]).max(extent[3] - extent[1]).max(1e-9);
1628        let per_side = ((count as f64).sqrt().ceil() as usize).clamp(1, 256);
1629        let cell = span / per_side as f64;
1630        let width = (((extent[2] - extent[0]) / cell).ceil() as usize).clamp(1, 512);
1631        let height = (((extent[3] - extent[1]) / cell).ceil() as usize).clamp(1, 512);
1632        Grid { origin: [extent[0], extent[1]], cell, width, height, buckets: vec![Vec::new(); width * height] }
1633    }
1634
1635    fn range(&self, b: [f64; 4]) -> (usize, usize, usize, usize) {
1636        let clamp = |v: f64, n: usize| (v.floor().max(0.0) as usize).min(n - 1);
1637        (
1638            clamp((b[0] - self.origin[0]) / self.cell, self.width),
1639            clamp((b[1] - self.origin[1]) / self.cell, self.height),
1640            clamp((b[2] - self.origin[0]) / self.cell, self.width),
1641            clamp((b[3] - self.origin[1]) / self.cell, self.height),
1642        )
1643    }
1644
1645    fn insert(&mut self, b: [f64; 4], id: usize) {
1646        let (x0, y0, x1, y1) = self.range(b);
1647        for y in y0..=y1 {
1648            for x in x0..=x1 {
1649                self.buckets[y * self.width + x].push(id);
1650            }
1651        }
1652    }
1653
1654    fn at(&self, p: [f64; 2]) -> &[usize] {
1655        let (x, y, _, _) = self.range([p[0], p[1], p[0], p[1]]);
1656        &self.buckets[y * self.width + x]
1657    }
1658}
1659
1660/// Everything the visibility test reads.
1661struct Scene<'a> {
1662    frame: ViewFrame,
1663    placed: &'a PlacedView,
1664    floors: Floors,
1665    occluders: Vec<Occluder<'a>>,
1666    grid: Grid,
1667    /// Nearest view depth of the model, model units, less a margin: where a
1668    /// view ray toward the eye can stop looking.
1669    near: f64,
1670    clip: &'a Clip<'a>,
1671}
1672
1673impl Scene<'_> {
1674    /// Whether a piece whose exact midpoint is `p` is hidden. `hint` is the
1675    /// face that hid the previous piece of the same curve, tried first — the
1676    /// next piece along is usually behind the same face — and is set to the
1677    /// face that hides this one.
1678    fn hidden(&self, p: Vec3, hint: &mut Option<usize>) -> bool {
1679        let (paper, view) = project(&self.frame, self.placed, p);
1680        let bar = self.floors.bar;
1681        if let Clip::Beyond { cap, .. } = self.clip {
1682            if view[2] > bar && inside_region(cap, paper) {
1683                return true;
1684            }
1685        }
1686        let forward = v3(self.frame.forward);
1687        // A hit in front of the piece by more than the depth floor, and (in a
1688        // section) not in the material the cut removed.
1689        // A face of a solid the section leaves whole hides from the removed side too.
1690        let in_front = |hit: Vec3, whole: bool| {
1691            let depth = dot3(sub3(a3(hit), self.frame.target), self.frame.forward);
1692            depth <= view[2] - bar
1693                && !(matches!(self.clip, Clip::Beyond { .. })
1694                    && !whole
1695                    && depth < -bar
1696                    && self.clip.margin(self.frame.to_view(a3(hit))) < -bar)
1697        };
1698        // The ray toward the eye against a flat carrier through `origin`.
1699        let plane_hit = |origin: Vec3, normal: Vec3| -> Option<Vec3> {
1700            let denominator = normal.dot(forward);
1701            if denominator.abs() <= GRAZING_COS {
1702                return None;
1703            }
1704            let lambda = normal.dot(origin.sub(p)) / denominator;
1705            (lambda < -bar).then(|| p.add(forward.scale(lambda)))
1706        };
1707        // A curved face: the ray toward the eye against its carrier, a hit in
1708        // front of the piece and inside the trim. The ray is the same line for
1709        // every face the piece is asked of, so it is made once.
1710        let reach = view[2] - self.near;
1711        let ray = std::cell::OnceCell::new();
1712        let curved_hides = |face: &FaceRecord, tests: &TrimTests, whole: bool| -> bool {
1713            if reach <= bar {
1714                return false;
1715            }
1716            let Some(ray) = ray.get_or_init(|| make_line(p.sub(forward.scale(reach)), p.sub(forward.scale(bar))).ok()) else {
1717                return false;
1718            };
1719            RAY_QUERIES.with(|count| count.set(count.get() + 1));
1720            let fit = KernelTolerances::default().intersection_fit;
1721            let hits = intersect_curve_surface(ray, &face.surface, fit);
1722            hits.is_ok_and(|hits| {
1723                hits.into_iter()
1724                    .filter(|hit| !hit.tangential)
1725                    .any(|hit| in_front(hit.point, whole) && in_face(face, Some(tests), [hit.u, hit.v], bar))
1726            })
1727        };
1728        // A flat face's one hit, in front of the piece, asked of the trim test.
1729        let flat_hides = |face: &FaceRecord, tests: &TrimTests, hit: Vec3| -> bool {
1730            uv_on(face, hit, bar).is_some_and(|uv| in_face(face, Some(tests), uv, bar))
1731        };
1732        // The piece is hidden when ANY occluder hides it, so the order they are
1733        // asked in is free, and it is chosen for cost. The face that hid the
1734        // previous piece is asked first, whole. Then every other face the paper
1735        // or a closed form decides — a triangle, a flat face whose projected
1736        // trim covers the point — then each curved face's ray, and last a flat
1737        // face the paper does not cover: near a trim of thousands of edges that
1738        // question goes to the kernel and costs more than all the others.
1739        let mut curved: Vec<usize> = Vec::new();
1740        let mut flat: Vec<(usize, Vec3)> = Vec::new();
1741        let first = hint.take();
1742        for index in first.into_iter().chain(self.grid.at(paper).iter().copied().filter(|index| Some(*index) != first)) {
1743            let occluder = &self.occluders[index];
1744            let pad = self.floors.crossing_mm + CHORD_MM;
1745            let holds = |b: &[f64; 4]| {
1746                paper[0] >= b[0] - pad && paper[0] <= b[2] + pad && paper[1] >= b[1] - pad && paper[1] <= b[3] + pad
1747            };
1748            if !holds(&occluder.bounds) {
1749                continue;
1750            }
1751            let hides = match &occluder.cover {
1752                Cover::Triangle { corners, normal } => {
1753                    plane_hit(corners[0], *normal).is_some_and(|hit| in_front(hit, occluder.whole) && in_triangle(corners, *normal, hit, bar))
1754                }
1755                Cover::Face { face, plane, spans, tests, trim } => {
1756                    if !spans.iter().any(|span| span.near <= view[2] - bar && span.holds(paper, pad)) {
1757                        continue;
1758                    }
1759                    let asked_first = Some(index) == first;
1760                    match plane {
1761                        Some((origin, normal)) => {
1762                            let Some(hit) = plane_hit(*origin, *normal).filter(|hit| in_front(*hit, occluder.whole)) else { continue };
1763                            if trim.as_ref().and_then(|trim| trim.covers(paper)) == Some(true) {
1764                                true
1765                            } else if asked_first {
1766                                flat_hides(face, tests, hit)
1767                            } else {
1768                                flat.push((index, hit));
1769                                continue;
1770                            }
1771                        }
1772                        None if asked_first => curved_hides(face, tests, occluder.whole),
1773                        None => {
1774                            curved.push(index);
1775                            continue;
1776                        }
1777                    }
1778                }
1779            };
1780            if hides {
1781                *hint = Some(index);
1782                return true;
1783            }
1784        }
1785        let face_of = |index: usize| match &self.occluders[index].cover {
1786            Cover::Face { face, tests, .. } => Some((*face, tests)),
1787            Cover::Triangle { .. } => None,
1788        };
1789        let hider = curved
1790            .into_iter()
1791            .find(|&index| face_of(index).is_some_and(|(face, tests)| curved_hides(face, tests, self.occluders[index].whole)))
1792            .or_else(|| {
1793                flat.into_iter()
1794                    .find(|&(index, hit)| face_of(index).is_some_and(|(face, tests)| flat_hides(face, tests, hit)))
1795                    .map(|(index, _)| index)
1796            });
1797        if hider.is_some() {
1798            *hint = hider;
1799        }
1800        hider.is_some()
1801    }
1802}
1803
1804/// Whether `hit`, a point in the plane of the triangle `corners` (unit normal
1805/// `normal`), is inside it or within `bar` of it — the band a face's trim is
1806/// asked with ([`in_face`]), so a hit on the edge two triangles share is
1807/// inside both rather than in the crack between them.
1808fn in_triangle(corners: &[Vec3; 3], normal: Vec3, hit: Vec3, bar: f64) -> bool {
1809    (0..3).all(|k| {
1810        let (a, b) = (corners[k], corners[(k + 1) % 3]);
1811        let side = b.sub(a);
1812        let length = side.length();
1813        length <= 0.0 || side.cross(hit.sub(a)).dot(normal) / length >= -bar
1814    })
1815}
1816
1817fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
1818    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
1819}
1820
1821fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
1822    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
1823}
1824
1825/// Even-odd crossing count of a paper point against a set of polylines —
1826/// the cut region of a section.
1827pub fn inside_region(region: &[Vec<[f64; 2]>], p: [f64; 2]) -> bool {
1828    let mut inside = false;
1829    for line in region {
1830        for w in line.windows(2) {
1831            let (a, b) = (w[0], w[1]);
1832            if (a[1] <= p[1]) == (b[1] <= p[1]) {
1833                continue;
1834            }
1835            let x = a[0] + (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]);
1836            if x > p[0] {
1837                inside = !inside;
1838            }
1839        }
1840    }
1841    inside
1842}
1843
1844// ---------------------------------------------------------------------------
1845// The pass
1846// ---------------------------------------------------------------------------
1847
1848/// The visible model lines of `solids` through `frame` at `placed`'s scale,
1849/// restricted to `clip`.
1850pub fn visible_edges(solids: &[&BrepSolid], frame: &ViewFrame, placed: &PlacedView, clip: &Clip) -> HlrResult {
1851    let trims: Vec<SolidTrims> = solids.iter().map(|solid| SolidTrims::of(solid)).collect();
1852    let trims: Vec<&SolidTrims> = trims.iter().collect();
1853    visible_edges_with(solids, &trims, frame, placed, clip)
1854}
1855
1856/// [`visible_edges`] with each solid's [`SolidTrims`] kept from before.
1857pub fn visible_edges_with(
1858    solids: &[&BrepSolid],
1859    trims: &[&SolidTrims],
1860    frame: &ViewFrame,
1861    placed: &PlacedView,
1862    clip: &Clip,
1863) -> HlrResult {
1864    TRIM_QUERIES.with(|count| count.set(0));
1865    RAY_QUERIES.with(|count| count.set(0));
1866    let floors = Floors::of(placed.scale);
1867    let mut candidates: Vec<Candidate> = Vec::new();
1868    // Per face: the paper boxes of its boundary edges and its silhouettes.
1869    let mut face_boxes: HashMap<(usize, usize, usize), (Vec<[f64; 4]>, Vec<[f64; 4]>)> = HashMap::new();
1870    // Per flat face: its trim on the paper, from its boundary edges' samples.
1871    let mut paper_trims: HashMap<(usize, usize, usize), Option<PaperTrim>> = HashMap::new();
1872    for (solid_index, solid) in solids.iter().enumerate() {
1873        let owners = edge_faces(solid);
1874        let edges = edge_candidates(solid_index, solid, frame, placed, &floors);
1875        for candidate in &edges {
1876            let Some((_, edge_id)) = candidate.edge else { continue };
1877            for (shell, face) in owners.get(&edge_id).into_iter().flatten() {
1878                face_boxes.entry((solid_index, *shell, *face)).or_default().0.push(candidate.bounds);
1879            }
1880        }
1881        let paper_of_edge: HashMap<u64, &[[f64; 2]]> =
1882            edges.iter().filter_map(|c| c.edge.map(|(_, id)| (id, c.samples.paper.as_slice()))).collect();
1883        let mut silhouettes: Vec<Candidate> = Vec::new();
1884        for (shell_index, shell) in solid.shells.iter().enumerate() {
1885            for (face_index, face) in shell.faces.iter().enumerate() {
1886                let found = silhouette_candidates(face, &trims[solid_index].faces[shell_index][face_index], frame, placed, &floors);
1887                let entry = face_boxes.entry((solid_index, shell_index, face_index)).or_default();
1888                entry.1.extend(found.iter().map(|c| c.bounds));
1889                silhouettes.extend(found);
1890                if planar(&face.surface) {
1891                    paper_trims.insert((solid_index, shell_index, face_index), PaperTrim::of(solid, face, &paper_of_edge, &floors));
1892                }
1893            }
1894        }
1895        let whole = clip.whole(solid_index);
1896        candidates.extend(edges.into_iter().chain(silhouettes).map(|c| Candidate { whole, ..c }));
1897    }
1898    let mut occluders: Vec<Occluder> = Vec::new();
1899    let mut near = f64::INFINITY;
1900    let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
1901    for (solid_index, solid) in solids.iter().enumerate() {
1902        for (shell_index, shell) in solid.shells.iter().enumerate() {
1903            for (face_index, face) in shell.faces.iter().enumerate() {
1904                let (boundary, silhouettes) =
1905                    face_boxes.get(&(solid_index, shell_index, face_index)).cloned().unwrap_or_default();
1906                let bounds = face_bounds(face, &boundary, &silhouettes, frame, placed);
1907                if !(bounds[0] <= bounds[2]) {
1908                    continue;
1909                }
1910                let spans = span_hulls(face, frame, placed);
1911                near = spans.iter().fold(near, |near, span| near.min(span.near));
1912                extent = [
1913                    extent[0].min(bounds[0]),
1914                    extent[1].min(bounds[1]),
1915                    extent[2].max(bounds[2]),
1916                    extent[3].max(bounds[3]),
1917                ];
1918                let plane = if planar(&face.surface) {
1919                    let du = face.surface.domain_u().unwrap_or([0.0, 1.0]);
1920                    let dv = face.surface.domain_v().unwrap_or([0.0, 1.0]);
1921                    let (u, v) = (0.5 * (du[0] + du[1]), 0.5 * (dv[0] + dv[1]));
1922                    match (face.surface.evaluate(u, v), face.surface.normal(u, v)) {
1923                        (Ok(origin), Ok(normal)) => Some((origin, normal)),
1924                        _ => None,
1925                    }
1926                } else {
1927                    None
1928                };
1929                let trim = paper_trims.remove(&(solid_index, shell_index, face_index)).flatten();
1930                let tests = &trims[solid_index].faces[shell_index][face_index];
1931                occluders.push(Occluder { cover: Cover::Face { face, plane, spans, tests, trim }, bounds, whole: clip.whole(solid_index) });
1932            }
1933        }
1934    }
1935    decide(candidates, occluders, extent, near, frame, placed, clip)
1936}
1937
1938/// The shared back half of both passes: split every candidate where the
1939/// projection can change what is in front of it, decide each piece against
1940/// `occluders`, and join the visible pieces into runs.
1941fn decide(
1942    candidates: Vec<Candidate>,
1943    occluders: Vec<Occluder>,
1944    extent: [f64; 4],
1945    near: f64,
1946    frame: &ViewFrame,
1947    placed: &PlacedView,
1948    clip: &Clip,
1949) -> HlrResult {
1950    let floors = Floors::of(placed.scale);
1951    let mut result = HlrResult::default();
1952    if occluders.is_empty() {
1953        return result;
1954    }
1955    let mut grid = Grid::new(extent, occluders.len());
1956    for (index, occluder) in occluders.iter().enumerate() {
1957        grid.insert(occluder.bounds, index);
1958    }
1959    let scene = Scene {
1960        frame: *frame,
1961        placed,
1962        floors,
1963        occluders,
1964        grid,
1965        near: near - floors.bar * 10.0 - 1.0,
1966        clip,
1967    };
1968
1969    let stations = split_parameters(&candidates, &scene);
1970    result.inked = candidates.iter().filter(|c| c.inked).count();
1971    result.silhouettes = candidates.iter().filter(|c| c.silhouette).count();
1972
1973    // How many INKED edges meet at each vertex — the model's topology, not
1974    // what survived visibility: a silhouette corner of a box is where two
1975    // visible edges and a hidden one meet, and it stays a corner.
1976    let mut valence: HashMap<VertexKey, usize> = HashMap::new();
1977    for candidate in candidates.iter().filter(|c| c.inked) {
1978        if let Some((start, end)) = candidate.ends {
1979            *valence.entry(start).or_default() += 1;
1980            *valence.entry(end).or_default() += 1;
1981        }
1982    }
1983    let mut runs: Vec<Run> = Vec::new();
1984    for (index, candidate) in candidates.iter().enumerate() {
1985        if !candidate.inked {
1986            continue;
1987        }
1988        let pieces = pieces_of(candidate, &stations[index], &scene);
1989        result.pieces += pieces.len();
1990        runs.extend(runs_of(candidate, &pieces, &scene));
1991    }
1992    result.runs = chain_runs(runs, &valence, floors.crossing_mm);
1993    result.trim_queries = TRIM_QUERIES.with(|count| count.get());
1994    result.ray_queries = RAY_QUERIES.with(|count| count.get());
1995    result
1996}
1997
1998// ---------------------------------------------------------------------------
1999// The mesh approximation
2000// ---------------------------------------------------------------------------
2001
2002/// One solid's DISPLAY, in world coordinates: what a placement is drawn from
2003/// while its B-rep has not reached the scene.
2004#[derive(Debug, Clone, Default)]
2005pub struct MeshSolid {
2006    pub positions: Vec<[f64; 3]>,
2007    pub triangles: Vec<[u32; 3]>,
2008    /// Per triangle, the display face it tessellates.
2009    pub faces: Vec<u32>,
2010    /// Every drawn display edge — each B-rep edge the tessellator emitted.
2011    pub edges: Vec<MeshEdge>,
2012}
2013
2014/// One display edge of a [`MeshSolid`]: its polyline, and the topology
2015/// vertices it starts and ends on — where runs chain, as the exact pass chains
2016/// through the B-rep's own `start_vertex_id` / `end_vertex_id`. `None` when
2017/// either end is not recoverably one vertex.
2018#[derive(Debug, Clone, Default)]
2019pub struct MeshEdge {
2020    pub points: Vec<[f64; 3]>,
2021    pub ends: Option<(u64, u64)>,
2022}
2023
2024/// The model lines of `meshes` through `frame` at `placed`'s scale, restricted
2025/// to `clip` — the STATED APPROXIMATION a placement draws while the exact
2026/// solids are not in the scene (their topology request is in flight, or the
2027/// registry could not clone a handle). It is the analytic pass's own split,
2028/// visibility and run code on different inputs:
2029///
2030/// - **candidates** are every display edge (each B-rep edge's tessellated
2031///   polyline, so a round's tangent lines are drawn like any other edge)
2032///   except a SEAM — an edge whose every chord is a mesh edge with the same
2033///   face's triangles on both sides — and the display mesh's SILHOUETTES, the
2034///   mesh edges inside one face whose two triangles face opposite ways along
2035///   the view;
2036/// - **occluders** are the display triangles, each hit in closed form.
2037///
2038/// What it gives up is what the display gave up: a curve is its
2039/// tessellation's chords, and an edge between two fragments of one surface is
2040/// drawn, because nothing in a display tells a split from a tangent line.
2041pub fn visible_mesh_lines(meshes: &[MeshSolid], frame: &ViewFrame, placed: &PlacedView, clip: &Clip) -> HlrResult {
2042    TRIM_QUERIES.with(|count| count.set(0));
2043    RAY_QUERIES.with(|count| count.set(0));
2044    let forward = v3(frame.forward);
2045    let mut candidates: Vec<Candidate> = Vec::new();
2046    let mut occluders: Vec<Occluder> = Vec::new();
2047    let mut near = f64::INFINITY;
2048    let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
2049    for (solid_index, mesh) in meshes.iter().enumerate() {
2050        let points: Vec<Vec3> = mesh.positions.iter().map(|p| v3(*p)).collect();
2051        let welded = WeldedMesh::of(mesh, &points, forward);
2052        for edge in &mesh.edges {
2053            let chain: Vec<Vec3> = edge.points.iter().map(|p| v3(*p)).collect();
2054            if welded.seam(&chain) {
2055                continue;
2056            }
2057            // Runs chain through the topology vertices the edge runs between —
2058            // the model's adjacency, never the distance between two ends.
2059            if let Some(candidate) = polyline_candidate(chain, false, frame, placed) {
2060                candidates.push(Candidate {
2061                    ends: edge.ends.map(|(start, end)| ((solid_index, start), (solid_index, end))),
2062                    whole: clip.whole(solid_index),
2063                    ..candidate
2064                });
2065            }
2066        }
2067        for chain in welded.silhouettes() {
2068            candidates.extend(polyline_candidate(chain, true, frame, placed).map(|c| Candidate { whole: clip.whole(solid_index), ..c }));
2069        }
2070        for triangle in &mesh.triangles {
2071            let Some(corners) = triangle.iter().map(|&i| points.get(i as usize).copied()).collect::<Option<Vec<Vec3>>>() else {
2072                continue;
2073            };
2074            let corners = [corners[0], corners[1], corners[2]];
2075            let Ok(normal) = corners[1].sub(corners[0]).cross(corners[2].sub(corners[0])).normalized() else {
2076                continue;
2077            };
2078            let mut bounds = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
2079            for corner in corners {
2080                let (paper, view) = project(frame, placed, corner);
2081                bounds = [bounds[0].min(paper[0]), bounds[1].min(paper[1]), bounds[2].max(paper[0]), bounds[3].max(paper[1])];
2082                near = near.min(view[2]);
2083            }
2084            extent = [extent[0].min(bounds[0]), extent[1].min(bounds[1]), extent[2].max(bounds[2]), extent[3].max(bounds[3])];
2085            occluders.push(Occluder { cover: Cover::Triangle { corners, normal }, bounds, whole: clip.whole(solid_index) });
2086        }
2087    }
2088    decide(candidates, occluders, extent, near, frame, placed, clip)
2089}
2090
2091/// An inked candidate on a model polyline (a display edge, a mesh silhouette
2092/// chain), or `None` when it has no length.
2093fn polyline_candidate<'a>(chain: Vec<Vec3>, silhouette: bool, frame: &ViewFrame, placed: &PlacedView) -> Option<Candidate<'a>> {
2094    if chain.len() < 2 {
2095        return None;
2096    }
2097    let last = (chain.len() - 1) as f64;
2098    let intervals = chain.len() - 1;
2099    let carrier = Carrier::Polyline(chain);
2100    let samples = sample(&carrier, 0.0, last, intervals, frame, placed);
2101    if samples.len() < 2 || samples.paper_length() <= 0.0 {
2102        return None;
2103    }
2104    let bounds = bounds_of(&samples.paper);
2105    Some(Candidate { carrier, samples, inked: true, silhouette, edge: None, ends: None, bounds, whole: false })
2106}
2107
2108/// A display mesh welded by its exact coordinates — the tessellator writes a
2109/// point its triangles share as one value, whatever copies the shading split
2110/// it into — with the faces and facings of the triangles on each mesh edge.
2111struct WeldedMesh {
2112    index: HashMap<[u64; 3], u32>,
2113    position: Vec<Vec3>,
2114    /// Per welded mesh edge: `(face, faces the eye)` of each triangle on it.
2115    owners: HashMap<[u32; 2], Vec<(u32, bool)>>,
2116}
2117
2118impl WeldedMesh {
2119    fn of(mesh: &MeshSolid, points: &[Vec3], forward: Vec3) -> WeldedMesh {
2120        let key = |p: Vec3| [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
2121        let mut index: HashMap<[u64; 3], u32> = HashMap::new();
2122        let mut position = Vec::new();
2123        let welded: Vec<u32> = points
2124            .iter()
2125            .map(|p| {
2126                *index.entry(key(*p)).or_insert_with(|| {
2127                    position.push(*p);
2128                    (position.len() - 1) as u32
2129                })
2130            })
2131            .collect();
2132        let mut owners: HashMap<[u32; 2], Vec<(u32, bool)>> = HashMap::new();
2133        for (t, triangle) in mesh.triangles.iter().enumerate() {
2134            if triangle.iter().any(|&i| i as usize >= points.len()) {
2135                continue;
2136            }
2137            let [a, b, c] = [points[triangle[0] as usize], points[triangle[1] as usize], points[triangle[2] as usize]];
2138            let facing = b.sub(a).cross(c.sub(a)).dot(forward);
2139            if facing == 0.0 {
2140                continue;
2141            }
2142            let face = mesh.faces.get(t).copied().unwrap_or(u32::MAX);
2143            let w = [welded[triangle[0] as usize], welded[triangle[1] as usize], welded[triangle[2] as usize]];
2144            for k in 0..3 {
2145                let (i, j) = (w[k], w[(k + 1) % 3]);
2146                if i != j {
2147                    owners.entry([i.min(j), i.max(j)]).or_default().push((face, facing < 0.0));
2148                }
2149            }
2150        }
2151        WeldedMesh { index, position, owners }
2152    }
2153
2154    /// The faces on the mesh edge between two model points, when both are
2155    /// mesh vertices joined by one.
2156    fn faces_on(&self, a: Vec3, b: Vec3) -> Option<&[(u32, bool)]> {
2157        let key = |p: Vec3| [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
2158        let (i, j) = (*self.index.get(&key(a))?, *self.index.get(&key(b))?);
2159        self.owners.get(&[i.min(j), i.max(j)]).map(Vec::as_slice)
2160    }
2161
2162    /// Whether a display edge is a SEAM: every chord of it a mesh edge with
2163    /// two triangles of ONE face on it — the surface meeting itself, not a
2164    /// line of the model. An edge whose chords are not mesh edges is drawn.
2165    fn seam(&self, chain: &[Vec3]) -> bool {
2166        chain.windows(2).all(|w| {
2167            self.faces_on(w[0], w[1]).is_some_and(|faces| faces.len() == 2 && faces[0].0 == faces[1].0)
2168        })
2169    }
2170
2171    /// The silhouette chains: every mesh edge whose two triangles belong to
2172    /// ONE display face and face opposite ways along the view, chained through
2173    /// vertices exactly two of them meet at. An edge between two faces is a
2174    /// display edge already.
2175    fn silhouettes(&self) -> Vec<Vec<Vec3>> {
2176        let mut segments: Vec<[u32; 2]> = self
2177            .owners
2178            .iter()
2179            .filter(|(_, list)| list.len() == 2 && list[0].0 == list[1].0 && list[0].1 != list[1].1)
2180            .map(|(edge, _)| *edge)
2181            .collect();
2182        // HashMap order is not a drawing's business.
2183        segments.sort_unstable();
2184        let mut incident: HashMap<u32, Vec<usize>> = HashMap::new();
2185        for (i, segment) in segments.iter().enumerate() {
2186            incident.entry(segment[0]).or_default().push(i);
2187            incident.entry(segment[1]).or_default().push(i);
2188        }
2189        let valence = |v: u32| incident.get(&v).map_or(0, Vec::len);
2190        let mut used = vec![false; segments.len()];
2191        let mut starts: Vec<usize> = (0..segments.len()).collect();
2192        // From a chain END first, so an open chain keeps its ends.
2193        starts.sort_by_key(|&i| (valence(segments[i][0]) == 2 && valence(segments[i][1]) == 2, i));
2194        let mut out = Vec::new();
2195        for start in starts {
2196            if used[start] {
2197                continue;
2198            }
2199            used[start] = true;
2200            let segment = segments[start];
2201            let (head, mut tail) = if valence(segment[0]) != 2 { (segment[0], segment[1]) } else { (segment[1], segment[0]) };
2202            let mut path = vec![head, tail];
2203            while valence(tail) == 2 && tail != head {
2204                let Some(next) = incident[&tail].iter().copied().find(|i| !used[*i]) else { break };
2205                used[next] = true;
2206                tail = if segments[next][0] == tail { segments[next][1] } else { segments[next][0] };
2207                path.push(tail);
2208            }
2209            out.push(path.into_iter().map(|v| self.position[v as usize]).collect());
2210        }
2211        out
2212    }
2213}
2214
2215/// Every place a candidate must be split, as sorted curve parameters: where
2216/// another candidate crosses it or ends on it, and where it leaves the clip.
2217fn split_parameters(candidates: &[Candidate], scene: &Scene) -> Vec<Vec<f64>> {
2218    let floors = scene.floors;
2219    let mut splits: Vec<Vec<f64>> = vec![Vec::new(); candidates.len()];
2220    // The segment grid.
2221    let mut extent = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
2222    let mut count = 0;
2223    for c in candidates {
2224        extent = [extent[0].min(c.bounds[0]), extent[1].min(c.bounds[1]), extent[2].max(c.bounds[2]), extent[3].max(c.bounds[3])];
2225        count += c.samples.len();
2226    }
2227    if !(extent[0] <= extent[2]) {
2228        return splits;
2229    }
2230    let mut grid = Grid::new(extent, count.max(1));
2231    let mut segments: Vec<(usize, usize)> = Vec::new();
2232    for (ci, c) in candidates.iter().enumerate() {
2233        for si in 0..c.samples.len().saturating_sub(1) {
2234            let (a, b) = (c.samples.paper[si], c.samples.paper[si + 1]);
2235            let pad = floors.crossing_mm + CHORD_MM;
2236            grid.insert(
2237                [a[0].min(b[0]) - pad, a[1].min(b[1]) - pad, a[0].max(b[0]) + pad, a[1].max(b[1]) + pad],
2238                segments.len(),
2239            );
2240            segments.push((ci, si));
2241        }
2242    }
2243    let param_at = |ci: usize, si: usize, t: f64| {
2244        let params = &candidates[ci].samples.params;
2245        params[si] + (params[si + 1] - params[si]) * t
2246    };
2247    let tolerance = floors.crossing_mm + CHORD_MM;
2248    let mut seen: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
2249    for bucket in &grid.buckets {
2250        for (k, &first) in bucket.iter().enumerate() {
2251            for &second in &bucket[k + 1..] {
2252                let (lo, hi) = (first.min(second), first.max(second));
2253                let ((ca, sa), (cb, sb)) = (segments[lo], segments[hi]);
2254                if ca == cb && sa.abs_diff(sb) <= 1 {
2255                    continue;
2256                }
2257                if !candidates[ca].inked && !candidates[cb].inked {
2258                    continue;
2259                }
2260                if !seen.insert((lo, hi)) {
2261                    continue;
2262                }
2263                let pa = &candidates[ca].samples.paper;
2264                let pb = &candidates[cb].samples.paper;
2265                let vec2 = |p: [f64; 2]| Vec2 { x: p[0], y: p[1] };
2266                if let Some([ta, tb]) = segment_intersection(vec2(pa[sa]), vec2(pa[sa + 1]), vec2(pb[sb]), vec2(pb[sb + 1]), tolerance) {
2267                    splits[ca].push(param_at(ca, sa, ta));
2268                    splits[cb].push(param_at(cb, sb, tb));
2269                }
2270                // An END of one on the other, including along a collinear
2271                // overlap the crossing test does not report.
2272                for (from, from_seg, onto, onto_seg) in [(ca, sa, cb, sb), (cb, sb, ca, sa)] {
2273                    let fp = &candidates[from].samples.paper;
2274                    let op = &candidates[onto].samples.paper;
2275                    let last = fp.len() - 1;
2276                    for (end_seg, end) in [(0usize, fp[0]), (last - 1, fp[last])] {
2277                        if from_seg != end_seg {
2278                            continue;
2279                        }
2280                        let (distance, t) = segment_foot(end, op[onto_seg], op[onto_seg + 1]);
2281                        if distance <= tolerance {
2282                            splits[onto].push(param_at(onto, onto_seg, t));
2283                        }
2284                    }
2285                }
2286            }
2287        }
2288    }
2289    // The clip's own boundary (a solid the section leaves whole crosses it uncut).
2290    for (ci, c) in candidates.iter().enumerate() {
2291        if !c.inked || c.whole {
2292            continue;
2293        }
2294        let side = |view: [f64; 3]| -> f64 {
2295            match scene.clip {
2296                Clip::Whole => 1.0,
2297                Clip::Beyond { .. } => scene.clip.margin(view),
2298                Clip::Circle { radius } => radius - (view[0] * view[0] + view[1] * view[1]).sqrt(),
2299            }
2300        };
2301        if matches!(scene.clip, Clip::Whole) {
2302            continue;
2303        }
2304        for i in 1..c.samples.len() {
2305            let (g0, g1) = (side(c.samples.view[i - 1]), side(c.samples.view[i]));
2306            if (g0 < 0.0) == (g1 < 0.0) {
2307                continue;
2308            }
2309            let (mut lo, mut hi) = (c.samples.params[i - 1], c.samples.params[i]);
2310            for _ in 0..BISECTION_STEPS {
2311                let mid = 0.5 * (lo + hi);
2312                let Some(p) = c.carrier.at(mid) else { break };
2313                let g = side(scene.frame.to_view(a3(p)));
2314                if (g < 0.0) == (g0 < 0.0) {
2315                    lo = mid;
2316                } else {
2317                    hi = mid;
2318                }
2319            }
2320            splits[ci].push(0.5 * (lo + hi));
2321        }
2322    }
2323    for list in &mut splits {
2324        list.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2325    }
2326    splits
2327}
2328
2329/// One decided piece of a candidate: its parameter span and its paper ends.
2330struct Piece {
2331    lo: f64,
2332    hi: f64,
2333    visible: bool,
2334}
2335
2336/// Cut a candidate at its split parameters (merging any closer together on
2337/// the paper than the crossing floor) and decide each piece.
2338fn pieces_of(candidate: &Candidate, splits: &[f64], scene: &Scene) -> Vec<Piece> {
2339    let samples = &candidate.samples;
2340    let (s0, s1) = (samples.params[0], *samples.params.last().unwrap());
2341    let mut cuts = vec![s0];
2342    let paper_at = |s: f64| candidate.carrier.at(s).map(|p| project(&scene.frame, scene.placed, p).0);
2343    let mut last_paper = samples.paper[0];
2344    for &s in splits {
2345        if s <= s0 || s >= s1 {
2346            continue;
2347        }
2348        let Some(p) = paper_at(s) else { continue };
2349        if paper_distance(p, last_paper) < scene.floors.crossing_mm {
2350            continue;
2351        }
2352        cuts.push(s);
2353        last_paper = p;
2354    }
2355    let end_paper = *samples.paper.last().unwrap();
2356    if cuts.len() > 1 && paper_distance(last_paper, end_paper) < scene.floors.crossing_mm {
2357        cuts.pop();
2358    }
2359    cuts.push(s1);
2360    let mut pieces = Vec::with_capacity(cuts.len() - 1);
2361    let mut hint = None;
2362    for w in cuts.windows(2) {
2363        let (lo, hi) = (w[0], w[1]);
2364        let Some(mid) = candidate.carrier.at(0.5 * (lo + hi)) else { continue };
2365        let view = scene.frame.to_view(a3(mid));
2366        let kept = match scene.clip {
2367            Clip::Whole => true,
2368            Clip::Beyond { .. } => candidate.whole || scene.clip.margin(view) >= -scene.floors.bar,
2369            Clip::Circle { radius } => (view[0] * view[0] + view[1] * view[1]).sqrt() <= radius + scene.floors.bar,
2370        };
2371        let visible = kept && !scene.hidden(mid, &mut hint);
2372        pieces.push(Piece { lo, hi, visible });
2373    }
2374    pieces
2375}
2376
2377/// A visible run on the paper, with the B-rep vertices it touches (for
2378/// chaining through a vertex two inked edges share).
2379struct Run {
2380    points: Vec<[f64; 2]>,
2381    start: Option<VertexKey>,
2382    end: Option<VertexKey>,
2383}
2384
2385/// The visible runs of one candidate: maximal chains of consecutive visible
2386/// pieces, wrapped round the seam of a closed curve. A run keeps its own ends
2387/// and the SAMPLES strictly between them, so a straight edge is two points.
2388fn runs_of(candidate: &Candidate, pieces: &[Piece], scene: &Scene) -> Vec<Run> {
2389    let samples = &candidate.samples;
2390    let closed = samples.len() > 2
2391        && paper_distance(samples.paper[0], *samples.paper.last().unwrap()) < scene.floors.crossing_mm
2392        && samples.points[0].sub(*samples.points.last().unwrap()).length() <= scene.floors.bar;
2393    let mut spans: Vec<(f64, f64, bool, bool)> = Vec::new(); // (lo, hi, touches start, touches end)
2394    let mut i = 0;
2395    while i < pieces.len() {
2396        if !pieces[i].visible {
2397            i += 1;
2398            continue;
2399        }
2400        let first = i;
2401        while i < pieces.len() && pieces[i].visible {
2402            i += 1;
2403        }
2404        spans.push((pieces[first].lo, pieces[i - 1].hi, first == 0, i == pieces.len()));
2405    }
2406    let (s0, s1) = (samples.params[0], *samples.params.last().unwrap());
2407    // A closed curve visible across its seam is one run, not two.
2408    let mut wrapped: Option<(f64, f64)> = None;
2409    if closed && spans.len() >= 2 && spans[0].2 && spans.last().unwrap().3 {
2410        let head = spans.remove(0);
2411        let tail = spans.pop().unwrap();
2412        wrapped = Some((tail.0, head.1));
2413    }
2414    let point_at = |s: f64| candidate.carrier.at(s).map(|p| project(&scene.frame, scene.placed, p).0);
2415    let mut out = Vec::new();
2416    let mut emit = |pieces: &[(f64, f64)], start: Option<VertexKey>, end: Option<VertexKey>| {
2417        let mut points = Vec::new();
2418        for (k, &(lo, hi)) in pieces.iter().enumerate() {
2419            if k == 0 {
2420                if let Some(p) = point_at(lo) {
2421                    points.push(p);
2422                }
2423            }
2424            for (index, s) in samples.params.iter().enumerate() {
2425                if *s > lo && *s < hi {
2426                    points.push(samples.paper[index]);
2427                }
2428            }
2429            if let Some(p) = point_at(hi) {
2430                points.push(p);
2431            }
2432        }
2433        if points.len() >= 2 {
2434            out.push(Run { points, start, end });
2435        }
2436    };
2437    if let Some((lo, hi)) = wrapped {
2438        emit(&[(lo, s1), (s0, hi)], None, None);
2439    }
2440    for (lo, hi, at_start, at_end) in spans {
2441        let ends = candidate.ends;
2442        let whole_closed = closed && at_start && at_end;
2443        emit(
2444            &[(lo, hi)],
2445            if at_start && !whole_closed { ends.map(|e| e.0) } else { None },
2446            if at_end && !whole_closed { ends.map(|e| e.1) } else { None },
2447        );
2448    }
2449    out
2450}
2451
2452/// Chain runs through every vertex exactly two INKED edges meet at — the
2453/// mesh pass's rule read on the B-rep's own vertices — when both runs reach it.
2454fn chain_runs(runs: Vec<Run>, valence: &HashMap<VertexKey, usize>, floor: f64) -> Vec<Vec<[f64; 2]>> {
2455    let mut at_vertex: HashMap<VertexKey, Vec<(usize, bool)>> = HashMap::new();
2456    for (index, run) in runs.iter().enumerate() {
2457        if let Some(v) = run.start {
2458            at_vertex.entry(v).or_default().push((index, true));
2459        }
2460        if let Some(v) = run.end {
2461            at_vertex.entry(v).or_default().push((index, false));
2462        }
2463    }
2464    // Neighbour through each end: (run, whether we enter at its start).
2465    let mut next: Vec<[Option<(usize, bool)>; 2]> = vec![[None, None]; runs.len()];
2466    for (vertex, list) in &at_vertex {
2467        if valence.get(vertex) != Some(&2) || list.len() != 2 || list[0].0 == list[1].0 {
2468            continue;
2469        }
2470        let ((a, a_start), (b, b_start)) = (list[0], list[1]);
2471        let (pa, pb) = (
2472            if a_start { runs[a].points[0] } else { *runs[a].points.last().unwrap() },
2473            if b_start { runs[b].points[0] } else { *runs[b].points.last().unwrap() },
2474        );
2475        if paper_distance(pa, pb) > floor + CHORD_MM {
2476            continue;
2477        }
2478        next[a][if a_start { 0 } else { 1 }] = Some((b, b_start));
2479        next[b][if b_start { 0 } else { 1 }] = Some((a, a_start));
2480    }
2481    let mut used = vec![false; runs.len()];
2482    let mut out = Vec::new();
2483    // Open chains first (from a run with a free end), then closed rings.
2484    let mut order: Vec<usize> = (0..runs.len()).collect();
2485    order.sort_by_key(|&i| (next[i][0].is_some() && next[i][1].is_some(), i));
2486    for start in order {
2487        if used[start] {
2488            continue;
2489        }
2490        used[start] = true;
2491        // Walk forward from the start run's END; a free START end is where
2492        // the chain begins.
2493        let mut points = runs[start].points.clone();
2494        if next[start][0].is_some() && next[start][1].is_none() {
2495            points.reverse();
2496            let mut cursor = next[start][0];
2497            extend_chain(&runs, &next, &mut used, &mut points, &mut cursor);
2498        } else {
2499            let mut cursor = next[start][1];
2500            extend_chain(&runs, &next, &mut used, &mut points, &mut cursor);
2501        }
2502        out.push(points);
2503    }
2504    out
2505}
2506
2507fn extend_chain(
2508    runs: &[Run],
2509    next: &[[Option<(usize, bool)>; 2]],
2510    used: &mut [bool],
2511    points: &mut Vec<[f64; 2]>,
2512    cursor: &mut Option<(usize, bool)>,
2513) {
2514    while let Some((run, enter_at_start)) = *cursor {
2515        if used[run] {
2516            break;
2517        }
2518        used[run] = true;
2519        let mut piece = runs[run].points.clone();
2520        if !enter_at_start {
2521            piece.reverse();
2522        }
2523        points.extend(piece.into_iter().skip(1));
2524        *cursor = next[run][if enter_at_start { 1 } else { 0 }];
2525    }
2526}
2527
2528// ---------------------------------------------------------------------------
2529// The exact section
2530// ---------------------------------------------------------------------------
2531
2532/// One cut curve of a section on the paper, ORIENTED so the cut region lies
2533/// to its left as the section camera sees it (the region's outward normal is
2534/// toward the eye): a whole set of them sums to the region's signed area.
2535pub type CutCurve = Vec<[f64; 2]>;
2536
2537/// The exact section of `solids` by the view's target plane (the camera
2538/// looks along its normal): per face, the plane's intersection with the
2539/// carrier — the kernel's closed forms for a plane against a cylinder, cone,
2540/// sphere or torus (`intersect_analytic_pair`), marched for a fitted surface
2541/// (`intersect_surfaces`), and for a plane against a plane the line their two
2542/// equations share, which the kernel's analytic lane leaves to its iso path on
2543/// purpose — trimmed to the face where its boundary edges cross the plane.
2544pub fn section_curves(solids: &[&BrepSolid], frame: &ViewFrame, placed: &PlacedView) -> Vec<CutCurve> {
2545    let trims: Vec<SolidTrims> = solids.iter().map(|solid| SolidTrims::of(solid)).collect();
2546    let trims: Vec<&SolidTrims> = trims.iter().collect();
2547    section_curves_with(solids, &trims, frame, placed)
2548}
2549
2550/// [`section_curves`] with each solid's [`SolidTrims`] kept from before.
2551pub fn section_curves_with(solids: &[&BrepSolid], trims: &[&SolidTrims], frame: &ViewFrame, placed: &PlacedView) -> Vec<CutCurve> {
2552    let floors = Floors::of(placed.scale);
2553    let tolerances = KernelTolerances::default();
2554    let normal = v3(frame.forward);
2555    let target = v3(frame.target);
2556    let mut out = Vec::new();
2557    for (solid, trims) in solids.iter().zip(trims) {
2558        let mut reach: f64 = 1.0;
2559        for shell in &solid.shells {
2560            for face in &shell.faces {
2561                for row in &face.surface.control_points {
2562                    for cp in row {
2563                        let w = if cp.w != 0.0 { cp.w } else { 1.0 };
2564                        reach = reach.max(Vec3::new(cp.x / w, cp.y / w, cp.z / w).sub(target).length());
2565                    }
2566                }
2567            }
2568        }
2569        let reach = reach * 1.5;
2570        let (right, up) = (v3(frame.right), v3(frame.up));
2571        let Ok(cutting) = make_plane(target.sub(right.scale(reach)).sub(up.scale(reach)), right, up, 2.0 * reach, 2.0 * reach)
2572        else {
2573            continue;
2574        };
2575        for (shell, shell_trims) in solid.shells.iter().zip(&trims.faces) {
2576            for (face, tests) in shell.faces.iter().zip(shell_trims) {
2577                for carrier in section_carriers(&cutting, face, normal, target, reach, &tolerances) {
2578                    for curve in trim_section(solid, face, Some(tests), carrier, &cutting, frame, placed, &floors) {
2579                        out.push(curve);
2580                    }
2581                }
2582            }
2583        }
2584    }
2585    out
2586}
2587
2588/// The untrimmed intersection of the cutting plane with one face's carrier.
2589fn section_carriers<'a>(
2590    cutting: &NurbsSurface,
2591    face: &'a FaceRecord,
2592    normal: Vec3,
2593    target: Vec3,
2594    reach: f64,
2595    tolerances: &KernelTolerances,
2596) -> Vec<(Carrier<'a>, f64, f64)> {
2597    let surface = &face.surface;
2598    if planar(surface) {
2599        let du = surface.domain_u().unwrap_or([0.0, 1.0]);
2600        let dv = surface.domain_v().unwrap_or([0.0, 1.0]);
2601        let (u, v) = (0.5 * (du[0] + du[1]), 0.5 * (dv[0] + dv[1]));
2602        let (Ok(origin), Ok(face_normal)) = (surface.evaluate(u, v), surface.normal(u, v)) else { return Vec::new() };
2603        let direction = normal.cross(face_normal);
2604        let Ok(direction) = direction.normalized() else { return Vec::new() }; // parallel or coplanar
2605        // The point of both planes nearest the target: solve n·x = n·t,
2606        // m·x = m·o in the span of the two normals.
2607        let (h1, h2) = (normal.dot(target), face_normal.dot(origin));
2608        let c = normal.dot(face_normal);
2609        let det = 1.0 - c * c;
2610        if det.abs() < 1e-18 {
2611            return Vec::new();
2612        }
2613        let a = (h1 - h2 * c) / det;
2614        let b = (h2 - h1 * c) / det;
2615        let point = normal.scale(a).add(face_normal.scale(b));
2616        let point = point.add(direction.scale(target.sub(point).dot(direction)));
2617        return vec![(
2618            Carrier::Line { a: point.sub(direction.scale(reach)), b: point.add(direction.scale(reach)) },
2619            0.0,
2620            1.0,
2621        )];
2622    }
2623    if let Some(curves) = intersect_analytic_pair(cutting, surface, tolerances.intersection_fit) {
2624        return curves
2625            .into_iter()
2626            .filter_map(|curve| {
2627                let [t0, t1] = curve.domain().ok()?;
2628                Some((Carrier::Curve(curve), t0, t1))
2629            })
2630            .collect();
2631    }
2632    let options = SurfaceIntersectionOptions { tolerance: tolerances.intersection_fit, ..Default::default() };
2633    match intersect_surfaces(cutting, surface, &options) {
2634        Ok(curves) => curves
2635            .into_iter()
2636            .filter(|curve| curve.points.len() >= 2)
2637            .map(|curve| {
2638                let mut points = curve.points;
2639                if curve.closed && points.first().map(|p| p.sub(*points.last().unwrap()).length()) > Some(0.0) {
2640                    points.push(points[0]);
2641                }
2642                let last = (points.len() - 1) as f64;
2643                (Carrier::Polyline(points), 0.0, last)
2644            })
2645            .collect(),
2646        Err(_) => Vec::new(),
2647    }
2648}
2649
2650/// The spans of one cut carrier that lie on `face`: split where the face's
2651/// boundary edges cross the cutting plane (`intersect_curve_surface`), each
2652/// span kept when its midpoint classifies inside the trim, then sampled
2653/// against the chord and oriented.
2654fn trim_section(
2655    solid: &BrepSolid,
2656    face: &FaceRecord,
2657    tests: Option<&TrimTests>,
2658    carrier: (Carrier, f64, f64),
2659    cutting: &NurbsSurface,
2660    frame: &ViewFrame,
2661    placed: &PlacedView,
2662    floors: &Floors,
2663) -> Vec<CutCurve> {
2664    let (carrier, s0, s1) = carrier;
2665    let tolerances = KernelTolerances::default();
2666    let initial = match &carrier {
2667        Carrier::Polyline(points) => points.len().saturating_sub(1).max(1),
2668        Carrier::Curve(curve) => curve_initial(curve),
2669        _ => 1,
2670    };
2671    let base = sample(&carrier, s0, s1, initial, frame, placed);
2672    if base.len() < 2 {
2673        return Vec::new();
2674    }
2675    // Where the face's boundary crosses the plane, as carrier parameters.
2676    let mut cuts: Vec<f64> = vec![s0, s1];
2677    for lp in &face.loops {
2678        for coedge in &lp.coedges {
2679            let Some(edge) = solid.edges.iter().find(|e| e.id == coedge.edge_id) else { continue };
2680            let Ok(hits) = intersect_curve_surface(&edge.curve, cutting, tolerances.intersection_fit) else { continue };
2681            for hit in hits {
2682                if hit.t < edge.t0.min(edge.t1) - 1e-12 || hit.t > edge.t0.max(edge.t1) + 1e-12 {
2683                    continue;
2684                }
2685                if let Some(s) = nearest_parameter(&carrier, &base, hit.point) {
2686                    cuts.push(s);
2687                }
2688            }
2689        }
2690    }
2691    cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
2692    let mut out = Vec::new();
2693    for w in cuts.windows(2) {
2694        let (lo, hi) = (w[0], w[1]);
2695        if hi - lo <= 0.0 {
2696            continue;
2697        }
2698        let Some(mid) = carrier.at(0.5 * (lo + hi)) else { continue };
2699        let Some(uv) = uv_on(face, mid, floors.bar) else { continue };
2700        if !in_face(face, tests, uv, floors.bar) {
2701            continue;
2702        }
2703        let piece = sample(&carrier, lo, hi, initial_between(&base, lo, hi), frame, placed);
2704        if piece.len() < 2 || piece.paper_length() < floors.crossing_mm {
2705            continue;
2706        }
2707        let mut paper = piece.paper;
2708        // Orient: the tangent along m × n_F, m the cap's outward normal
2709        // (toward the eye) and n_F the face's outward normal — the region is
2710        // then on the left as the eye sees it.
2711        let step = (hi - lo) * 1e-4;
2712        let tangent = match (carrier.at(0.5 * (lo + hi) + step), carrier.at(0.5 * (lo + hi) - step)) {
2713            (Some(a), Some(b)) => a.sub(b),
2714            _ => piece.points[piece.points.len() - 1].sub(piece.points[0]),
2715        };
2716        if let Some(n_face) = outward_normal(face, mid) {
2717            let eye = v3(frame.forward).scale(-1.0);
2718            if tangent.dot(eye.cross(n_face)) < 0.0 {
2719                paper.reverse();
2720            }
2721        }
2722        out.push(paper);
2723    }
2724    out
2725}
2726
2727/// The carrier parameter nearest a model point.
2728fn nearest_parameter(carrier: &Carrier, samples: &Samples, p: Vec3) -> Option<f64> {
2729    if let Carrier::Edge(curve) = carrier {
2730        return project_point_to_curve(curve, p).ok().map(|foot| foot.u);
2731    }
2732    // Nearest chord, then its foot's parameter.
2733    let mut best: Option<(f64, f64)> = None;
2734    for i in 0..samples.len().saturating_sub(1) {
2735        let (a, b) = (samples.points[i], samples.points[i + 1]);
2736        let d = b.sub(a);
2737        let length2 = d.dot(d);
2738        let t = if length2 > 0.0 { (p.sub(a).dot(d) / length2).clamp(0.0, 1.0) } else { 0.0 };
2739        let distance = p.sub(a.add(d.scale(t))).length();
2740        let s = samples.params[i] + (samples.params[i + 1] - samples.params[i]) * t;
2741        if best.is_none_or(|(bd, _)| distance < bd) {
2742            best = Some((distance, s));
2743        }
2744    }
2745    best.map(|(_, s)| s)
2746}
2747