Skip to main content

geop_ops_drawing/
hidden_lines.rs

1//! Hidden-line removal: a part's edges and silhouettes, projected onto the
2//! paper, each piece of them classified visible or hidden.
3//!
4//! What a view draws is the edges where faces meet at an angle (tangent
5//! edges, where they meet smoothly, only if asked), and the silhouettes of
6//! curved faces (see [`crate::silhouette`]). Along such a curve, what is in
7//! front of it changes only where its projection meets the projection of
8//! another — passing behind an edge or a silhouette — or turns back on
9//! itself, where the curve runs along the line of sight. So:
10//!
11//! 1. every curve is projected (exactly: the projection is linear, so a
12//!    NURBS curve projects to a NURBS curve with the same knots);
13//! 2. each is cut wherever its projection crosses or runs along another's
14//!    ([`curve_curve_overlaps_and_crossings`]) and wherever it is stationary
15//!    along the paper's axes ([`NurbCurve::stationary_parameters`]), which
16//!    holds every point where it turns back — and leaves every piece
17//!    monotone along both axes, so the pieces' ends span the view;
18//! 3. one point of each piece decides it: a ray from there towards the eye
19//!    either crosses a face first (hidden) or does not (visible).
20//!
21//! A ray crossing a face's trim right at its boundary has met an edge in
22//! front, whose projection the piece then runs along: the piece is drawn by
23//! that edge already, and is classified hidden, so that the last step drops
24//! it with every other piece lying on one already drawn — a hidden line
25//! under a visible one is never drawn, nor any line twice.
26//!
27//! [`NurbCurve::stationary_parameters`]: geop_core_geometry::nurb_curve::NurbCurve::stationary_parameters
28
29use std::collections::HashMap;
30
31use geop_core_geometry::{
32    contains::curve::curve_could_contain,
33    intersection::{
34        curve_curve_overlaps_and_crossings, curve_surface_crossings, curve_surface_overlaps,
35        refine_crossing,
36    },
37    nurb_curve::{NurbCurve2D, NurbCurve3D},
38    nurb_surface::NurbSurface3D,
39};
40use geop_core_math::{
41    geop_error::{GeopError, GeopResult, WithContext},
42    plain::diagonal,
43    scalars::Scalar,
44    vector::{Vector2, Vector3},
45};
46use geop_core_topology::{
47    CoedgeGeometry, CoedgeId, EdgeId, FaceId, Model,
48    contains::face::{PointClassification, face_contains},
49};
50
51use crate::{
52    silhouette::{Silhouette, edge_crossings, face_silhouettes},
53    view::ViewFrame,
54};
55use geop_core_geometry::search::{MAX_NODES, PROJECT_ITERATIONS, min_subdivision_size};
56
57/// Seed for the containment tests.
58const FACE_CONTAINS_SEED: u64 = 0xD_0A_11;
59/// Where a search for where two lines cross on the paper hands over when
60/// one with the fine handoff could not settle (see [`meetings`]), and
61/// where one between the lines of two parts does (see
62/// [`crate::scene`]): far below what a sheet shows at any scale it is
63/// drawn at.
64pub(crate) const COARSE_SUBDIVISION: f64 = 1e-5;
65
66/// What a line of a view shows.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub enum LineKind {
69    /// An edge where two faces meet at an angle, or a face's free edge.
70    Edge,
71    /// An edge where two faces meet smoothly.
72    Tangent,
73    /// Where a curved face turns away from the eye.
74    Silhouette,
75}
76
77/// What to draw.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub struct ViewOptions {
80    /// Whether tangent edges are drawn.
81    pub tangent_edges: bool,
82    /// Whether hidden lines are kept.
83    pub hidden_lines: bool,
84}
85
86impl Default for ViewOptions {
87    fn default() -> Self {
88        ViewOptions {
89            tangent_edges: false,
90            hidden_lines: true,
91        }
92    }
93}
94
95/// A piece of a line of a view, visible or hidden throughout.
96#[derive(Clone, Debug)]
97pub struct ViewLine {
98    /// On the paper, in model units.
99    pub curve: NurbCurve2D,
100    /// The same piece in space.
101    pub curve3: NurbCurve3D,
102    pub kind: LineKind,
103    pub visible: bool,
104    /// The edge it is a piece of, if it is one: of the model of its body.
105    pub edge: Option<EdgeId>,
106    /// Which body of the scene it is a line of (see [`crate::scene`]): 0
107    /// in a view of one model.
108    pub body: usize,
109    /// Which curve it is a piece of, and where: its parameter range there.
110    pub(crate) source: usize,
111    pub(crate) range: (Scalar, Scalar),
112}
113
114/// A part seen from one direction.
115#[derive(Clone, Debug)]
116pub struct ProjectedView {
117    pub frame: ViewFrame,
118    pub lines: Vec<ViewLine>,
119}
120
121impl ProjectedView {
122    /// The box the view's lines span on the paper, `[x_min, y_min, x_max,
123    /// y_max]`, each an enclosure: the ends of its pieces, which are
124    /// monotone along both axes. `None` for a view without lines.
125    pub fn extents(&self) -> Option<[Scalar; 4]> {
126        let mut b: Option<[Scalar; 4]> = None;
127        for line in &self.lines {
128            let (t0, t1) = line.curve.domain();
129            for t in [t0, t1] {
130                let Ok(p) = line.curve.evaluate(t) else {
131                    continue;
132                };
133                let (x, y) = (p[0], p[1]);
134                b = Some(match b {
135                    None => [x, y, x, y],
136                    Some([a, c, e, f]) => [a.min(x), c.min(y), e.max(x), f.max(y)],
137                });
138            }
139        }
140        b
141    }
142}
143
144/// A curve to draw, before it is cut into pieces.
145struct Source {
146    curve3: NurbCurve3D,
147    curve: NurbCurve2D,
148    kind: LineKind,
149    edge: Option<EdgeId>,
150    silhouette: Option<Silhouette>,
151    /// The faces it lies on: those an edge bounds, a silhouette's face.
152    on: Vec<FaceId>,
153    /// Which curve it is a piece of, once curves are cut into monotone
154    /// pieces.
155    origin: usize,
156    /// Its projection's box, `[x_lo, y_lo, x_hi, y_hi]`, holding every
157    /// control point's whole enclosure.
158    bounds: [f64; 4],
159}
160
161/// The box holding `curve`'s control points (and so the curve), outward.
162pub(crate) fn bounds(curve: &NurbCurve2D) -> [f64; 4] {
163    let [x, y, _] = curve.aabb();
164    [
165        x.lower().to_f64(),
166        y.lower().to_f64(),
167        x.upper().to_f64(),
168        y.upper().to_f64(),
169    ]
170}
171
172/// Whether two boxes on the paper, `[x_lo, y_lo, x_hi, y_hi]`, could come
173/// within the sheet's resolution ([`COARSE_SUBDIVISION`]) of each other:
174/// what lies that close is one line there (see [`meetings`]).
175pub(crate) fn boxes_meet(a: &[f64; 4], b: &[f64; 4]) -> bool {
176    let r = COARSE_SUBDIVISION;
177    a[0] <= b[2] + r && b[0] <= a[2] + r && a[1] <= b[3] + r && b[1] <= a[3] + r
178}
179
180/// Whether `curve` projects to a single point: a line along the line of
181/// sight, seen end on.
182fn is_point(curve: &NurbCurve2D) -> GeopResult<bool> {
183    let first = curve.evaluate(curve.domain().0)?;
184    for q in &curve.control_points {
185        if !q.dehomogenized()?.could_be_equal(&first) {
186            return Ok(false);
187        }
188    }
189    Ok(true)
190}
191
192/// The coedges of every edge of `faces`.
193fn edge_coedges(model: &Model, faces: &[FaceId]) -> Vec<(EdgeId, Vec<CoedgeId>)> {
194    let mut order = Vec::new();
195    let mut map: HashMap<EdgeId, Vec<CoedgeId>> = HashMap::new();
196    for &face_id in faces {
197        for coedge_id in model.iterate_face_coedges(face_id) {
198            if let CoedgeGeometry::Edge(edge_id) = model.coedges[&coedge_id].geometry {
199                let entry = map.entry(edge_id).or_default();
200                if entry.is_empty() {
201                    order.push(edge_id);
202                }
203                entry.push(coedge_id);
204            }
205        }
206    }
207    order
208        .into_iter()
209        .map(|e| {
210            let coedges = map.remove(&e).unwrap_or_default();
211            (e, coedges)
212        })
213        .collect()
214}
215
216/// How the edge `edge_id`, between the faces of `coedges`, is drawn seen
217/// along `d`: sharp, it is an edge; smooth, it is a tangent edge — or, where
218/// the faces turn away from the eye along it, their silhouette.
219fn edge_kind(
220    model: &Model,
221    edge_id: EdgeId,
222    coedges: &[CoedgeId],
223    d: &Vector3,
224) -> GeopResult<LineKind> {
225    let [a, b] = coedges else {
226        return Ok(LineKind::Edge);
227    };
228    let curve = &model.get_edge(edge_id)?.curve;
229    let point = curve.evaluate(curve.mid_parameter()?)?;
230    let normal = |coedge_id: CoedgeId| -> GeopResult<Vector3> {
231        let coedge = model.get_coedge(coedge_id)?;
232        let surface = &model.get_face(coedge.face)?.surface;
233        let seed = coedge.pcurve.evaluate(coedge.pcurve.mid_parameter()?)?;
234        let (u, v) = surface.project(point, seed[0], seed[1], PROJECT_ITERATIONS)?;
235        surface.normal(u, v)
236    };
237    let (Ok(na), Ok(nb)) = (normal(*a), normal(*b)) else {
238        return Ok(LineKind::Edge);
239    };
240    if !na.prod_cross(&nb).could_be_equal(&Vector3::zero()) {
241        return Ok(LineKind::Edge);
242    }
243    Ok(if na.prod_dot(d).could_be_equal(Scalar::ZERO) {
244        LineKind::Silhouette
245    } else {
246        LineKind::Tangent
247    })
248}
249
250/// A face that can hide something, with the box holding it.
251pub(crate) struct Occluder {
252    face: FaceId,
253    surface: NurbSurface3D,
254    /// `[lo, hi]` per axis, holding every control point's whole enclosure.
255    pub(crate) bounds: [[f64; 2]; 3],
256}
257
258impl Occluder {
259    pub(crate) fn of(model: &Model, face: FaceId) -> GeopResult<Self> {
260        let surface = model.get_face(face)?.surface.clone();
261        let bounds = surface
262            .aabb()
263            .map(|x| [x.lower().to_f64(), x.upper().to_f64()]);
264        Ok(Occluder {
265            face,
266            surface,
267            bounds,
268        })
269    }
270
271    /// Whether the segment from `a` to `b` could meet the box.
272    fn could_meet(&self, a: &Vector3, b: &Vector3) -> bool {
273        (0..3).all(|k| {
274            let (x, y) = (a[k], b[k]);
275            let lo = x.lower().min(y.lower()).to_f64();
276            let hi = x.upper().max(y.upper()).to_f64();
277            lo <= self.bounds[k][1] && self.bounds[k][0] <= hi
278        })
279    }
280}
281
282/// Whether `point` is seen from the eye: whether a ray from it towards the
283/// eye (`toward_eye`, `length` long) leaves every face of `occluders`
284/// behind. A crossing at the point itself — it lies on the faces it bounds
285/// — is no crossing; one right on a face's trim boundary means an edge is
286/// in front (see the module docs) and counts as hiding it.
287pub(crate) fn seen(
288    model: &Model,
289    occluders: &[Occluder],
290    point: Vector3,
291    toward_eye: &Vector3,
292    length: Scalar,
293) -> GeopResult<bool> {
294    let far = point.add(&toward_eye.prod_scalar(length));
295    let ray = NurbCurve3D::line(point, far)?;
296    let coarse = Scalar::from_f64(COARSE_SUBDIVISION);
297    let mut behind_an_edge = false;
298    for occluder in occluders.iter().filter(|o| o.could_meet(&point, &far)) {
299        let (face_id, surface) = (occluder.face, &occluder.surface);
300        let ctx =
301            |e: GeopError| e.with_context(format!("the ray from {point:?} against face {face_id}"));
302        let overlaps =
303            curve_surface_overlaps(&ray, surface, coarse, MAX_NODES, min_subdivision_size())
304                .with_context(&ctx)?;
305        if !overlaps.is_empty() {
306            // The ray runs in the face's surface, to what a sheet shows:
307            // the face is seen edge on here, and an edge-on face hides
308            // nothing. Edge on to within the rounding of a placed part's
309            // turn, the ray runs `1e-12` off it, and a search for where it
310            // crosses would have to cover all its length in pieces that
311            // small.
312            continue;
313        }
314        // A ray from a silhouette leaves its face tangent to it, and a hair
315        // off tangent in a part placed by a pose rounded in its last bits:
316        // the touch at its start no longer pins onto the start, and only
317        // the sheet's resolution, as a handoff, isolates it — a crossing
318        // that close to the start is the point itself, and not past it.
319        let crossings = curve_surface_crossings(&ray, surface, MAX_NODES, min_subdivision_size())
320            .or_else(|error| {
321                curve_surface_crossings(&ray, surface, MAX_NODES, coarse).map_err(|e| {
322                    e.with_context(format!(
323                        "searched again to the sheet's resolution, after: {error}"
324                    ))
325                })
326            })
327            .with_context(&ctx)?;
328        for (t, uv) in crossings {
329            let (t, uv) = if t.definitely_greater(Scalar::ZERO) {
330                (t, uv)
331            } else {
332                refine_crossing(&ray, surface, t, uv)
333            };
334            if !t.definitely_greater(Scalar::ZERO) {
335                continue;
336            }
337            match face_contains(
338                model,
339                face_id,
340                uv[0].midpoint(),
341                uv[1].midpoint(),
342                MAX_NODES,
343                min_subdivision_size(),
344                FACE_CONTAINS_SEED,
345            )
346            .with_context(&ctx)?
347            {
348                PointClassification::Inside => return Ok(false),
349                PointClassification::Outside => {}
350                PointClassification::OnCoedge | PointClassification::OnVertex => {
351                    behind_an_edge = true
352                }
353            }
354        }
355    }
356    Ok(!behind_an_edge)
357}
358
359/// The box holding the boxes of all `occluders`: its lowest corner and its
360/// highest, the other way round if there are none.
361pub(crate) fn occluders_bounds<'o>(
362    occluders: impl IntoIterator<Item = &'o Occluder>,
363) -> ([f64; 3], [f64; 3]) {
364    let mut lo = [f64::INFINITY; 3];
365    let mut hi = [f64::NEG_INFINITY; 3];
366    for o in occluders {
367        for k in 0..3 {
368            lo[k] = lo[k].min(o.bounds[k][0]);
369            hi[k] = hi[k].max(o.bounds[k][1]);
370        }
371    }
372    (lo, hi)
373}
374
375/// The ray length that clears every occluder from anywhere on them: twice
376/// the diagonal of their boxes' box.
377pub(crate) fn ray_length<'o>(occluders: impl IntoIterator<Item = &'o Occluder>) -> Scalar {
378    let (lo, hi) = occluders_bounds(occluders);
379    Scalar::from_f64(2.0 * diagonal(lo, hi) + 1.0)
380}
381
382/// The parameter boxes in `boxes` merged where they overlap, as sharp cut
383/// points strictly inside `(t0, t1)`: where to cut is a free choice within
384/// each.
385pub(crate) fn cut_points(mut boxes: Vec<Scalar>, (t0, t1): (Scalar, Scalar)) -> Vec<Scalar> {
386    boxes.sort_by(|a, b| a.lower().to_f64().total_cmp(&b.lower().to_f64()));
387    let mut merged: Vec<Scalar> = Vec::new();
388    for b in boxes {
389        match merged.last_mut() {
390            Some(last) if last.could_be_equal(b) => *last = last.union(b),
391            _ => merged.push(b),
392        }
393    }
394    merged
395        .into_iter()
396        .filter(|b| b.definitely_greater(t0) && b.definitely_less(t1))
397        .map(|b| b.midpoint())
398        .collect()
399}
400
401/// The lines of the faces `faces` of `model` seen through `frame`, each
402/// piece visible or hidden. Every face both draws and hides.
403pub fn project_view(
404    model: &Model,
405    faces: &[FaceId],
406    frame: &ViewFrame,
407    options: &ViewOptions,
408) -> GeopResult<ProjectedView> {
409    let d = frame.direction.vector();
410    // Each curve with the parameters it is cut at before anything else.
411    let mut sources: Vec<(Source, Vec<Scalar>)> = Vec::new();
412    let mut add = |curve3: NurbCurve3D,
413                   kind: LineKind,
414                   edge: Option<EdgeId>,
415                   silhouette: Option<Silhouette>,
416                   on: Vec<FaceId>,
417                   cuts: Vec<Scalar>|
418     -> GeopResult<()> {
419        let curve = frame.project_curve(&curve3)?;
420        if is_point(&curve)? {
421            return Ok(());
422        }
423        let bounds = bounds(&curve);
424        sources.push((
425            Source {
426                curve3,
427                curve,
428                kind,
429                edge,
430                silhouette,
431                on,
432                origin: sources.len(),
433                bounds,
434            },
435            cuts,
436        ));
437        Ok(())
438    };
439    for (edge_id, coedges) in edge_coedges(model, faces) {
440        let ctx = |e: GeopError| e.with_context(format!("project_view: edge {edge_id}"));
441        let kind = edge_kind(model, edge_id, &coedges, &d).with_context(&ctx)?;
442        if kind == LineKind::Tangent && !options.tangent_edges {
443            continue;
444        }
445        let curve = model.get_edge(edge_id)?.curve.clone();
446        // Where it crosses the silhouettes of the faces it bounds.
447        let mut cuts = Vec::new();
448        let mut on = Vec::new();
449        for &coedge_id in &coedges {
450            let face = model.get_coedge(coedge_id)?.face;
451            on.push(face);
452            cuts.extend(edge_crossings(model, face, coedge_id, &curve, &d).with_context(&ctx)?);
453        }
454        add(curve, kind, Some(edge_id), None, on, cuts).with_context(&ctx)?;
455    }
456    for &face_id in faces {
457        for silhouette in face_silhouettes(model, face_id, &d)? {
458            add(
459                silhouette.curve.clone(),
460                LineKind::Silhouette,
461                None,
462                Some(silhouette),
463                vec![face_id],
464                Vec::new(),
465            )?;
466        }
467    }
468
469    // Every curve cut where it is stationary along either axis of the
470    // paper, before the curves are compared: then each runs one way along
471    // both, and a curve seen edge on — its projection a segment it runs
472    // back and forth along — becomes segments that overlap others plainly
473    // instead of folding onto themselves.
474    let axes = [
475        Vector2::from_array([Scalar::ONE, Scalar::ZERO]),
476        Vector2::from_array([Scalar::ZERO, Scalar::ONE]),
477    ];
478    let mut monotone = Vec::with_capacity(sources.len());
479    for (source, mut stationary) in sources {
480        for axis in &axes {
481            stationary.extend(
482                source
483                    .curve
484                    .stationary_parameters(axis, MAX_NODES, min_subdivision_size())
485                    .map_err(|e| {
486                        e.with_context(format!(
487                            "project_view: the {:?} of edge {:?}",
488                            source.kind, source.edge
489                        ))
490                    })?,
491            );
492        }
493        let domain = source.curve.domain();
494        let mut ends = vec![domain.0];
495        ends.extend(cut_points(stationary, domain));
496        ends.push(domain.1);
497        if ends.len() == 2 {
498            monotone.push(source);
499            continue;
500        }
501        for pair in ends.windows(2) {
502            let curve = source.curve.sub_curve(pair[0], pair[1])?;
503            if is_point(&curve)? {
504                continue;
505            }
506            monotone.push(Source {
507                curve3: source.curve3.sub_curve(pair[0], pair[1])?,
508                bounds: bounds(&curve),
509                curve,
510                kind: source.kind,
511                edge: source.edge,
512                silhouette: source.silhouette.clone(),
513                on: source.on.clone(),
514                origin: source.origin,
515            });
516        }
517    }
518    let sources = monotone;
519
520    // Where each piece has to be cut.
521    let mut cuts: Vec<Vec<Scalar>> = vec![Vec::new(); sources.len()];
522    for i in 0..sources.len() {
523        for j in i + 1..sources.len() {
524            if !boxes_meet(&sources[i].bounds, &sources[j].bounds) {
525                continue;
526            }
527            // An edge meets the silhouette of a face it bounds where it
528            // crosses it on the face — found already, and cut at — and
529            // touches it there on the paper, tangentially: no search
530            // here could isolate that.
531            let bounds_silhouette = |a: &Source, b: &Source| {
532                a.edge.is_some()
533                    && b.kind == LineKind::Silhouette
534                    && b.edge.is_none()
535                    && b.on.iter().any(|f| a.on.contains(f))
536            };
537            if bounds_silhouette(&sources[i], &sources[j])
538                || bounds_silhouette(&sources[j], &sources[i])
539            {
540                continue;
541            }
542            let (a, b) = (&sources[i], &sources[j]);
543            let (a_cuts, b_cuts) = meetings(
544                (&a.curve, &a.curve3),
545                (&b.curve, &b.curve3),
546                min_subdivision_size(),
547            )
548            .map_err(|e| {
549                let describe = |s: &Source| match s.edge {
550                    Some(edge) => format!("{:?} of edge {edge} (of faces {:?})", s.kind, s.on),
551                    None => format!(
552                        "{:?} of face {:?}",
553                        s.kind,
554                        s.silhouette.as_ref().map(|s| s.face)
555                    ),
556                };
557                e.with_context(format!(
558                    "project_view: where the {} and the {} cross on the paper, seen through \
559                     {frame:?}: on the paper {:?} and {:?}",
560                    describe(&sources[i]),
561                    describe(&sources[j]),
562                    sources[i].curve,
563                    sources[j].curve,
564                ))
565            })?;
566            cuts[i].extend(a_cuts);
567            cuts[j].extend(b_cuts);
568        }
569    }
570
571    // Each piece, decided by its middle.
572    let toward_eye = frame.toward_eye();
573    let occluders = faces
574        .iter()
575        .map(|&f| Occluder::of(model, f))
576        .collect::<GeopResult<Vec<_>>>()?;
577    let length = ray_length(&occluders);
578    // Every piece, in order along each curve: its source, its range, and
579    // its visibility or why no point of it could be decided.
580    let mut pieces: Vec<(usize, Scalar, Scalar, Result<bool, GeopError>)> = Vec::new();
581    for (index, (source, cuts)) in sources.iter().zip(cuts).enumerate() {
582        let domain = source.curve.domain();
583        let mut ends = vec![domain.0];
584        ends.extend(cut_points(cuts, domain));
585        ends.push(domain.1);
586        for pair in ends.windows(2) {
587            let (a, b) = (pair[0], pair[1]);
588            let ctx = |e: GeopError| {
589                let face = source.silhouette.as_ref().map(|s| s.face);
590                let (p, q) = (source.curve3.evaluate(a), source.curve3.evaluate(b));
591                e.with_context(format!(
592                    "project_view: the piece [{a:?}, {b:?}] of a line of kind {:?} (edge {:?}, \
593                     face {face:?}), from {p:?} to {q:?} in space",
594                    source.kind, source.edge
595                ))
596            };
597            // Any point of the piece decides it; the middle first, and two
598            // more should a ray from there graze a face too closely for
599            // its search to settle — as one does from where an edge
600            // crosses the silhouette of a face it bounds, the ray leaving
601            // tangent to that face.
602            let decide = |f: f64| -> GeopResult<bool> {
603                let t = Scalar::fraction(a, b, Scalar::from_f64(f));
604                let mut point = source.curve3.evaluate(t)?;
605                if let Some(silhouette) = &source.silhouette {
606                    point = silhouette.exact_point(model, &d, &point)?;
607                }
608                seen(model, &occluders, point, &toward_eye, length)
609            };
610            let mut outcome = Err(GeopError::new("no point tried"));
611            for f in [0.5, 0.25, 0.75] {
612                outcome = decide(f).map_err(ctx);
613                if outcome.is_ok() {
614                    break;
615                }
616            }
617            pieces.push((index, a, b, outcome));
618        }
619    }
620    // A piece none of whose points could be decided is a sliver next to
621    // such a graze: it takes the visibility of its nearest neighbour along
622    // the same curve.
623    let known: Vec<Option<bool>> = pieces.iter().map(|p| p.3.as_ref().ok().copied()).collect();
624    let origin = |k: usize| sources[pieces[k].0].origin;
625    let mut lines = Vec::new();
626    for k in 0..pieces.len() {
627        let visible = match known[k] {
628            Some(v) => v,
629            None => {
630                let along = |i: usize| (origin(i) == origin(k)).then_some(known[i]).flatten();
631                let neighbour = (1..pieces.len()).find_map(|step| {
632                    let before = k.checked_sub(step).and_then(along);
633                    before.or_else(|| (k + step < pieces.len()).then(|| along(k + step)).flatten())
634                });
635                match neighbour {
636                    Some(v) => v,
637                    None => {
638                        let (_, _, _, outcome) = pieces.swap_remove(k);
639                        return Err(outcome.unwrap_err());
640                    }
641                }
642            }
643        };
644        let (index, a, b, _) = &pieces[k];
645        let source = &sources[*index];
646        if !visible && !options.hidden_lines {
647            continue;
648        }
649        lines.push(ViewLine {
650            curve: source.curve.sub_curve(*a, *b)?,
651            curve3: source.curve3.sub_curve(*a, *b)?,
652            kind: source.kind,
653            visible,
654            edge: source.edge,
655            body: 0,
656            source: *index,
657            range: (*a, *b),
658        });
659    }
660
661    Ok(ProjectedView {
662        frame: *frame,
663        lines: rejoin(&sources, drop_drawn(lines, |_, _| true)?)?,
664    })
665}
666
667/// Where the projections of `a` and `b` cross or begin and end running
668/// along each other — on each other, or within `reach` of each other (see
669/// [`curve_curve_overlaps_and_crossings`]): the parameters to cut each at.
670/// `handoff` is where the search hands over (see `AGENTS.md`).
671fn search(
672    a: &NurbCurve2D,
673    b: &NurbCurve2D,
674    reach: Scalar,
675    handoff: Scalar,
676) -> GeopResult<(Vec<Scalar>, Vec<Scalar>)> {
677    let (overlaps, crossings) =
678        curve_curve_overlaps_and_crossings(a, b, reach, MAX_NODES, handoff)?;
679    let (mut on_a, mut on_b) = (Vec::new(), Vec::new());
680    for (s, t) in crossings {
681        on_a.push(s);
682        on_b.push(t);
683    }
684    for o in overlaps {
685        on_a.extend([o.start.t, o.end.t]);
686        on_b.extend([o.start.partner, o.end.partner]);
687    }
688    Ok((on_a, on_b))
689}
690
691/// Where the pieces `a` and `b`, each on the paper and in space, have to
692/// be cut for each other (see [`search`]), searched with the handoff
693/// `handoff`.
694///
695/// What a sheet shows has a resolution ([`COARSE_SUBDIVISION`]), and two
696/// lines running within it of each other along a stretch are one line
697/// there: they are cut where that stretch begins and ends (the `reach` of
698/// [`search`]), so that one of them drops the other's piece along it as
699/// drawn (see [`drop_drawn`]). The two edges of a face seen edge on in a
700/// part placed by a turn rounded in its last bits lie `1e-13` apart on the
701/// paper: the exact search finds them apart, or cannot get through them in
702/// pieces that small, and the hidden one was drawn beside the visible.
703///
704/// Two curves meeting at a common end often meet there tangentially on the
705/// paper — an edge running into another smoothly, seen from the side —
706/// and a subdivision search cannot isolate a tangency: it runs out of
707/// nodes. Their common end is an end of both pieces already, so nothing
708/// needs cutting there. Should the search fail, it is run again on the
709/// halves of the two, leaving out the two halves that meet at the common
710/// end. A crossing within those two halves, other than at the common end,
711/// would go unseen: a known limit, taken rather than failing the view.
712///
713/// Two curves touching elsewhere — a screw's head seen from above, round,
714/// touching the line a flat of the nut under it is seen as — come closer
715/// than a fine handoff along a stretch the search cannot get through
716/// either. Should all else fail, the search is run once more with the
717/// sheet's resolution as its handoff too: the touch comes out as one wide
718/// cluster, cut at a free point inside it, and two crossings closer
719/// together than that are taken as one.
720pub(crate) fn meetings(
721    (a, a3): (&NurbCurve2D, &NurbCurve3D),
722    (b, b3): (&NurbCurve2D, &NurbCurve3D),
723    handoff: Scalar,
724) -> GeopResult<(Vec<Scalar>, Vec<Scalar>)> {
725    let coarse = Scalar::from_f64(COARSE_SUBDIVISION);
726    let fine = || -> GeopResult<(Vec<Scalar>, Vec<Scalar>)> {
727        let error = match search(a, b, coarse, handoff) {
728            Ok(found) => return Ok(found),
729            Err(e) => e,
730        };
731        let ends = |c: &NurbCurve3D| -> GeopResult<[Vector3; 2]> {
732            let (t0, t1) = c.domain();
733            Ok([c.evaluate(t0)?, c.evaluate(t1)?])
734        };
735        let (ea, eb) = (ends(a3)?, ends(b3)?);
736        let shared = (0..2)
737            .flat_map(|i| (0..2).map(move |j| (i, j)))
738            .find(|&(i, j)| ea[i].could_be_equal(&eb[j]));
739        let ends_context = format!("their ends in space: {ea:?} and {eb:?}");
740        let Some((end_a, end_b)) = shared else {
741            return Err(error.with_context(ends_context));
742        };
743        let error = error.with_context(ends_context);
744        let halves = |c: &NurbCurve2D| -> GeopResult<[NurbCurve2D; 2]> {
745            let (l, r) = c.split_mid()?;
746            Ok([l, r])
747        };
748        let (ha, hb) = (halves(a)?, halves(b)?);
749        let (mut on_a, mut on_b) = (Vec::new(), Vec::new());
750        for (i, half_a) in ha.iter().enumerate() {
751            for (j, half_b) in hb.iter().enumerate() {
752                if i == end_a && j == end_b {
753                    continue;
754                }
755                let (x, y) = search(half_a, half_b, coarse, handoff).map_err(|e| {
756                    e.with_context(format!(
757                        "searched again without the common end, after: {error}"
758                    ))
759                })?;
760                on_a.extend(x);
761                on_b.extend(y);
762            }
763        }
764        Ok((on_a, on_b))
765    };
766    fine().or_else(|error| {
767        search(a, b, coarse, coarse).map_err(|e| {
768            e.with_context(format!(
769                "searched again to the sheet's resolution, after: {error}"
770            ))
771        })
772    })
773}
774
775/// `lines` with the pieces of one curve that follow on from each other, and
776/// are both visible or both hidden, joined again: the cuts that did not
777/// change anything undone, so a circle seen round stays a circle.
778fn rejoin(sources: &[Source], mut lines: Vec<ViewLine>) -> GeopResult<Vec<ViewLine>> {
779    lines.sort_by(|a, b| {
780        (a.source, a.range.0.to_f64())
781            .partial_cmp(&(b.source, b.range.0.to_f64()))
782            .unwrap_or(std::cmp::Ordering::Equal)
783    });
784    let mut joined: Vec<ViewLine> = Vec::new();
785    for line in lines {
786        if let Some(last) = joined.last_mut()
787            && last.source == line.source
788            && last.visible == line.visible
789            && last.range.1.could_be_equal(line.range.0)
790        {
791            last.range.1 = line.range.1;
792            continue;
793        }
794        joined.push(line);
795    }
796    // A closed curve cut at its start joins across it, too.
797    let mut out: Vec<ViewLine> = Vec::new();
798    for mut line in joined {
799        let source = &sources[line.source];
800        let (t0, t1) = source.curve.domain();
801        let whole = line.range.0.could_be_equal(t0) && line.range.1.could_be_equal(t1);
802        if whole {
803            line.curve = source.curve.clone();
804            line.curve3 = source.curve3.clone();
805        } else {
806            line.curve = source.curve.sub_curve(line.range.0, line.range.1)?;
807            line.curve3 = source.curve3.sub_curve(line.range.0, line.range.1)?;
808        }
809        out.push(line);
810    }
811    Ok(out)
812}
813
814/// `lines` without every piece lying on one drawn before it — visible ones
815/// first, so a hidden line under a visible one goes — of the pieces
816/// `compare` says to compare it with. Pieces are cut wherever their
817/// projections meet, so a piece lies on another all along or only touches
818/// it at its ends: its middle tells which.
819pub(crate) fn drop_drawn(
820    mut lines: Vec<ViewLine>,
821    compare: impl Fn(&ViewLine, &ViewLine) -> bool,
822) -> GeopResult<Vec<ViewLine>> {
823    lines.sort_by_key(|l| !l.visible);
824    let mut kept: Vec<(ViewLine, [f64; 4])> = Vec::new();
825    // On one drawn to the sheet's resolution: the edges of a face seen edge
826    // on in a part turned by a rounded pose lie `1e-13` apart, one line.
827    let reach = Scalar::from_f64(COARSE_SUBDIVISION);
828    let around = reach.neg().union(reach);
829    for line in lines {
830        let mid = (line.curve.evaluate(line.curve.mid_parameter()?)?).map(|x| x.add(around));
831        let here = bounds(&line.curve);
832        let mut drawn = false;
833        for (other, there) in &kept {
834            if boxes_meet(&here, there)
835                && compare(&line, other)
836                && curve_could_contain(&other.curve, &mid, MAX_NODES, min_subdivision_size())?
837                    .is_some()
838            {
839                drawn = true;
840                break;
841            }
842        }
843        if !drawn {
844            kept.push((line, here));
845        }
846    }
847    Ok(kept.into_iter().map(|(l, _)| l).collect())
848}