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