Skip to main content

brep_kernel/csg/
imprint.rs

1use crate::classification::{parameter_point_in_face, PolygonClass};
2use crate::curve::KNOT_IDENTITY_TOL;
3use crate::spatial::{Aabb, Bvh};
4use crate::tolerance::{
5    assembler_weld, merge_scale, solid_scale, COINCIDENCE_DISTANCE_FLOOR, WELD_FLOOR,
6};
7use crate::topology::{BrepSolid, EdgeRecord, FaceRecord};
8use crate::{
9    build_pcurve_on_surface, classify_surface_pair_cached,
10    fit_polyline, fit_polyline_of_degree, PolylineFit,
11    intersect_curves, intersect_surfaces, intersect_surfaces_supplemental, project_point_to_curve,
12    project_point_to_surface, project_point_to_surface_seeded, KnotVector, NurbsCurve, NurbsSurface,
13    SurfaceClassifyData,
14    SurfaceIntersectionOptions, SurfacePairClassification, SurfacePairRelation, Vec2, Vec3, Vec4,
15};
16use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
17use serde::{Deserialize, Serialize};
18use std::time::Duration;
19use web_time::Instant;
20
21// The profile line buffer, `profile_line!` and `profile_enabled` live in
22// `crate::profile` (consolidation I8); these paths stay for their callers.
23pub(crate) use crate::profile::{profile_emit, profile_enabled, ProfileSink};
24
25/// Per-step wall-clock accumulators for `build_imprints`, printed to stderr
26/// when `BREP_PROFILE` is set. Native-only in practice (no env vars in wasm).
27#[derive(Default)]
28struct ImprintProfile {
29    enabled: bool,
30    pairs: u64,
31    marched_pairs: u64,
32    whole_reading_reuses: u64,
33    classify: Duration,
34    cosurface: Duration,
35    lies_on: Duration,
36    planar_iso: Duration,
37    analytic: Duration,
38    seeds: Duration,
39    march: Duration,
40    clip_and_fit: Duration,
41    process_curve: Duration,
42    /// `clip_and_fit` split: the swapped-order rescue marches, the trim clip,
43    /// the shared-boundary test, the polyline fit, and `process_curve`.
44    rescue_march: Duration,
45    clip: Duration,
46    shared_boundary: Duration,
47    fit: Duration,
48    process: Duration,
49    hermite_runs: u64,
50    fit_runs: u64,
51    hermite: Duration,
52    /// Appended at the end.
53    pairs_fits: u64,
54    exact_lane_fits: u64,
55    /// Slice 1: planar-iso sections whose exact pcurves were handed to the
56    /// builder, and those declined (a lifted chart, or no pcurve built).
57    exact_iso_sections: u64,
58    exact_iso_declined: u64,
59}
60
61impl ImprintProfile {
62    fn new() -> Self {
63        Self {
64            enabled: profile_enabled(),
65            ..Self::default()
66        }
67    }
68
69    fn lap(&self, started: &mut Option<Instant>) -> Duration {
70        crate::profile::lap(self.enabled, started)
71    }
72
73    fn report(&self) {
74        if !self.enabled {
75            return;
76        }
77        profile_line!("imprint.whole_reading_reuses count={}", self.whole_reading_reuses);
78        let ms = |d: Duration| d.as_secs_f64() * 1_000.0;
79        profile_line!(
80            "imprint.profile pairs={} marched={} classify={:.2} cosurface={:.2} lies_on={:.2} planar_iso={:.2} analytic={:.2} seeds={:.2} march={:.2} clip_fit={:.2} process_curve={:.2}",
81            self.pairs,
82            self.marched_pairs,
83            ms(self.classify),
84            ms(self.cosurface),
85            ms(self.lies_on),
86            ms(self.planar_iso),
87            ms(self.analytic),
88            ms(self.seeds),
89            ms(self.march),
90            ms(self.clip_and_fit),
91            ms(self.process_curve),
92        );
93        profile_line!(
94            "imprint.clip_fit rescue_march={:.2} clip={:.2} shared_boundary={:.2} fit={:.2} process={:.2} hermite={:.2} hermite_runs={} fit_runs={}",
95            ms(self.rescue_march),
96            ms(self.clip),
97            ms(self.shared_boundary),
98            ms(self.fit),
99            ms(self.process),
100            ms(self.hermite),
101            self.hermite_runs,
102            self.fit_runs,
103        );
104        profile_line!(
105            "imprint.fits pairs={} exact_lanes={} exact_iso_sections={} exact_iso_declined={}",
106            self.pairs_fits,
107            self.exact_lane_fits,
108            self.exact_iso_sections,
109            self.exact_iso_declined,
110        );
111        driver::fit_split_report();
112    }
113}
114
115/// Lane P3b row 0 (2026-10-26): where `process_curve`'s crossing loop, the
116/// lies-on span search and the boundary-reuse pass spend their time, read only
117/// under `BREP_PROFILE` and printed once per `build_imprints` as
118/// `imprint.split`. Thread-local because the builder and the support
119/// functions have no profile handle; `take` drains it for the report.
120#[derive(Default, Clone, Copy)]
121pub(crate) struct ImprintSplit {
122    pub pierce: Duration,
123    pub pierce_edges: u64,
124    pub overlap: Duration,
125    pub overlap_edges: u64,
126    pub crossing: Duration,
127    pub crossing_edges: u64,
128    pub recover: Duration,
129    pub recover_pierces: u64,
130    pub lies_whole: Duration,
131    pub lies_whole_calls: u64,
132    pub lies_whole_hits: u64,
133    pub lies_span: Duration,
134    pub lies_span_calls: u64,
135    pub reuse_edges: u64,
136}
137thread_local! {
138    static IMPRINT_SPLIT: std::cell::Cell<ImprintSplit> = const { std::cell::Cell::new(ImprintSplit {
139        pierce: Duration::ZERO, pierce_edges: 0, overlap: Duration::ZERO, overlap_edges: 0,
140        crossing: Duration::ZERO, crossing_edges: 0, recover: Duration::ZERO, recover_pierces: 0,
141        lies_whole: Duration::ZERO, lies_whole_calls: 0, lies_whole_hits: 0,
142        lies_span: Duration::ZERO, lies_span_calls: 0, reuse_edges: 0,
143    }) };
144}
145pub(crate) fn imprint_split_add(update: impl FnOnce(&mut ImprintSplit)) {
146    IMPRINT_SPLIT.with(|cell| { let mut split = cell.get(); update(&mut split); cell.set(split); });
147}
148pub(crate) fn imprint_split_take() -> ImprintSplit {
149    IMPRINT_SPLIT.with(|cell| cell.replace(ImprintSplit::default()))
150}
151
152/// Coarse observation of optional contact/graph calls, including early
153/// declines. It owns no geometry and does not change caller diagnostics.
154struct ImprintSubphaseClock {
155    started: Option<Instant>,
156    label: &'static str,
157}
158impl ImprintSubphaseClock {
159    fn new(label: &'static str) -> Self {
160        Self { started: profile_enabled().then(Instant::now), label }
161    }
162}
163impl Drop for ImprintSubphaseClock {
164    fn drop(&mut self) {
165        if let Some(started) = self.started {
166            profile_line!("imprint.subphase label={} ms={:.3}", self.label, started.elapsed().as_secs_f64() * 1000.0);
167        }
168    }
169}
170
171#[derive(Clone, Debug, Deserialize)]
172pub struct ImprintOptions {
173    #[serde(default = "default_tolerance")]
174    pub tolerance: f64,
175    #[serde(default = "default_maximum_fit_points")]
176    pub maximum_fit_points: usize,
177    #[serde(default)]
178    pub local_fit: bool,
179    #[serde(default)]
180    pub fit_chunk_points: Option<usize>,
181    pub maximum_ssi_step: Option<f64>,
182    /// `None`, the default, marches exactly as before. `BREP_SPAN_BUDGET`
183    /// overrides either way: `fit` is the pair's `intersection_fit` at the
184    /// operands' scale, a number is an absolute budget, `off` is none.
185    /// `BREP_SPAN_CENSUS=<file>` appends one line per clipped section with its
186    /// stations, deviation and refine route (`span-budget census:`), labelled
187    /// by `BREP_CENSUS_LABEL`.
188    #[serde(default)]
189    pub span_budget: Option<f64>,
190    /// Implies a span budget of the pair's `intersection_fit` when none is
191    /// set. Off by default; `BREP_HERMITE_SECTIONS` overrides either way (`0`
192    /// off, anything else on).
193    #[serde(default)]
194    pub hermite_sections: bool,
195    /// The TANGENT-CURVE lane: where an exact lane proves a pair's transverse
196    /// intersection empty, or the tangential-only gate would skip it, and the
197    /// pair classification reads near-tangent, look for an EXTENDED contact
198    /// (`crate::tangent_curve`), march it with one planar carrier, and record it
199    /// as a typed tangent section ([`ImprintResultRecord::tangent_sections`]).
200    /// The boolean then names the contact in any refusal it raises on the pair
201    /// instead of leaking the arrangement's own message. On by default since the
202    /// census of 2026-10-10 (every manifest leaf identical with the lane on and
203    /// off, `hlr_tests` green both ways); the section is recorded, never
204    /// imprinted, so nothing the boolean builds changes.
205    /// `BREP_TANGENT_CURVE_LANE=0` is the escape hatch. Appended at the end of
206    /// the struct (2026-10-10).
207    #[serde(default = "default_tangent_curve_lane")]
208    pub tangent_curve_lane: bool,
209    /// EXACT PCURVES FOR THE PLANAR-ISO LANE (slice 1, 2026-10-11, the
210    /// catalogue's P3 applied to the exact lane): a section the planar-iso
211    /// lane answers is an iso-curve of the ruled carrier and an affine image
212    /// on the plane, so both pcurves are written down on the curve's own
213    /// knots (`support::planar_iso_section`) instead of fitted, and the
214    /// junction pass moves their ends instead of re-marching them. Off by
215    /// default (the fitted pcurves are the shipped bytes);
216    /// `BREP_EXACT_ISO_PCURVES` overrides either way: `0`/`off`, `on`, or
217    /// `check`, which fits as before and appends one census line per pcurve
218    /// comparing the fitted and the exact one in 3-D
219    /// (`BREP_EXACT_ISO_CENSUS=<file>`, labelled by `BREP_CENSUS_LABEL`). A
220    /// face read in a lifted chart declines the exact pcurve (the iso line
221    /// is in the carrier's frame). Appended at the end of the struct.
222    #[serde(default)]
223    pub exact_iso_pcurves: bool,
224    /// MARCH STARTS: every surface/surface march the imprint runs traces
225    /// its starts in order (explicit trim-boundary pierces first, the most
226    /// transverse first, a run-through start consumed with a snapped
227    /// station) and seeds its interior candidates from the two carriers'
228    /// knot-span leaves instead of a fixed grid
229    /// ([`SurfaceIntersectionOptions::ordered_starts`],
230    /// [`SurfaceIntersectionOptions::tree_seeds`]). Off by default (the
231    /// grid-then-seeds march is the shipped one, byte for byte);
232    /// `BREP_MARCH_STARTS` overrides either way: `0`/`off`, `order` (A3's
233    /// ordering alone), `snap` (the ordering with run-through starts
234    /// consumed), `tree` (A4 alone), `order+tree`, anything else all three.
235    /// Appended at the end of the struct.
236    #[serde(default)]
237    pub march_starts: bool,
238    /// Candidates for one junction are captured within the wide residual
239    /// band; two combine only when they are connected (share a support face)
240    /// and lie within the strict radius built from their own evidence radii;
241    /// the surviving position is the lowest-dimensional candidate's, an
242    /// operand vertex over an operand edge's crossing over a section end on
243    /// two carriers. Off by default until the census in the record promotes
244    /// it; `BREP_JUNCTION_COMBINE` overrides either way (`0`/`off`, `check`
245    /// for the census that logs every junction's candidates to
246    /// `BREP_JUNCTION_CENSUS` without moving anything, anything else on).
247    /// Appended at the end of the struct (2026-10-11).
248    #[serde(default)]
249    pub two_radius_junctions: bool,
250    /// IMPRINT THE TANGENT SECTION (the pinched result): a planar-carrier
251    /// contact the tangent-curve lane traced is also IMPRINTED —
252    /// `process_curve` on the clipped run, so the contact becomes a section
253    /// piece on both carriers, the operand edges its ends lie on are split
254    /// there, and every earlier piece the ends lie on (the through-bore's cap
255    /// circles) is split at them — and its piece ids are recorded in
256    /// [`TangentSectionRecord::piece_ids`]. The boolean's assembler then
257    /// pairs the four coedges of such a piece by MATERIAL SIDE (two edge
258    /// records on one locus, each manifold) instead of by arrival, so the
259    /// grazing through-bore builds as one shell whose tangent edge is carried
260    /// twice. On by default since the census of 2026-10-11 (the battery
261    /// moving only the pins that asserted the old refusal and the recentre
262    /// pin whose unguarded far fillet now completes);
263    /// `BREP_TANGENT_CURVE_IMPRINT=0` is the escape hatch. Requires
264    /// `tangent_curve_lane`. Appended at the end of the struct (2026-10-11).
265    #[serde(default = "default_tangent_curve_imprint")]
266    pub tangent_curve_imprint: bool,
267}
268
269fn default_tolerance() -> f64 {
270    1e-7
271}
272
273fn default_tangent_curve_lane() -> bool {
274    switch!(live "BREP_TANGENT_CURVE_LANE", true)
275}
276
277fn default_tangent_curve_imprint() -> bool {
278    switch!(live "BREP_TANGENT_CURVE_IMPRINT", true)
279}
280
281fn default_maximum_fit_points() -> usize {
282    80
283}
284
285/// Angular band inside which two surface normals count as PARALLEL when a face
286/// pair is classified (`|n_a × n_b| <= PAIR_ANGULAR_TOLERANCE` ⇒ the carriers
287/// graze rather than cross there).  Single source for the pair classifier's
288/// angular tolerance and for the near-tangency test that picks the march step.
289const PAIR_ANGULAR_TOLERANCE: f64 = 1e-4;
290
291/// Whether the imprint reads `first` and `second` as ONE surface (the
292/// sampled pair classification's `Cosurface`, or `cosurface_pair`) and
293/// exchanges boundary curves across them instead of intersecting them. The
294/// feature-scale measure (`crate::feature_scale`) records no sliver between
295/// such a pair: the revolve pole fixtures' side planes and their copies stand
296/// 3.8e-6 apart over the region and follow the coincident-face rule.
297pub(crate) fn reads_as_one_surface(first: &NurbsSurface, second: &NurbsSurface, tolerance: f64) -> Result<bool, String> {
298    let relation = crate::classify_surface_pair(first, second, tolerance, PAIR_ANGULAR_TOLERANCE)?.relation;
299    Ok(relation == SurfacePairRelation::Cosurface
300        || support::cosurface_pair(first, second, tolerance).map_err(|refusal| refusal.message)?)
301}
302
303/// Fraction of the part extent used as the FLOOR of the near-tangency march
304/// step when the two carriers actually touch (`minimum_separation == 0`), so
305/// the reachable curve length is a fixed fraction of the model rather than an
306/// absolute distance.  See `march_maximum_step`.
307const NEAR_TANGENT_STEP_FRACTION: f64 = 1e-3;
308
309/// Span band for the B2 shared-section-edge decision, as a FRACTION of the part
310/// extent (so it tracks feature size, not an absolute distance). A SSI section
311/// is declared coincident with an existing boundary edge only when it lies
312/// within `SHARED_SECTION_BAND_FRACTION * extent` of that edge along its whole
313/// span in BOTH directions AND their endpoints already coincide within the weld
314/// radius. Sized to admit a genuine near-tangent GRAZE (the marched/analytic
315/// section drifts from the vendor edge by a graze-scale gap two-three orders
316/// above the fit residual — the B1-falsification measurement) while staying far
317/// below the feature scale at which a distinct same-endpoint edge diverges.
318/// Measured on fixture 09 (extent ~3.23, band ~3.23e-3): the two genuine graze
319/// gaps are 8.2e-4 and 1.67e-3 (ratios 2.5e-4 / 5.2e-4) — comfortably inside;
320/// fixtures 12/04 (the wrong-reuse tripwires) produce ZERO reuses and the
321/// prim×prim fuzz shows 0/2800 outcome changes, so the band is well separated
322/// from any distinct-edge scale.
323const SHARED_SECTION_BAND_FRACTION: f64 = 1e-3;
324
325/// Maximum march step for one face pair.
326///
327/// A NEAR-TANGENT pair marches finely: where the carriers graze, the corrector
328/// can hop between the two sheets of the contact, so the step stays inside the
329/// separation band.  Two things that step must NOT be:
330///
331/// * **applied to a pair that only crosses.** A pair is classified `Singular`
332///   when any sample near the contact has a DEGENERATE normal (a pole of a
333///   revolution carrier) — that says nothing about tangency, and the pair can
334///   cross at 90°.  Marching a transverse crossing at the tangency step crawls,
335///   and a curve longer than `maximum_steps × step` then dies as "trace
336///   exhausted" (2026-08-06 report: a draft-angle extrude wall against a
337///   revolve-of-revolve face — a 2.0-unit curve needing 4014 steps of 4000,
338///   10 points once marched at its own scale).  The step is therefore gated on
339///   the classification's own tangency evidence, not on the relation label.
340/// * **an absolute distance.** A fixed floor is a different fraction of every
341///   model, so the reachable curve length depended on part size.  The floor is
342///   a fraction of the part extent instead; `solid_scale` is at least 1, so
343///   unit-scale parts keep the historical `1e-3` exactly.
344fn march_maximum_step(
345    classification: &SurfacePairClassification,
346    requested: Option<f64>,
347    scale: f64,
348) -> Option<f64> {
349    let grazes = matches!(
350        classification.relation,
351        SurfacePairRelation::NearTangent | SurfacePairRelation::Singular
352    ) && classification.minimum_normal_cross <= PAIR_ANGULAR_TOLERANCE;
353    if !grazes {
354        return requested;
355    }
356    Some(
357        requested.unwrap_or(
358            classification
359                .minimum_separation
360                .max(NEAR_TANGENT_STEP_FRACTION * scale),
361        ) * 0.5,
362    )
363}
364
365impl Default for ImprintOptions {
366    fn default() -> Self {
367        Self {
368            tolerance: default_tolerance(),
369            maximum_fit_points: default_maximum_fit_points(),
370            local_fit: false,
371            fit_chunk_points: None,
372            maximum_ssi_step: None,
373            span_budget: None,
374            hermite_sections: false,
375            tangent_curve_lane: default_tangent_curve_lane(),
376            tangent_curve_imprint: default_tangent_curve_imprint(),
377            exact_iso_pcurves: false,
378            march_starts: false,
379            two_radius_junctions: false,
380        }
381    }
382}
383
384#[derive(Clone, Debug, Deserialize, Serialize)]
385pub struct ImprintVertex {
386    pub id: u64,
387    pub point: Vec3,
388}
389
390#[derive(Clone, Debug, Deserialize, Serialize)]
391pub struct FacePcurve {
392    pub operand: u8,
393    pub face_id: u64,
394    pub pcurve: NurbsCurve,
395}
396
397#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
398pub struct FaceKey {
399    pub operand: u8,
400    pub face_id: u64,
401}
402
403#[derive(Clone, Debug, Deserialize, Serialize)]
404pub struct ImprintPieceRecord {
405    pub id: u64,
406    pub curve: NurbsCurve,
407    pub t0: f64,
408    pub t1: f64,
409    pub start_vertex_id: u64,
410    pub end_vertex_id: u64,
411    pub pcurves: Vec<FacePcurve>,
412    pub support_faces: [FaceKey; 2],
413    /// OCCT common-block / `IsExistingPaveBlock` (B2): when this SSI section
414    /// curve was found to COINCIDE along its whole span with an EXISTING
415    /// boundary edge of one of its support faces, the section is not a new
416    /// 1-cell — it IS that boundary edge. Records `(operand, edge_id, aligned)`
417    /// of the existing edge to REUSE; `aligned` is whether the section's
418    /// start→end runs the same direction as the edge's start→end. The
419    /// assembler then resolves this piece to the shared boundary edge's
420    /// identity so both operands reference ONE edge (no duplicate section /
421    /// one-use edge). `None` = an ordinary freshly-minted section (the
422    /// historical behaviour; also what `BREP_SHARED_SECTION_EDGE=0` forces).
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub shared_edge: Option<(u8, u64, bool)>,
425}
426
427#[derive(Clone, Debug, Deserialize, Serialize)]
428pub struct EdgeSplitRecord {
429    pub operand: u8,
430    pub edge_id: u64,
431    pub parameters: Vec<f64>,
432}
433
434#[derive(Clone, Debug, Deserialize, Serialize)]
435pub struct FaceImprints {
436    pub operand: u8,
437    pub face_id: u64,
438    pub piece_ids: Vec<u64>,
439}
440
441#[derive(Clone, Debug, Deserialize, Serialize)]
442pub struct ImprintResultRecord {
443    pub vertices: Vec<ImprintVertex>,
444    pub pieces: Vec<ImprintPieceRecord>,
445    pub by_face: Vec<FaceImprints>,
446    pub edge_splits: Vec<EdgeSplitRecord>,
447    /// Operand edges geometrically OVERLAPPED by a section curve (the
448    /// cosurface/coincident locus: an inscribed sphere's contact circle
449    /// riding a cap ring). Fragment-selection fate must never flood across
450    /// them — the two sides of such an edge can lie on opposite sides of
451    /// the other operand (the cap disc inside, the wall outside).
452    #[serde(default)]
453    pub barrier_edges: Vec<(u8, u64)>,
454    /// Every ISOLATED TANGENT NODE the imprint admitted: a point where the two
455    /// carriers touch with parallel normals and the section crosses itself, so
456    /// the pair was marched rather than refused (see
457    /// `imprint/tangent_contact.rs`). The boolean attributes a later tearing to
458    /// these rather than reporting an anonymous degeneracy — the second-order
459    /// filter cannot decide a third-order singularity, so the assembly is the
460    /// authority on whether a node it admitted was really imprintable.
461    #[serde(default)]
462    pub tangent_nodes: Vec<Vec3>,
463    /// True when the imprint saw ANY surface-contact evidence — an accepted
464    /// pierce seed, a traced SSI branch (even one later clipped away), or a
465    /// minted piece. Pure material disjointness leaves none, while a
466    /// silently-lost section always leaves at least the upstream evidence, so
467    /// the boolean's legitimate-empty adjudication requires this to be false.
468    #[serde(default)]
469    pub section_evidence: bool,
470    /// Every face pair the imprint read as ONE surface and exchanged boundary
471    /// curves across instead of intersecting (`cosurface_pair`, or the sampled
472    /// pair classification's `Cosurface`). Selection holds a fragment whose
473    /// point lies in such a partner's trim On against it, so the coincidence
474    /// the sections were built on is the one the fragments are kept by.
475    #[serde(default)]
476    pub cosurface_pairs: Vec<(FaceKey, FaceKey)>,
477    /// Every EXTENDED tangential contact the tangent-curve lane recognised
478    /// ([`ImprintOptions::tangent_curve_lane`]): the carriers touch along a
479    /// curve inside both trims. A planar-carrier contact is marched and the
480    /// clipped run described; a curved/curved contact is named at its point
481    /// and not traced. None of these is imprinted yet — the record exists so
482    /// the boolean can attribute a refusal on the pair to the contact by name.
483    /// Appended at the end of the struct (2026-10-10).
484    #[serde(default)]
485    pub tangent_sections: Vec<TangentSectionRecord>,
486}
487
488/// One extended tangential contact the imprint recognised (see
489/// [`ImprintResultRecord::tangent_sections`]).
490#[derive(Clone, Debug, Deserialize, Serialize)]
491pub struct TangentSectionRecord {
492    pub first: FaceKey,
493    pub second: FaceKey,
494    /// Ends of the longest run of the traced contact inside both trims (the
495    /// contact point itself when the contact was not traced).
496    pub start: Vec3,
497    pub end: Vec3,
498    /// Chord length of that run.
499    pub length: f64,
500    /// The run is the whole traced curve and the curve closed on itself.
501    pub closed: bool,
502    /// Stations of the run.
503    pub stations: usize,
504    /// One carrier is a plane: the branch the lane traces. `false` names a
505    /// curved/curved contact the lane recognises but does not trace.
506    pub planar: bool,
507    /// The faces the contact INVOLVES: the two carriers' faces, and every face
508    /// of either operand that shares an edge the run's ends lie on (the
509    /// through-bore's contact line ends on the wall's top and bottom edges,
510    /// so the box's top and bottom faces are involved; a blind pocket's ends
511    /// lie on the bore's cap rings, so the caps are). The boolean's
512    /// attribution rewrites a refusal only when a face it names is in here.
513    #[serde(default)]
514    pub faces: Vec<FaceKey>,
515    /// The section pieces the contact was imprinted as
516    /// ([`ImprintOptions::tangent_curve_imprint`]): `process_curve`'s pieces
517    /// on the clipped run, in mint order. Empty when the section was recorded
518    /// only. The assembler pairs the coedges of these pieces by material side.
519    /// Appended at the end of the struct (2026-10-11).
520    #[serde(default, skip_serializing_if = "Vec::is_empty")]
521    pub piece_ids: Vec<u64>,
522    /// GRAZING PIERCES WITHOUT A SECTION (2026-10-17, the revolve-into-bore
523    /// report): a trim edge of one face pierces the other carrier INSIDE both
524    /// trims, but at an angle below the seed gate, and the march found no
525    /// branch for the pair. The pierce proves a section crosses the faces
526    /// there; the empty march says the crossing is too near tangency to walk
527    /// (a third-order contact: the branch is born tangent to a contact curve
528    /// on a shared boundary, `boolean::attribute_to_tangent_node`). Each
529    /// entry is the pierce point and its `|edge_tangent · normal|`. Such a
530    /// record is attribution only — nothing is imprinted — and names the
531    /// contact when the assembly tears downstream of it. Appended at the end
532    /// of the struct.
533    #[serde(default, skip_serializing_if = "Vec::is_empty")]
534    pub grazing: Vec<(Vec3, f64)>,
535}
536
537impl TangentSectionRecord {
538    /// A record standing on its grazing pierces ALONE — the lane found no
539    /// contact to name, so the driver set its ends to the pierces (its
540    /// `start` IS the first pierce, bit for bit, and nothing was traced). The
541    /// boolean's attribution reads such a record only as a fallback: a
542    /// contact the lane found, or a tangent node the imprint classified, is
543    /// the nearer cause and keeps its own wording.
544    pub fn pierce_only(&self) -> bool {
545        self.stations == 0
546            && self
547                .grazing
548                .first()
549                .is_some_and(|(point, _)| point.sub(self.start).length() == 0.0)
550    }
551}
552
553#[path = "imprint/ruled_torus.rs"]
554mod ruled_torus;
555pub(crate) use ruled_torus::{circular_revolution_profile, construct_ruled_torus, RuledTorusBoundary};
556
557#[path = "imprint/support.rs"]
558mod support;
559#[path = "imprint/builder.rs"]
560mod builder;
561#[path = "imprint/junctions.rs"]
562mod junctions;
563#[path = "imprint/self_touch.rs"]
564mod self_touch;
565#[path = "imprint/sections.rs"]
566mod sections;
567#[path = "imprint/tangent_contact.rs"]
568mod tangent_contact;
569#[path = "imprint/driver.rs"]
570mod driver;
571#[path = "imprint/work_profile.rs"]
572pub(crate) mod work_profile;
573#[path = "imprint/reuse_profile.rs"]
574pub(crate) mod reuse_profile;
575#[path = "imprint/hermite.rs"]
576mod hermite;
577pub(crate) use hermite::{carried_uv_enabled, carried_uv_min_stations};
578
579#[path = "imprint/gate_census.rs"]
580mod gate_census;
581
582use builder::*;
583use junctions::*;
584use sections::*;
585use support::*;
586
587#[path = "imprint/section_graph.rs"]
588mod section_graph;
589pub(crate) use section_graph::{plane_section_graph, plane_section_graph_range, plane_section_graph_range_bounded};
590
591/// Reuse imprint's exact ruled-carrier section at the assembly repair boundary.
592/// This exposes the construction, not its arrangement or acceptance policy.
593pub(crate) fn planar_carrier_section(
594    first: &NurbsSurface,
595    second: &NurbsSurface,
596    tolerance: f64,
597) -> Result<Option<NurbsCurve>, crate::KernelRefusal> {
598    support::planar_iso_intersection(first,second,tolerance)
599}
600
601use tangent_contact::classify_tangent_contact;
602
603pub use driver::build_imprints;
604pub(crate) use driver::{build_carrier_imprints, build_carrier_imprints_within, take_rescue_firings};
605pub(crate) use self_touch::self_touch_edge_splits;
606pub(crate) use support::clear_window_memo;
607pub(crate) use self_touch::clear_touch_memo;
608
609
610#[path = "imprint/pcurve_cache.rs"]
611mod pcurve_cache;
612use pcurve_cache::cached_build_pcurve as build_pcurve_on_surface_marched;
613
614#[path = "imprint/intersection_cache.rs"]
615mod intersection_cache;
616use intersection_cache::intersect as intersect_curve_surface;