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, 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}
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 driver::build_carrier_imprints;
315pub(crate) use self_touch::self_touch_edge_splits;