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, intersect_curve_surface, 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/// Per-step wall-clock accumulators for `build_imprints`, printed to stderr
22/// when `BREP_PROFILE` is set. Native-only in practice (no env vars in wasm).
23#[derive(Default)]
24struct ImprintProfile {
25    enabled: bool,
26    pairs: u64,
27    marched_pairs: u64,
28    classify: Duration,
29    cosurface: Duration,
30    lies_on: Duration,
31    planar_iso: Duration,
32    analytic: Duration,
33    seeds: Duration,
34    march: Duration,
35    clip_and_fit: Duration,
36    process_curve: Duration,
37    /// `clip_and_fit` split: the swapped-order rescue marches, the trim clip,
38    /// the shared-boundary test, the polyline fit, and `process_curve`.
39    rescue_march: Duration,
40    clip: Duration,
41    shared_boundary: Duration,
42    fit: Duration,
43    process: Duration,
44    hermite_runs: u64,
45    fit_runs: u64,
46    hermite: Duration,
47    /// Appended at the end.
48    pairs_fits: u64,
49    exact_lane_fits: u64,
50    /// Slice 1: planar-iso sections whose exact pcurves were handed to the
51    /// builder, and those declined (a lifted chart, or no pcurve built).
52    exact_iso_sections: u64,
53    exact_iso_declined: u64,
54}
55
56impl ImprintProfile {
57    fn new() -> Self {
58        Self {
59            enabled: std::env::var("BREP_PROFILE").is_ok(),
60            ..Self::default()
61        }
62    }
63
64    fn lap(&self, started: &mut Option<Instant>) -> Duration {
65        if !self.enabled {
66            return Duration::ZERO;
67        }
68        let now = Instant::now();
69        let elapsed = started.map(|s| now - s).unwrap_or_default();
70        *started = Some(now);
71        elapsed
72    }
73
74    fn report(&self) {
75        if !self.enabled {
76            return;
77        }
78        let ms = |d: Duration| d.as_secs_f64() * 1_000.0;
79        eprintln!(
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        eprintln!(
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        eprintln!(
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    }
112}
113
114#[derive(Clone, Debug, Deserialize)]
115pub struct ImprintOptions {
116    #[serde(default = "default_tolerance")]
117    pub tolerance: f64,
118    #[serde(default = "default_maximum_fit_points")]
119    pub maximum_fit_points: usize,
120    #[serde(default)]
121    pub local_fit: bool,
122    #[serde(default)]
123    pub fit_chunk_points: Option<usize>,
124    pub maximum_ssi_step: Option<f64>,
125    /// `None`, the default, marches exactly as before. `BREP_SPAN_BUDGET`
126    /// overrides either way: `fit` is the pair's `intersection_fit` at the
127    /// operands' scale, a number is an absolute budget, `off` is none.
128    /// `BREP_SPAN_CENSUS=<file>` appends one line per clipped section with its
129    /// stations, deviation and refine route (`span-budget census:`), labelled
130    /// by `BREP_CENSUS_LABEL`.
131    #[serde(default)]
132    pub span_budget: Option<f64>,
133    /// Implies a span budget of the pair's `intersection_fit` when none is
134    /// set. Off by default; `BREP_HERMITE_SECTIONS` overrides either way (`0`
135    /// off, anything else on).
136    #[serde(default)]
137    pub hermite_sections: bool,
138    /// The TANGENT-CURVE lane: where an exact lane proves a pair's transverse
139    /// intersection empty, or the tangential-only gate would skip it, and the
140    /// pair classification reads near-tangent, look for an EXTENDED contact
141    /// (`crate::tangent_curve`), march it with one planar carrier, and record it
142    /// as a typed tangent section ([`ImprintResultRecord::tangent_sections`]).
143    /// The boolean then names the contact in any refusal it raises on the pair
144    /// instead of leaking the arrangement's own message. On by default since the
145    /// census of 2026-10-10 (every manifest leaf identical with the lane on and
146    /// off, `hlr_tests` green both ways); the section is recorded, never
147    /// imprinted, so nothing the boolean builds changes.
148    /// `BREP_TANGENT_CURVE_LANE=0` is the escape hatch. Appended at the end of
149    /// the struct (2026-10-10).
150    #[serde(default = "default_tangent_curve_lane")]
151    pub tangent_curve_lane: bool,
152    /// EXACT PCURVES FOR THE PLANAR-ISO LANE (slice 1, 2026-10-11, the
153    /// catalogue's P3 applied to the exact lane): a section the planar-iso
154    /// lane answers is an iso-curve of the ruled carrier and an affine image
155    /// on the plane, so both pcurves are written down on the curve's own
156    /// knots (`support::planar_iso_section`) instead of fitted, and the
157    /// junction pass moves their ends instead of re-marching them. Off by
158    /// default (the fitted pcurves are the shipped bytes);
159    /// `BREP_EXACT_ISO_PCURVES` overrides either way: `0`/`off`, `on`, or
160    /// `check`, which fits as before and appends one census line per pcurve
161    /// comparing the fitted and the exact one in 3-D
162    /// (`BREP_EXACT_ISO_CENSUS=<file>`, labelled by `BREP_CENSUS_LABEL`). A
163    /// face read in a lifted chart declines the exact pcurve (the iso line
164    /// is in the carrier's frame). Appended at the end of the struct.
165    #[serde(default)]
166    pub exact_iso_pcurves: bool,
167    /// MARCH STARTS: every surface/surface march the imprint runs traces
168    /// its starts in order (explicit trim-boundary pierces first, the most
169    /// transverse first, a run-through start consumed with a snapped
170    /// station) and seeds its interior candidates from the two carriers'
171    /// knot-span leaves instead of a fixed grid
172    /// ([`SurfaceIntersectionOptions::ordered_starts`],
173    /// [`SurfaceIntersectionOptions::tree_seeds`]). Off by default (the
174    /// grid-then-seeds march is the shipped one, byte for byte);
175    /// `BREP_MARCH_STARTS` overrides either way: `0`/`off`, `order` (A3's
176    /// ordering alone), `snap` (the ordering with run-through starts
177    /// consumed), `tree` (A4 alone), `order+tree`, anything else all three.
178    /// Appended at the end of the struct.
179    #[serde(default)]
180    pub march_starts: bool,
181    /// Candidates for one junction are captured within the wide residual
182    /// band; two combine only when they are connected (share a support face)
183    /// and lie within the strict radius built from their own evidence radii;
184    /// the surviving position is the lowest-dimensional candidate's, an
185    /// operand vertex over an operand edge's crossing over a section end on
186    /// two carriers. Off by default until the census in the record promotes
187    /// it; `BREP_JUNCTION_COMBINE` overrides either way (`0`/`off`, `check`
188    /// for the census that logs every junction's candidates to
189    /// `BREP_JUNCTION_CENSUS` without moving anything, anything else on).
190    /// Appended at the end of the struct (2026-10-11).
191    #[serde(default)]
192    pub two_radius_junctions: bool,
193    /// IMPRINT THE TANGENT SECTION (the pinched result): a planar-carrier
194    /// contact the tangent-curve lane traced is also IMPRINTED —
195    /// `process_curve` on the clipped run, so the contact becomes a section
196    /// piece on both carriers, the operand edges its ends lie on are split
197    /// there, and every earlier piece the ends lie on (the through-bore's cap
198    /// circles) is split at them — and its piece ids are recorded in
199    /// [`TangentSectionRecord::piece_ids`]. The boolean's assembler then
200    /// pairs the four coedges of such a piece by MATERIAL SIDE (two edge
201    /// records on one locus, each manifold) instead of by arrival, so the
202    /// grazing through-bore builds as one shell whose tangent edge is carried
203    /// twice. On by default since the census of 2026-10-11 (the battery
204    /// moving only the pins that asserted the old refusal and the recentre
205    /// pin whose unguarded far fillet now completes);
206    /// `BREP_TANGENT_CURVE_IMPRINT=0` is the escape hatch. Requires
207    /// `tangent_curve_lane`. Appended at the end of the struct (2026-10-11).
208    #[serde(default = "default_tangent_curve_imprint")]
209    pub tangent_curve_imprint: bool,
210}
211
212fn default_tolerance() -> f64 {
213    1e-7
214}
215
216fn default_tangent_curve_lane() -> bool {
217    std::env::var("BREP_TANGENT_CURVE_LANE").as_deref() != Ok("0")
218}
219
220fn default_tangent_curve_imprint() -> bool {
221    std::env::var("BREP_TANGENT_CURVE_IMPRINT").as_deref() != Ok("0")
222}
223
224fn default_maximum_fit_points() -> usize {
225    80
226}
227
228/// Angular band inside which two surface normals count as PARALLEL when a face
229/// pair is classified (`|n_a × n_b| <= PAIR_ANGULAR_TOLERANCE` ⇒ the carriers
230/// graze rather than cross there).  Single source for the pair classifier's
231/// angular tolerance and for the near-tangency test that picks the march step.
232const PAIR_ANGULAR_TOLERANCE: f64 = 1e-4;
233
234/// Fraction of the part extent used as the FLOOR of the near-tangency march
235/// step when the two carriers actually touch (`minimum_separation == 0`), so
236/// the reachable curve length is a fixed fraction of the model rather than an
237/// absolute distance.  See `march_maximum_step`.
238const NEAR_TANGENT_STEP_FRACTION: f64 = 1e-3;
239
240/// Span band for the B2 shared-section-edge decision, as a FRACTION of the part
241/// extent (so it tracks feature size, not an absolute distance). A SSI section
242/// is declared coincident with an existing boundary edge only when it lies
243/// within `SHARED_SECTION_BAND_FRACTION * extent` of that edge along its whole
244/// span in BOTH directions AND their endpoints already coincide within the weld
245/// radius. Sized to admit a genuine near-tangent GRAZE (the marched/analytic
246/// section drifts from the vendor edge by a graze-scale gap two-three orders
247/// above the fit residual — the B1-falsification measurement) while staying far
248/// below the feature scale at which a distinct same-endpoint edge diverges.
249/// Measured on fixture 09 (extent ~3.23, band ~3.23e-3): the two genuine graze
250/// gaps are 8.2e-4 and 1.67e-3 (ratios 2.5e-4 / 5.2e-4) — comfortably inside;
251/// fixtures 12/04 (the wrong-reuse tripwires) produce ZERO reuses and the
252/// prim×prim fuzz shows 0/2800 outcome changes, so the band is well separated
253/// from any distinct-edge scale.
254const SHARED_SECTION_BAND_FRACTION: f64 = 1e-3;
255
256/// Maximum march step for one face pair.
257///
258/// A NEAR-TANGENT pair marches finely: where the carriers graze, the corrector
259/// can hop between the two sheets of the contact, so the step stays inside the
260/// separation band.  Two things that step must NOT be:
261///
262/// * **applied to a pair that only crosses.** A pair is classified `Singular`
263///   when any sample near the contact has a DEGENERATE normal (a pole of a
264///   revolution carrier) — that says nothing about tangency, and the pair can
265///   cross at 90°.  Marching a transverse crossing at the tangency step crawls,
266///   and a curve longer than `maximum_steps × step` then dies as "trace
267///   exhausted" (2026-08-06 report: a draft-angle extrude wall against a
268///   revolve-of-revolve face — a 2.0-unit curve needing 4014 steps of 4000,
269///   10 points once marched at its own scale).  The step is therefore gated on
270///   the classification's own tangency evidence, not on the relation label.
271/// * **an absolute distance.** A fixed floor is a different fraction of every
272///   model, so the reachable curve length depended on part size.  The floor is
273///   a fraction of the part extent instead; `solid_scale` is at least 1, so
274///   unit-scale parts keep the historical `1e-3` exactly.
275fn march_maximum_step(
276    classification: &SurfacePairClassification,
277    requested: Option<f64>,
278    scale: f64,
279) -> Option<f64> {
280    let grazes = matches!(
281        classification.relation,
282        SurfacePairRelation::NearTangent | SurfacePairRelation::Singular
283    ) && classification.minimum_normal_cross <= PAIR_ANGULAR_TOLERANCE;
284    if !grazes {
285        return requested;
286    }
287    Some(
288        requested.unwrap_or(
289            classification
290                .minimum_separation
291                .max(NEAR_TANGENT_STEP_FRACTION * scale),
292        ) * 0.5,
293    )
294}
295
296impl Default for ImprintOptions {
297    fn default() -> Self {
298        Self {
299            tolerance: default_tolerance(),
300            maximum_fit_points: default_maximum_fit_points(),
301            local_fit: false,
302            fit_chunk_points: None,
303            maximum_ssi_step: None,
304            span_budget: None,
305            hermite_sections: false,
306            tangent_curve_lane: default_tangent_curve_lane(),
307            tangent_curve_imprint: default_tangent_curve_imprint(),
308            exact_iso_pcurves: false,
309            march_starts: false,
310            two_radius_junctions: false,
311        }
312    }
313}
314
315#[derive(Clone, Debug, Deserialize, Serialize)]
316pub struct ImprintVertex {
317    pub id: u64,
318    pub point: Vec3,
319}
320
321#[derive(Clone, Debug, Deserialize, Serialize)]
322pub struct FacePcurve {
323    pub operand: u8,
324    pub face_id: u64,
325    pub pcurve: NurbsCurve,
326}
327
328#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
329pub struct FaceKey {
330    pub operand: u8,
331    pub face_id: u64,
332}
333
334#[derive(Clone, Debug, Deserialize, Serialize)]
335pub struct ImprintPieceRecord {
336    pub id: u64,
337    pub curve: NurbsCurve,
338    pub t0: f64,
339    pub t1: f64,
340    pub start_vertex_id: u64,
341    pub end_vertex_id: u64,
342    pub pcurves: Vec<FacePcurve>,
343    pub support_faces: [FaceKey; 2],
344    /// OCCT common-block / `IsExistingPaveBlock` (B2): when this SSI section
345    /// curve was found to COINCIDE along its whole span with an EXISTING
346    /// boundary edge of one of its support faces, the section is not a new
347    /// 1-cell — it IS that boundary edge. Records `(operand, edge_id, aligned)`
348    /// of the existing edge to REUSE; `aligned` is whether the section's
349    /// start→end runs the same direction as the edge's start→end. The
350    /// assembler then resolves this piece to the shared boundary edge's
351    /// identity so both operands reference ONE edge (no duplicate section /
352    /// one-use edge). `None` = an ordinary freshly-minted section (the
353    /// historical behaviour; also what `BREP_SHARED_SECTION_EDGE=0` forces).
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    pub shared_edge: Option<(u8, u64, bool)>,
356}
357
358#[derive(Clone, Debug, Deserialize, Serialize)]
359pub struct EdgeSplitRecord {
360    pub operand: u8,
361    pub edge_id: u64,
362    pub parameters: Vec<f64>,
363}
364
365#[derive(Clone, Debug, Deserialize, Serialize)]
366pub struct FaceImprints {
367    pub operand: u8,
368    pub face_id: u64,
369    pub piece_ids: Vec<u64>,
370}
371
372#[derive(Clone, Debug, Deserialize, Serialize)]
373pub struct ImprintResultRecord {
374    pub vertices: Vec<ImprintVertex>,
375    pub pieces: Vec<ImprintPieceRecord>,
376    pub by_face: Vec<FaceImprints>,
377    pub edge_splits: Vec<EdgeSplitRecord>,
378    /// Operand edges geometrically OVERLAPPED by a section curve (the
379    /// cosurface/coincident locus: an inscribed sphere's contact circle
380    /// riding a cap ring). Fragment-selection fate must never flood across
381    /// them — the two sides of such an edge can lie on opposite sides of
382    /// the other operand (the cap disc inside, the wall outside).
383    #[serde(default)]
384    pub barrier_edges: Vec<(u8, u64)>,
385    /// Every ISOLATED TANGENT NODE the imprint admitted: a point where the two
386    /// carriers touch with parallel normals and the section crosses itself, so
387    /// the pair was marched rather than refused (see
388    /// `imprint/tangent_contact.rs`). The boolean attributes a later tearing to
389    /// these rather than reporting an anonymous degeneracy — the second-order
390    /// filter cannot decide a third-order singularity, so the assembly is the
391    /// authority on whether a node it admitted was really imprintable.
392    #[serde(default)]
393    pub tangent_nodes: Vec<Vec3>,
394    /// True when the imprint saw ANY surface-contact evidence — an accepted
395    /// pierce seed, a traced SSI branch (even one later clipped away), or a
396    /// minted piece. Pure material disjointness leaves none, while a
397    /// silently-lost section always leaves at least the upstream evidence, so
398    /// the boolean's legitimate-empty adjudication requires this to be false.
399    #[serde(default)]
400    pub section_evidence: bool,
401    /// Every face pair the imprint read as ONE surface and exchanged boundary
402    /// curves across instead of intersecting (`cosurface_pair`, or the sampled
403    /// pair classification's `Cosurface`). Selection holds a fragment whose
404    /// point lies in such a partner's trim On against it, so the coincidence
405    /// the sections were built on is the one the fragments are kept by.
406    #[serde(default)]
407    pub cosurface_pairs: Vec<(FaceKey, FaceKey)>,
408    /// Every EXTENDED tangential contact the tangent-curve lane recognised
409    /// ([`ImprintOptions::tangent_curve_lane`]): the carriers touch along a
410    /// curve inside both trims. A planar-carrier contact is marched and the
411    /// clipped run described; a curved/curved contact is named at its point
412    /// and not traced. None of these is imprinted yet — the record exists so
413    /// the boolean can attribute a refusal on the pair to the contact by name.
414    /// Appended at the end of the struct (2026-10-10).
415    #[serde(default)]
416    pub tangent_sections: Vec<TangentSectionRecord>,
417}
418
419/// One extended tangential contact the imprint recognised (see
420/// [`ImprintResultRecord::tangent_sections`]).
421#[derive(Clone, Debug, Deserialize, Serialize)]
422pub struct TangentSectionRecord {
423    pub first: FaceKey,
424    pub second: FaceKey,
425    /// Ends of the longest run of the traced contact inside both trims (the
426    /// contact point itself when the contact was not traced).
427    pub start: Vec3,
428    pub end: Vec3,
429    /// Chord length of that run.
430    pub length: f64,
431    /// The run is the whole traced curve and the curve closed on itself.
432    pub closed: bool,
433    /// Stations of the run.
434    pub stations: usize,
435    /// One carrier is a plane: the branch the lane traces. `false` names a
436    /// curved/curved contact the lane recognises but does not trace.
437    pub planar: bool,
438    /// The faces the contact INVOLVES: the two carriers' faces, and every face
439    /// of either operand that shares an edge the run's ends lie on (the
440    /// through-bore's contact line ends on the wall's top and bottom edges,
441    /// so the box's top and bottom faces are involved; a blind pocket's ends
442    /// lie on the bore's cap rings, so the caps are). The boolean's
443    /// attribution rewrites a refusal only when a face it names is in here.
444    #[serde(default)]
445    pub faces: Vec<FaceKey>,
446    /// The section pieces the contact was imprinted as
447    /// ([`ImprintOptions::tangent_curve_imprint`]): `process_curve`'s pieces
448    /// on the clipped run, in mint order. Empty when the section was recorded
449    /// only. The assembler pairs the coedges of these pieces by material side.
450    /// Appended at the end of the struct (2026-10-11).
451    #[serde(default, skip_serializing_if = "Vec::is_empty")]
452    pub piece_ids: Vec<u64>,
453}
454
455#[path = "imprint/ruled_torus.rs"]
456mod ruled_torus;
457pub(crate) use ruled_torus::{circular_revolution_profile, construct_ruled_torus, RuledTorusBoundary};
458
459#[path = "imprint/support.rs"]
460mod support;
461#[path = "imprint/builder.rs"]
462mod builder;
463#[path = "imprint/junctions.rs"]
464mod junctions;
465#[path = "imprint/self_touch.rs"]
466mod self_touch;
467#[path = "imprint/sections.rs"]
468mod sections;
469#[path = "imprint/tangent_contact.rs"]
470mod tangent_contact;
471#[path = "imprint/driver.rs"]
472mod driver;
473#[path = "imprint/hermite.rs"]
474mod hermite;
475
476#[path = "imprint/gate_census.rs"]
477mod gate_census;
478
479use builder::*;
480use junctions::*;
481use sections::*;
482use support::*;
483
484#[path = "imprint/section_graph.rs"]
485mod section_graph;
486pub(crate) use section_graph::{plane_section_graph, plane_section_graph_range, plane_section_graph_range_bounded};
487
488/// Reuse imprint's exact ruled-carrier section at the assembly repair boundary.
489/// This exposes the construction, not its arrangement or acceptance policy.
490pub(crate) fn planar_carrier_section(
491    first: &NurbsSurface,
492    second: &NurbsSurface,
493    tolerance: f64,
494) -> Result<Option<NurbsCurve>, crate::KernelRefusal> {
495    support::planar_iso_intersection(first,second,tolerance)
496}
497
498use tangent_contact::classify_tangent_contact;
499
500pub use driver::build_imprints;
501pub(crate) use driver::{build_carrier_imprints, take_rescue_firings};
502pub(crate) use self_touch::self_touch_edge_splits;
503
504
505#[path = "imprint/pcurve_cache.rs"]
506mod pcurve_cache;
507use pcurve_cache::cached_build_pcurve as build_pcurve_on_surface_marched;