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, build_pcurve_on_surface_marched, classify_surface_pair_cached,
10    fit_polyline, intersect_curve_surface,
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}
38
39impl ImprintProfile {
40    fn new() -> Self {
41        Self {
42            enabled: std::env::var("BREP_PROFILE").is_ok(),
43            ..Self::default()
44        }
45    }
46
47    fn lap(&self, started: &mut Option<Instant>) -> Duration {
48        if !self.enabled {
49            return Duration::ZERO;
50        }
51        let now = Instant::now();
52        let elapsed = started.map(|s| now - s).unwrap_or_default();
53        *started = Some(now);
54        elapsed
55    }
56
57    fn report(&self) {
58        if !self.enabled {
59            return;
60        }
61        let ms = |d: Duration| d.as_secs_f64() * 1_000.0;
62        eprintln!(
63            "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}",
64            self.pairs,
65            self.marched_pairs,
66            ms(self.classify),
67            ms(self.cosurface),
68            ms(self.lies_on),
69            ms(self.planar_iso),
70            ms(self.analytic),
71            ms(self.seeds),
72            ms(self.march),
73            ms(self.clip_and_fit),
74            ms(self.process_curve),
75        );
76    }
77}
78
79#[derive(Clone, Debug, Deserialize)]
80pub struct ImprintOptions {
81    #[serde(default = "default_tolerance")]
82    pub tolerance: f64,
83    #[serde(default = "default_maximum_fit_points")]
84    pub maximum_fit_points: usize,
85    #[serde(default)]
86    pub local_fit: bool,
87    #[serde(default)]
88    pub fit_chunk_points: Option<usize>,
89    pub maximum_ssi_step: Option<f64>,
90}
91
92fn default_tolerance() -> f64 {
93    1e-7
94}
95
96fn default_maximum_fit_points() -> usize {
97    80
98}
99
100/// Angular band inside which two surface normals count as PARALLEL when a face
101/// pair is classified (`|n_a × n_b| <= PAIR_ANGULAR_TOLERANCE` ⇒ the carriers
102/// graze rather than cross there).  Single source for the pair classifier's
103/// angular tolerance and for the near-tangency test that picks the march step.
104const PAIR_ANGULAR_TOLERANCE: f64 = 1e-4;
105
106/// Fraction of the part extent used as the FLOOR of the near-tangency march
107/// step when the two carriers actually touch (`minimum_separation == 0`), so
108/// the reachable curve length is a fixed fraction of the model rather than an
109/// absolute distance.  See `march_maximum_step`.
110const NEAR_TANGENT_STEP_FRACTION: f64 = 1e-3;
111
112/// Span band for the B2 shared-section-edge decision, as a FRACTION of the part
113/// extent (so it tracks feature size, not an absolute distance). A SSI section
114/// is declared coincident with an existing boundary edge only when it lies
115/// within `SHARED_SECTION_BAND_FRACTION * extent` of that edge along its whole
116/// span in BOTH directions AND their endpoints already coincide within the weld
117/// radius. Sized to admit a genuine near-tangent GRAZE (the marched/analytic
118/// section drifts from the vendor edge by a graze-scale gap two-three orders
119/// above the fit residual — the B1-falsification measurement) while staying far
120/// below the feature scale at which a distinct same-endpoint edge diverges.
121/// Measured on fixture 09 (extent ~3.23, band ~3.23e-3): the two genuine graze
122/// gaps are 8.2e-4 and 1.67e-3 (ratios 2.5e-4 / 5.2e-4) — comfortably inside;
123/// fixtures 12/04 (the wrong-reuse tripwires) produce ZERO reuses and the
124/// prim×prim fuzz shows 0/2800 outcome changes, so the band is well separated
125/// from any distinct-edge scale.
126const SHARED_SECTION_BAND_FRACTION: f64 = 1e-3;
127
128/// Maximum march step for one face pair.
129///
130/// A NEAR-TANGENT pair marches finely: where the carriers graze, the corrector
131/// can hop between the two sheets of the contact, so the step stays inside the
132/// separation band.  Two things that step must NOT be:
133///
134/// * **applied to a pair that only crosses.** A pair is classified `Singular`
135///   when any sample near the contact has a DEGENERATE normal (a pole of a
136///   revolution carrier) — that says nothing about tangency, and the pair can
137///   cross at 90°.  Marching a transverse crossing at the tangency step crawls,
138///   and a curve longer than `maximum_steps × step` then dies as "trace
139///   exhausted" (2026-08-06 report: a draft-angle extrude wall against a
140///   revolve-of-revolve face — a 2.0-unit curve needing 4014 steps of 4000,
141///   10 points once marched at its own scale).  The step is therefore gated on
142///   the classification's own tangency evidence, not on the relation label.
143/// * **an absolute distance.** A fixed floor is a different fraction of every
144///   model, so the reachable curve length depended on part size.  The floor is
145///   a fraction of the part extent instead; `solid_scale` is at least 1, so
146///   unit-scale parts keep the historical `1e-3` exactly.
147fn march_maximum_step(
148    classification: &SurfacePairClassification,
149    requested: Option<f64>,
150    scale: f64,
151) -> Option<f64> {
152    let grazes = matches!(
153        classification.relation,
154        SurfacePairRelation::NearTangent | SurfacePairRelation::Singular
155    ) && classification.minimum_normal_cross <= PAIR_ANGULAR_TOLERANCE;
156    if !grazes {
157        return requested;
158    }
159    Some(
160        requested.unwrap_or(
161            classification
162                .minimum_separation
163                .max(NEAR_TANGENT_STEP_FRACTION * scale),
164        ) * 0.5,
165    )
166}
167
168impl Default for ImprintOptions {
169    fn default() -> Self {
170        Self {
171            tolerance: default_tolerance(),
172            maximum_fit_points: default_maximum_fit_points(),
173            local_fit: false,
174            fit_chunk_points: None,
175            maximum_ssi_step: None,
176        }
177    }
178}
179
180#[derive(Clone, Debug, Deserialize, Serialize)]
181pub struct ImprintVertex {
182    pub id: u64,
183    pub point: Vec3,
184}
185
186#[derive(Clone, Debug, Deserialize, Serialize)]
187pub struct FacePcurve {
188    pub operand: u8,
189    pub face_id: u64,
190    pub pcurve: NurbsCurve,
191}
192
193#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash, Serialize)]
194pub struct FaceKey {
195    pub operand: u8,
196    pub face_id: u64,
197}
198
199#[derive(Clone, Debug, Deserialize, Serialize)]
200pub struct ImprintPieceRecord {
201    pub id: u64,
202    pub curve: NurbsCurve,
203    pub t0: f64,
204    pub t1: f64,
205    pub start_vertex_id: u64,
206    pub end_vertex_id: u64,
207    pub pcurves: Vec<FacePcurve>,
208    pub support_faces: [FaceKey; 2],
209    /// OCCT common-block / `IsExistingPaveBlock` (B2): when this SSI section
210    /// curve was found to COINCIDE along its whole span with an EXISTING
211    /// boundary edge of one of its support faces, the section is not a new
212    /// 1-cell — it IS that boundary edge. Records `(operand, edge_id, aligned)`
213    /// of the existing edge to REUSE; `aligned` is whether the section's
214    /// start→end runs the same direction as the edge's start→end. The
215    /// assembler then resolves this piece to the shared boundary edge's
216    /// identity so both operands reference ONE edge (no duplicate section /
217    /// one-use edge). `None` = an ordinary freshly-minted section (the
218    /// historical behaviour; also what `BREP_SHARED_SECTION_EDGE=0` forces).
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub shared_edge: Option<(u8, u64, bool)>,
221}
222
223#[derive(Clone, Debug, Deserialize, Serialize)]
224pub struct EdgeSplitRecord {
225    pub operand: u8,
226    pub edge_id: u64,
227    pub parameters: Vec<f64>,
228}
229
230#[derive(Clone, Debug, Deserialize, Serialize)]
231pub struct FaceImprints {
232    pub operand: u8,
233    pub face_id: u64,
234    pub piece_ids: Vec<u64>,
235}
236
237#[derive(Clone, Debug, Deserialize, Serialize)]
238pub struct ImprintResultRecord {
239    pub vertices: Vec<ImprintVertex>,
240    pub pieces: Vec<ImprintPieceRecord>,
241    pub by_face: Vec<FaceImprints>,
242    pub edge_splits: Vec<EdgeSplitRecord>,
243    /// Operand edges geometrically OVERLAPPED by a section curve (the
244    /// cosurface/coincident locus: an inscribed sphere's contact circle
245    /// riding a cap ring). Fragment-selection fate must never flood across
246    /// them — the two sides of such an edge can lie on opposite sides of
247    /// the other operand (the cap disc inside, the wall outside).
248    #[serde(default)]
249    pub barrier_edges: Vec<(u8, u64)>,
250    /// Every ISOLATED TANGENT NODE the imprint admitted: a point where the two
251    /// carriers touch with parallel normals and the section crosses itself, so
252    /// the pair was marched rather than refused (see
253    /// `imprint/tangent_contact.rs`). The boolean attributes a later tearing to
254    /// these rather than reporting an anonymous degeneracy — the second-order
255    /// filter cannot decide a third-order singularity, so the assembly is the
256    /// authority on whether a node it admitted was really imprintable.
257    #[serde(default)]
258    pub tangent_nodes: Vec<Vec3>,
259    /// True when the imprint saw ANY surface-contact evidence — an accepted
260    /// pierce seed, a traced SSI branch (even one later clipped away), or a
261    /// minted piece. Pure material disjointness leaves none, while a
262    /// silently-lost section always leaves at least the upstream evidence, so
263    /// the boolean's legitimate-empty adjudication requires this to be false.
264    #[serde(default)]
265    pub section_evidence: bool,
266}
267
268#[path = "imprint/support.rs"]
269mod support;
270#[path = "imprint/builder.rs"]
271mod builder;
272#[path = "imprint/junctions.rs"]
273mod junctions;
274#[path = "imprint/self_touch.rs"]
275mod self_touch;
276#[path = "imprint/sections.rs"]
277mod sections;
278#[path = "imprint/tangent_contact.rs"]
279mod tangent_contact;
280#[path = "imprint/driver.rs"]
281mod driver;
282// BREP private tests: 926d1ffe6a42f122
283
284use builder::*;
285use junctions::*;
286use sections::*;
287use support::*;
288use tangent_contact::classify_tangent_contact;
289
290pub use driver::build_imprints;
291pub(crate) use self_touch::self_touch_edge_splits;
292
293// BREP private tests: 4efc8161f2a59e6e