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