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