@ifc-lite/clash 1.6.8 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/step.d.ts.map +1 -1
- package/dist/adapters/step.js +19 -2
- package/dist/adapters/step.js.map +1 -1
- package/dist/analysis.d.ts +37 -1
- package/dist/analysis.d.ts.map +1 -1
- package/dist/analysis.js +39 -0
- package/dist/analysis.js.map +1 -1
- package/dist/contact/narrow-phase.d.ts +6 -1
- package/dist/contact/narrow-phase.d.ts.map +1 -1
- package/dist/contact/narrow-phase.js +65 -1
- package/dist/contact/narrow-phase.js.map +1 -1
- package/dist/contact/shared-faces.d.ts +5 -1
- package/dist/contact/shared-faces.d.ts.map +1 -1
- package/dist/contact/shared-faces.js +49 -1
- package/dist/contact/shared-faces.js.map +1 -1
- package/dist/contact/tri-tri.d.ts +33 -4
- package/dist/contact/tri-tri.d.ts.map +1 -1
- package/dist/contact/tri-tri.js +37 -11
- package/dist/contact/tri-tri.js.map +1 -1
- package/dist/duplicate-metric.d.ts +74 -0
- package/dist/duplicate-metric.d.ts.map +1 -0
- package/dist/duplicate-metric.js +117 -0
- package/dist/duplicate-metric.js.map +1 -0
- package/dist/duplicate-sets.d.ts +47 -0
- package/dist/duplicate-sets.d.ts.map +1 -0
- package/dist/duplicate-sets.js +124 -0
- package/dist/duplicate-sets.js.map +1 -0
- package/dist/duplicates.d.ts +99 -11
- package/dist/duplicates.d.ts.map +1 -1
- package/dist/duplicates.js +270 -76
- package/dist/duplicates.js.map +1 -1
- package/dist/engine-ts/depth.d.ts +99 -0
- package/dist/engine-ts/depth.d.ts.map +1 -0
- package/dist/engine-ts/depth.js +172 -0
- package/dist/engine-ts/depth.js.map +1 -0
- package/dist/engine-ts/kernel.d.ts +3 -1
- package/dist/engine-ts/kernel.d.ts.map +1 -1
- package/dist/engine-ts/narrow.d.ts +7 -1
- package/dist/engine-ts/narrow.d.ts.map +1 -1
- package/dist/engine-ts/narrow.js +53 -95
- package/dist/engine-ts/narrow.js.map +1 -1
- package/dist/engine-ts/obb.d.ts +116 -0
- package/dist/engine-ts/obb.d.ts.map +1 -0
- package/dist/engine-ts/obb.js +331 -0
- package/dist/engine-ts/obb.js.map +1 -0
- package/dist/engine-ts/orchestrator.d.ts.map +1 -1
- package/dist/engine-ts/orchestrator.js +10 -13
- package/dist/engine-ts/orchestrator.js.map +1 -1
- package/dist/engine-ts/tri-mesh.d.ts +84 -22
- package/dist/engine-ts/tri-mesh.d.ts.map +1 -1
- package/dist/engine-ts/tri-mesh.js +151 -48
- package/dist/engine-ts/tri-mesh.js.map +1 -1
- package/dist/engine-ts/ts-kernel.d.ts.map +1 -1
- package/dist/engine-ts/ts-kernel.js +1 -0
- package/dist/engine-ts/ts-kernel.js.map +1 -1
- package/dist/engine-wasm/wasm-kernel.d.ts.map +1 -1
- package/dist/engine-wasm/wasm-kernel.js +7 -0
- package/dist/engine-wasm/wasm-kernel.js.map +1 -1
- package/dist/grouping.d.ts +4 -0
- package/dist/grouping.d.ts.map +1 -1
- package/dist/grouping.js +18 -43
- package/dist/grouping.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/math/aabb.d.ts +10 -1
- package/dist/math/aabb.d.ts.map +1 -1
- package/dist/math/aabb.js +33 -13
- package/dist/math/aabb.js.map +1 -1
- package/dist/shape-signature.d.ts +41 -0
- package/dist/shape-signature.d.ts.map +1 -0
- package/dist/shape-signature.js +113 -0
- package/dist/shape-signature.js.map +1 -0
- package/dist/triage.d.ts.map +1 -1
- package/dist/triage.js +11 -3
- package/dist/triage.js.map +1 -1
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/union-find.d.ts +11 -0
- package/dist/union-find.d.ts.map +1 -0
- package/dist/union-find.js +44 -0
- package/dist/union-find.js.map +1 -0
- package/package.json +7 -6
package/dist/duplicates.js
CHANGED
|
@@ -1,12 +1,47 @@
|
|
|
1
1
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* Duplicate / fully-overlapping element detection (#1280).
|
|
6
|
+
*
|
|
7
|
+
* The first thing people do when reviewing a single discipline model is hunt for
|
|
8
|
+
* accidentally duplicated or coincident objects — re-imported geometry, a wall
|
|
9
|
+
* pasted twice, a column modelled on top of another. That is *not* a discipline
|
|
10
|
+
* clash, so it gets its own lightweight pass: AABB proximity decides *what* is
|
|
11
|
+
* reported, and a tessellation-invariant shape signature (surface area +
|
|
12
|
+
* enclosed volume) decides only whether a reported pair is labelled an exact
|
|
13
|
+
* duplicate. No narrow-phase triangle-vs-triangle work. The broad phase is a
|
|
14
|
+
* one-axis sort-and-sweep, which handles mixed-scale models correctly (no grid
|
|
15
|
+
* cell size to mis-tune), so it is cheap enough to run on every load; the
|
|
16
|
+
* signature is computed lazily, only for pairs that already coincide. Matching
|
|
17
|
+
* on AABBs has a known limit for nested solids — see the "nested-solids limit"
|
|
18
|
+
* section on {@link findDuplicates} for exactly what it can and cannot tell
|
|
19
|
+
* apart.
|
|
20
|
+
*
|
|
21
|
+
* "Same object" is decided by {@link boxDistance} against `positionTolerance`, a
|
|
22
|
+
* plain distance in metres. It used to be AABB intersection-over-union ≥ 0.9,
|
|
23
|
+
* which is a ratio and so carried no physical tolerance at all: the same setting
|
|
24
|
+
* allowed 5 mm across a DN100 pipe and 421 mm across an 8 m slab (see
|
|
25
|
+
* `DuplicateOptions.iouThreshold`).
|
|
26
|
+
*
|
|
27
|
+
* Output is a normal {@link ClashResult} (rule id `duplicates`) so the existing
|
|
28
|
+
* panel, grouping and BCF export render it with no special-casing. It is
|
|
29
|
+
* *pairwise*: N coincident copies of one object yield N(N−1)/2 clashes. For a
|
|
30
|
+
* reader-facing list use `groupDuplicateSets`, which collapses those pairs into
|
|
31
|
+
* one finding per coincident set.
|
|
32
|
+
*/
|
|
4
33
|
import { center, overlapBounds } from './math/aabb.js';
|
|
34
|
+
import { boxDistance, boxesTouch, minExtent, similarity } from './duplicate-metric.js';
|
|
35
|
+
import { sameShape, shapeSignature } from './shape-signature.js';
|
|
5
36
|
import { isExcluded, qualifiedKey } from './exclude.js';
|
|
37
|
+
import { summarizeClashes } from './analysis.js';
|
|
6
38
|
const DEFAULTS = {
|
|
39
|
+
positionTolerance: 0.01,
|
|
40
|
+
exactTolerance: 0.001,
|
|
41
|
+
/** Legacy IoU mode only. */
|
|
7
42
|
iouThreshold: 0.9,
|
|
43
|
+
/** Legacy IoU mode only. */
|
|
8
44
|
exactThreshold: 0.99,
|
|
9
|
-
positionTolerance: 0.01,
|
|
10
45
|
};
|
|
11
46
|
export const DUPLICATES_RULE = {
|
|
12
47
|
id: 'duplicates',
|
|
@@ -14,103 +49,229 @@ export const DUPLICATES_RULE = {
|
|
|
14
49
|
a: '*',
|
|
15
50
|
mode: 'hard',
|
|
16
51
|
};
|
|
17
|
-
function aabbVolume(b) {
|
|
18
|
-
const dx = Math.max(0, b.max[0] - b.min[0]);
|
|
19
|
-
const dy = Math.max(0, b.max[1] - b.min[1]);
|
|
20
|
-
const dz = Math.max(0, b.max[2] - b.min[2]);
|
|
21
|
-
return dx * dy * dz;
|
|
22
|
-
}
|
|
23
|
-
/** Intersection-over-union of two AABBs (0 when disjoint). */
|
|
24
|
-
function aabbIoU(a, b) {
|
|
25
|
-
const ox = Math.min(a.max[0], b.max[0]) - Math.max(a.min[0], b.min[0]);
|
|
26
|
-
const oy = Math.min(a.max[1], b.max[1]) - Math.max(a.min[1], b.min[1]);
|
|
27
|
-
const oz = Math.min(a.max[2], b.max[2]) - Math.max(a.min[2], b.min[2]);
|
|
28
|
-
if (ox <= 0 || oy <= 0 || oz <= 0)
|
|
29
|
-
return 0;
|
|
30
|
-
const inter = ox * oy * oz;
|
|
31
|
-
const union = aabbVolume(a) + aabbVolume(b) - inter;
|
|
32
|
-
return union > 0 ? inter / union : 0;
|
|
33
|
-
}
|
|
34
|
-
function aabbApproxEqual(a, b, tol) {
|
|
35
|
-
for (let i = 0; i < 3; i += 1) {
|
|
36
|
-
if (Math.abs(a.min[i] - b.min[i]) > tol)
|
|
37
|
-
return false;
|
|
38
|
-
if (Math.abs(a.max[i] - b.max[i]) > tol)
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
/** Similarity in `[0,1]`: AABB IoU, falling back to box-equality for degenerate
|
|
44
|
-
* (zero-volume / planar) elements where IoU is undefined. */
|
|
45
|
-
function similarity(a, b, tol) {
|
|
46
|
-
const iou = aabbIoU(a, b);
|
|
47
|
-
if (iou > 0)
|
|
48
|
-
return iou;
|
|
49
|
-
// Both (near) degenerate: an exact box match still means "same place".
|
|
50
|
-
if (aabbVolume(a) <= 0 || aabbVolume(b) <= 0) {
|
|
51
|
-
return aabbApproxEqual(a, b, tol) ? 1 : 0;
|
|
52
|
-
}
|
|
53
|
-
return 0;
|
|
54
|
-
}
|
|
55
|
-
/** Shortest dimension of a box — the depth one element is embedded in another. */
|
|
56
|
-
function minExtent(b) {
|
|
57
|
-
return Math.min(b.max[0] - b.min[0], b.max[1] - b.min[1], b.max[2] - b.min[2]);
|
|
58
|
-
}
|
|
59
|
-
function triCount(el) {
|
|
60
|
-
return Math.floor(el.indices.length / 3);
|
|
61
|
-
}
|
|
62
52
|
function toRef(el) {
|
|
63
53
|
return { key: el.key, ref: el.ref, model: el.model, tag: el.tag, name: el.name };
|
|
64
54
|
}
|
|
65
|
-
function buildSummary(clashes) {
|
|
66
|
-
const byRule = {};
|
|
67
|
-
const byTypePair = {};
|
|
68
|
-
const bySeverity = { critical: 0, major: 0, minor: 0, info: 0 };
|
|
69
|
-
for (const c of clashes) {
|
|
70
|
-
byRule[c.rule] = (byRule[c.rule] ?? 0) + 1;
|
|
71
|
-
const pair = [c.a.tag, c.b.tag].sort().join(' vs ');
|
|
72
|
-
byTypePair[pair] = (byTypePair[pair] ?? 0) + 1;
|
|
73
|
-
bySeverity[c.severity] += 1;
|
|
74
|
-
}
|
|
75
|
-
return { total: clashes.length, byRule, byTypePair, bySeverity };
|
|
76
|
-
}
|
|
77
55
|
/**
|
|
78
56
|
* Find duplicate / fully-overlapping elements. Returns a {@link ClashResult}
|
|
79
|
-
* where each clash is a near-coincident pair
|
|
80
|
-
*
|
|
81
|
-
*
|
|
57
|
+
* where each clash is a near-coincident pair. `settings.tolerance` is
|
|
58
|
+
* `positionTolerance`, the value that actually decided the matches.
|
|
59
|
+
*
|
|
60
|
+
* Severity says exactly this much:
|
|
61
|
+
* - `major` — some pair of the two elements' boxes coincides within
|
|
62
|
+
* `exactTolerance` **and** the two elements' meshes — summed over the
|
|
63
|
+
* several parts a multi-material / CSG element emits — agree to within 5% on
|
|
64
|
+
* both surface area and enclosed volume. Read it as "the same object, in the
|
|
65
|
+
* same place"; it survives one copy being re-tessellated, and it does not
|
|
66
|
+
* depend on which of a multi-part element's part pairings the sweep visited
|
|
67
|
+
* first.
|
|
68
|
+
* - `minor` — near-coincident, but something differs: the boxes are further
|
|
69
|
+
* apart than `exactTolerance`, the shapes disagree, or one element carries no
|
|
70
|
+
* measurable geometry.
|
|
71
|
+
*
|
|
72
|
+
* What `major` still cannot tell you: the signature is two scalars over the
|
|
73
|
+
* surface, so two genuinely different solids that happen to agree in both area
|
|
74
|
+
* and volume to within 5% are indistinguishable here, as are an element and its
|
|
75
|
+
* mirror image.
|
|
76
|
+
*
|
|
77
|
+
* ## The nested-solids limit
|
|
78
|
+
*
|
|
79
|
+
* Matching remains AABB-only, so an element nested inside another — a pipe in a
|
|
80
|
+
* sleeve, a duct in a shaft, an assembly and the envelope drawn round it — is,
|
|
81
|
+
* in principle, something this pass cannot tell from a duplicate. Two gates
|
|
82
|
+
* already narrow that to a documented residual, and `duplicates.test.ts` pins
|
|
83
|
+
* each case:
|
|
84
|
+
*
|
|
85
|
+
* - Nesting has clearance, and {@link boxDistance} measures clearance. Anything
|
|
86
|
+
* looser than ~7 mm of annulus is more than `positionTolerance` away and is
|
|
87
|
+
* never reported at all: a DN100 pipe in a DN125 sleeve is 17.7 mm away, a
|
|
88
|
+
* 400×300 duct in a 500×400 shaft is 70.7 mm.
|
|
89
|
+
* - Tighter nesting *is* reported, but only ever as `minor`. Reaching `major`
|
|
90
|
+
* additionally requires the two meshes to agree on surface area and enclosed
|
|
91
|
+
* volume within 5%, which distinct solids sharing a box do not: a 50 mm and a
|
|
92
|
+
* 55 mm tube are 9.1% / 17.4% apart, a railing and its envelope box 73.2% /
|
|
93
|
+
* 88.4%.
|
|
94
|
+
*
|
|
95
|
+
* So the residual is exactly this: a pair nested within a few millimetres is
|
|
96
|
+
* listed as a `minor` candidate overlap. That is the intended reading of
|
|
97
|
+
* `minor` — "these two coincide, and this pass will not claim they are the same
|
|
98
|
+
* object" — and a reviewer still sees it, which for a coincidence hunt is the
|
|
99
|
+
* safe direction.
|
|
100
|
+
*
|
|
101
|
+
* Measured across five public models (duplex, AC20-FZK-Haus, Office_A_20110811,
|
|
102
|
+
* dental_clinic, Infra-Bridge), *no* reported pair was a nested one: all 33
|
|
103
|
+
* findings are same-type pairs of equal bounds, and the count of nested or
|
|
104
|
+
* cross-type pairs stays 0 even at a 250 mm tolerance, 25× the default. Note
|
|
105
|
+
* what that does not cover — none of the five carries pipe or duct segments, so
|
|
106
|
+
* the pipe-in-sleeve case is evidenced only by the constructed fixtures above.
|
|
107
|
+
*
|
|
108
|
+
* Closing the residual would need a narrow phase, which this pass deliberately
|
|
109
|
+
* does not run, and the cheap substitutes are worse rather than merely weaker.
|
|
110
|
+
* An area-weighted centroid plus surface inertia — the obvious candidate, and
|
|
111
|
+
* the same O(triangles) cost as the signature (3.6 ms vs 2.6 ms over
|
|
112
|
+
* dental_clinic's 236,795 triangles) — discriminates *less* on every nested
|
|
113
|
+
* case measured, contributes nothing at all when the nesting is concentric
|
|
114
|
+
* (identical centroids for pipe-in-sleeve and pipe-plus-insulation), and is not
|
|
115
|
+
* tessellation-invariant: it reads 39.6% apart on one box against a finer
|
|
116
|
+
* triangulation of the same box, which would break the exact-duplicate case
|
|
117
|
+
* this signature exists to keep together. A voxel occupancy sample would
|
|
118
|
+
* separate nested from coincident, but it needs a cell size — reintroducing the
|
|
119
|
+
* scale-dependent knob that replacing the IoU gate removed.
|
|
82
120
|
*/
|
|
83
121
|
export function findDuplicates(elements, options = {}) {
|
|
122
|
+
// A caller that passes an IoU threshold is asking for IoU semantics; honour
|
|
123
|
+
// them (deprecated) rather than reinterpreting the number under a metric where
|
|
124
|
+
// it means nothing. Everyone else — the viewer, the CLI, every caller that
|
|
125
|
+
// passes no thresholds — gets the distance gate.
|
|
126
|
+
const legacyIoU = options.iouThreshold != null || options.exactThreshold != null;
|
|
84
127
|
const iouThreshold = options.iouThreshold ?? DEFAULTS.iouThreshold;
|
|
85
128
|
const exactThreshold = options.exactThreshold ?? DEFAULTS.exactThreshold;
|
|
86
129
|
const positionTolerance = options.positionTolerance ?? DEFAULTS.positionTolerance;
|
|
130
|
+
const exactTolerance = options.exactTolerance ?? DEFAULTS.exactTolerance;
|
|
87
131
|
const exclusions = options.exclusions;
|
|
88
132
|
const clashes = [];
|
|
89
|
-
|
|
133
|
+
/** clash id → index into `clashes`, for the cross-part dedup + upgrade below. */
|
|
134
|
+
const seen = new Map();
|
|
135
|
+
// Shape signatures are only needed for pairs that already coincide within
|
|
136
|
+
// `exactTolerance`, and each element's is O(its triangles). Compute them
|
|
137
|
+
// lazily and keep them, so a model with no coincident pairs never touches a
|
|
138
|
+
// vertex and a duplicated element is measured once however many partners it
|
|
139
|
+
// has.
|
|
140
|
+
//
|
|
141
|
+
// The signature is aggregated over ALL the meshes one element emitted (one
|
|
142
|
+
// per material / CSG part, sharing `(model, ref)`): area and volume both sum
|
|
143
|
+
// over parts. The exact-duplicate label is a statement about the ELEMENT
|
|
144
|
+
// pair, and the several cross-part pairs of one element pair collapse to a
|
|
145
|
+
// single clash id below — so comparing per-part signatures would let
|
|
146
|
+
// whichever part pairing the sweep reached first decide the label. Summing
|
|
147
|
+
// per-part |volume|s counts any overlap between parts twice, but it does so
|
|
148
|
+
// identically on both copies, so the comparison is unaffected.
|
|
149
|
+
const partsOf = new Map();
|
|
150
|
+
for (let i = 0; i < elements.length; i += 1) {
|
|
151
|
+
const k = `${elements[i].model} ${elements[i].ref}`;
|
|
152
|
+
const list = partsOf.get(k);
|
|
153
|
+
if (list)
|
|
154
|
+
list.push(i);
|
|
155
|
+
else
|
|
156
|
+
partsOf.set(k, [i]);
|
|
157
|
+
}
|
|
158
|
+
const signatures = new Map();
|
|
159
|
+
const signatureOf = (i) => {
|
|
160
|
+
const k = `${elements[i].model} ${elements[i].ref}`;
|
|
161
|
+
const cached = signatures.get(k);
|
|
162
|
+
if (cached)
|
|
163
|
+
return cached;
|
|
164
|
+
let area = 0;
|
|
165
|
+
let volume = 0;
|
|
166
|
+
for (const part of partsOf.get(k) ?? [i]) {
|
|
167
|
+
const s = shapeSignature(elements[part]);
|
|
168
|
+
area += s.area;
|
|
169
|
+
volume += s.volume;
|
|
170
|
+
}
|
|
171
|
+
const computed = { area, volume };
|
|
172
|
+
signatures.set(k, computed);
|
|
173
|
+
return computed;
|
|
174
|
+
};
|
|
175
|
+
// A key repeated inside one model across *different* elements is a defect in
|
|
176
|
+
// the file (`ifc-lite validate` reports duplicated GlobalIds), not proof of
|
|
177
|
+
// one element — and "the same object exported twice" is exactly what a
|
|
178
|
+
// duplicate hunt is for. So identity is (model, ref): `key` is the GlobalId,
|
|
179
|
+
// which a broken exporter can repeat, while `ref` is the express id (or its
|
|
180
|
+
// federated remap), which is unique by construction.
|
|
181
|
+
//
|
|
182
|
+
// A key does legitimately repeat across the several meshes one element emits
|
|
183
|
+
// (one per material / CSG part) — but those share the element's `ref` too, so
|
|
184
|
+
// they are still skipped as self-pairs. Only keys carried by two *distinct*
|
|
185
|
+
// refs are ambiguous, and only those get the ref folded into the clash id, so
|
|
186
|
+
// every id a well-formed file produced before is unchanged.
|
|
187
|
+
const ambiguousKeys = new Set();
|
|
188
|
+
{
|
|
189
|
+
const refOfKey = new Map();
|
|
190
|
+
for (const el of elements) {
|
|
191
|
+
const k = `${el.model} ${el.key}`;
|
|
192
|
+
const first = refOfKey.get(k);
|
|
193
|
+
if (first === undefined)
|
|
194
|
+
refOfKey.set(k, el.ref);
|
|
195
|
+
else if (first !== el.ref)
|
|
196
|
+
ambiguousKeys.add(k);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const pairKey = (el) => {
|
|
200
|
+
const k = `${el.model} ${el.key}`;
|
|
201
|
+
return ambiguousKeys.has(k) ? `${k}#${el.ref}` : k;
|
|
202
|
+
};
|
|
90
203
|
const consider = (i, j) => {
|
|
91
204
|
if (i >= j)
|
|
92
205
|
return; // unordered pairs once
|
|
93
206
|
const elA = elements[i];
|
|
94
207
|
const elB = elements[j];
|
|
95
|
-
if (elA.
|
|
208
|
+
if (elA.ref === elB.ref && elA.model === elB.model)
|
|
96
209
|
return;
|
|
97
210
|
if (exclusions &&
|
|
98
211
|
isExcluded(exclusions, qualifiedKey(elA.model, elA.key), qualifiedKey(elB.model, elB.key))) {
|
|
99
212
|
return;
|
|
100
213
|
}
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
214
|
+
let exact;
|
|
215
|
+
if (legacyIoU) {
|
|
216
|
+
const sim = similarity(elA.bounds, elB.bounds, positionTolerance);
|
|
217
|
+
if (sim < iouThreshold)
|
|
218
|
+
return;
|
|
219
|
+
exact = sim >= exactThreshold;
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
if (!boxesTouch(elA.bounds, elB.bounds))
|
|
223
|
+
return;
|
|
224
|
+
const dist = boxDistance(elA.bounds, elB.bounds);
|
|
225
|
+
// Written as an acceptance (`!(dist <= tol)`), not a rejection
|
|
226
|
+
// (`dist > tol`), so a non-comparable distance abstains instead of
|
|
227
|
+
// asserting a pair. NaN fails every comparison, so bounds carrying NaN
|
|
228
|
+
// pass `boxesTouch` (its own comparisons are false too) and would fall
|
|
229
|
+
// through a `>` rejection and be reported as coincident with elements
|
|
230
|
+
// hundreds of metres away.
|
|
231
|
+
//
|
|
232
|
+
// The deprecated `iouThreshold` branch above is NOT a model for this: it
|
|
233
|
+
// is a rejection (`sim < iouThreshold`), and it happens to abstain on two
|
|
234
|
+
// NaN boxes only because `similarity` clamps them to 0 rather than
|
|
235
|
+
// returning NaN. It does not abstain in general — against a degenerate
|
|
236
|
+
// (zero-volume) element it takes the `aabbApproxEqual` fallback, whose
|
|
237
|
+
// per-axis comparisons are all false against NaN, so it returns 1 and
|
|
238
|
+
// reports the pair even at the default 0.9; and at `iouThreshold` 0 the
|
|
239
|
+
// rejection is false for every pair. That branch is deprecated and
|
|
240
|
+
// unchanged here; the two gates do not agree on non-finite bounds.
|
|
241
|
+
if (!(dist <= positionTolerance))
|
|
242
|
+
return;
|
|
243
|
+
exact = dist <= exactTolerance;
|
|
244
|
+
}
|
|
245
|
+
// Only now, on a pair that already coincides, is the shape worth measuring.
|
|
246
|
+
if (exact)
|
|
247
|
+
exact = sameShape(signatureOf(i), signatureOf(j));
|
|
106
248
|
const severity = exact ? 'major' : 'minor';
|
|
107
|
-
const ka =
|
|
108
|
-
const kb =
|
|
249
|
+
const ka = pairKey(elA);
|
|
250
|
+
const kb = pairKey(elB);
|
|
109
251
|
const [lo, hi] = ka < kb ? [ka, kb] : [kb, ka];
|
|
110
252
|
const id = `duplicates ${lo} ${hi}`;
|
|
111
|
-
|
|
253
|
+
const existing = seen.get(id);
|
|
254
|
+
if (existing !== undefined) {
|
|
255
|
+
// Multi-part elements: every cross-part pair of one element pair carries
|
|
256
|
+
// this id, and which pair arrives first is an artefact of the sweep
|
|
257
|
+
// order. The label must not be: if a later pair shows the copies
|
|
258
|
+
// actually coincide within `exactTolerance` (the shape term is already
|
|
259
|
+
// per-element), upgrade the recorded finding — and carry this pair's
|
|
260
|
+
// geometry, so the record describes the coincident parts rather than
|
|
261
|
+
// the loose cross pairing that happened to be swept first.
|
|
262
|
+
if (severity === 'major' && clashes[existing].severity === 'minor') {
|
|
263
|
+
const bounds = overlapBounds(elA.bounds, elB.bounds);
|
|
264
|
+
clashes[existing] = {
|
|
265
|
+
...clashes[existing],
|
|
266
|
+
severity,
|
|
267
|
+
distance: -Math.max(0, minExtent(bounds)),
|
|
268
|
+
point: center(bounds),
|
|
269
|
+
bounds,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
112
272
|
return;
|
|
113
|
-
|
|
273
|
+
}
|
|
274
|
+
seen.set(id, clashes.length);
|
|
114
275
|
const bounds = overlapBounds(elA.bounds, elB.bounds);
|
|
115
276
|
clashes.push({
|
|
116
277
|
id,
|
|
@@ -120,7 +281,10 @@ export function findDuplicates(elements, options = {}) {
|
|
|
120
281
|
status: 'hard',
|
|
121
282
|
// Coincident solids fully embed each other; report the embedded depth so
|
|
122
283
|
// they read as real overlaps (not zero-distance contacts) and sort first.
|
|
284
|
+
// This is a box dimension, not a mesh measurement, so it is labelled the
|
|
285
|
+
// same as the narrow-phase AABB fallback.
|
|
123
286
|
distance: -Math.max(0, minExtent(bounds)),
|
|
287
|
+
distanceKind: 'estimate',
|
|
124
288
|
point: center(bounds),
|
|
125
289
|
bounds,
|
|
126
290
|
severity,
|
|
@@ -128,10 +292,18 @@ export function findDuplicates(elements, options = {}) {
|
|
|
128
292
|
};
|
|
129
293
|
// Broad phase: one-axis sort-and-sweep over the AABBs. Unlike a fixed-size
|
|
130
294
|
// hash grid, this makes NO assumption about element scale — so two large
|
|
131
|
-
// objects offset by a few metres (still
|
|
295
|
+
// objects offset by a few metres (still inside a metre-scale tolerance) are
|
|
132
296
|
// never skipped just because many small elements shrank an average cell size.
|
|
133
297
|
// Sweep along the axis with the widest spread of box minima so the active set
|
|
134
|
-
// (and thus the comparison count) stays small.
|
|
298
|
+
// (and thus the comparison count) stays small. Eviction drops only boxes that
|
|
299
|
+
// no longer touch on `axis`, which is lossless for the DEFAULT gate — a pair
|
|
300
|
+
// that does not touch is rejected by `boxesTouch` anyway. It is NOT lossless
|
|
301
|
+
// for the deprecated `iouThreshold` branch: `similarity`'s degenerate fallback
|
|
302
|
+
// matches disjoint boxes that are within `tol` per axis, so in legacy mode the
|
|
303
|
+
// sweep can evict a pair that gate would have reported, and whether it does
|
|
304
|
+
// depends on which axis has the widest spread of minima — i.e. on the rest of
|
|
305
|
+
// the model. That axis-dependence predates the distance gate (the sweep is
|
|
306
|
+
// unchanged), so `iouThreshold` still restores the pre-1.7 gate as documented.
|
|
135
307
|
let axis = 0;
|
|
136
308
|
let bestSpread = -Infinity;
|
|
137
309
|
for (let a = 0; a < 3; a += 1) {
|
|
@@ -150,7 +322,28 @@ export function findDuplicates(elements, options = {}) {
|
|
|
150
322
|
axis = a;
|
|
151
323
|
}
|
|
152
324
|
}
|
|
153
|
-
|
|
325
|
+
// The sweep key must be a TOTAL order, so it is compared, never subtracted.
|
|
326
|
+
// `a - b` returns NaN for any pair involving a non-finite minimum — NaN bounds
|
|
327
|
+
// from a direct SDK caller, and `+Infinity` from `fromPositions` when no vertex
|
|
328
|
+
// on an axis was finite (it returns the box inverted). A comparator that
|
|
329
|
+
// answers NaN violates the contract `Array.prototype.sort` requires, and V8's
|
|
330
|
+
// TimSort then merges runs against that answer and emits an arbitrary
|
|
331
|
+
// permutation of the WHOLE array: the sweep's eviction sees minima going
|
|
332
|
+
// backwards, drops boxes that are still live, and real duplicates elsewhere in
|
|
333
|
+
// the model silently disappear. One unjudgeable element must cost only itself.
|
|
334
|
+
// Non-finite minima sort last, after every finite one, which is what the gate
|
|
335
|
+
// already does with them — `consider` abstains on a distance it cannot
|
|
336
|
+
// compare, so they contribute nothing wherever they sit, and putting them at
|
|
337
|
+
// the end keeps them out of the active set for the whole finite sweep.
|
|
338
|
+
const sweepKey = (i) => {
|
|
339
|
+
const v = elements[i].bounds.min[axis];
|
|
340
|
+
return Number.isFinite(v) ? v : Number.POSITIVE_INFINITY;
|
|
341
|
+
};
|
|
342
|
+
const order = elements.map((_, i) => i).sort((x, y) => {
|
|
343
|
+
const kx = sweepKey(x);
|
|
344
|
+
const ky = sweepKey(y);
|
|
345
|
+
return kx < ky ? -1 : kx > ky ? 1 : 0;
|
|
346
|
+
});
|
|
154
347
|
// `active` holds indices whose box still extends past the current box's start
|
|
155
348
|
// on `axis`; only those can overlap, so we compare against just them.
|
|
156
349
|
const active = [];
|
|
@@ -170,8 +363,9 @@ export function findDuplicates(elements, options = {}) {
|
|
|
170
363
|
clashes.sort((x, y) => (x.id < y.id ? -1 : x.id > y.id ? 1 : 0));
|
|
171
364
|
return {
|
|
172
365
|
clashes,
|
|
173
|
-
summary:
|
|
366
|
+
summary: summarizeClashes(clashes),
|
|
174
367
|
rulesRun: [DUPLICATES_RULE],
|
|
368
|
+
ruleCoverage: [{ rule: DUPLICATES_RULE.id, matchedA: elements.length, matchedB: null }],
|
|
175
369
|
settings: { tolerance: positionTolerance, excludeVoidsAndHosts: exclusions != null },
|
|
176
370
|
};
|
|
177
371
|
}
|
package/dist/duplicates.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"duplicates.js","sourceRoot":"","sources":["../src/duplicates.ts"],"names":[],"mappings":"AAAA;;+DAE+D;
|
|
1
|
+
{"version":3,"file":"duplicates.js","sourceRoot":"","sources":["../src/duplicates.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Depth measurement and the f32-precision floor for the narrow phase
|
|
3
|
+
* (`narrow.ts`). Split out to keep `narrow.ts` under the ~400-line module
|
|
4
|
+
* rule; mirrors `rust/clash/src/depth.rs` structurally so the two kernels
|
|
5
|
+
* stay easy to diff.
|
|
6
|
+
*/
|
|
7
|
+
import type { AABB, ClashElement, ClashRule, Vec3 } from '../types.js';
|
|
8
|
+
import type { TriMesh } from './tri-mesh.js';
|
|
9
|
+
import type { NarrowResult } from './narrow.js';
|
|
10
|
+
/**
|
|
11
|
+
* Exact box-box penetration when BOTH meshes are (within tolerance)
|
|
12
|
+
* rectangular boxes, else `null`. `mtd` is the only source of a `'mesh'`
|
|
13
|
+
* label for a distance that used to come from `TriMesh.maxPenetrationInto` —
|
|
14
|
+
* a nearest-crossing-vertex sampling probe that converges to 0 under
|
|
15
|
+
* retessellation instead of to the true depth (see `obb.ts`, `obb.test.ts`).
|
|
16
|
+
*
|
|
17
|
+
* `through` flags a THROUGH-PENETRATION pair — a thin member piercing clean
|
|
18
|
+
* through the other, e.g. a duct through a wall. There, the minimum-
|
|
19
|
+
* translation-distance `obb.ts` computes is dominated by the piercing
|
|
20
|
+
* member's own extent along the shared axis, not by the material actually
|
|
21
|
+
* crossed (review: #2536, reproduced a 5.5x inflation on exactly this
|
|
22
|
+
* shape), so `depthClashResult` reports the caller's AABB estimate for that
|
|
23
|
+
* shape — matching what `main` did before the box-exact metric existed, and
|
|
24
|
+
* honest about not being a measurement. The MTD is still returned (not
|
|
25
|
+
* discarded here) because the f32 floor must see it: which number gets
|
|
26
|
+
* REPORTED is a separate, later decision from whether the pair is
|
|
27
|
+
* measurable at all (see `depthClashResult`).
|
|
28
|
+
*/
|
|
29
|
+
export interface BoxPenetration {
|
|
30
|
+
/** Exact box-box minimum-translation depth (Gottschalk 15-axis SAT). */
|
|
31
|
+
mtd: number;
|
|
32
|
+
/** The MTD is inflated by the piercing member's own extent; report the
|
|
33
|
+
* AABB estimate instead (see `isThroughPenetration`). */
|
|
34
|
+
through: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function boxPenetration(small: TriMesh, large: TriMesh): BoxPenetration | null;
|
|
37
|
+
/**
|
|
38
|
+
* Deepest penetration of `mesh`'s crossing-triangle VERTICES into `other`:
|
|
39
|
+
* the maximum distance-to-surface of `other` over the vertices of the
|
|
40
|
+
* triangles flagged in `crossFlags` (the pairs the narrow phase saw
|
|
41
|
+
* genuinely crossing `other`) that lie inside `other`. Each vertex is
|
|
42
|
+
* visited once (deduped by vertex index, in index order — bit-identical to
|
|
43
|
+
* the Rust `crossing_vertex_penetration`). Returns 0 when no flagged vertex
|
|
44
|
+
* is inside.
|
|
45
|
+
*
|
|
46
|
+
* This is NOT a depth metric and must never be reported as one — it is the
|
|
47
|
+
* sampling probe PR #2536 was held over (`maxPenetrationInto`): its value is
|
|
48
|
+
* an O(edge length) artifact that converges to 0 under retessellation
|
|
49
|
+
* instead of to the true depth. It survives with exactly one client: the
|
|
50
|
+
* f32 noise-floor gate for a CONTAINED pair (`depthClashResult`), where the
|
|
51
|
+
* question is not "how deep?" but "is any mesh-level penetration measurably
|
|
52
|
+
* above the floor at all?" — sub-floor here means every crossing vertex sits
|
|
53
|
+
* within f32 rounding noise of the other surface, i.e. surfaces authored
|
|
54
|
+
* flush, which no amount of retessellation turns into a real overlap. For
|
|
55
|
+
* that yes/no question the probe's underestimation is harmless:
|
|
56
|
+
* underestimating can only keep a pair BELOW the floor, and the floor is
|
|
57
|
+
* the very thing being tested.
|
|
58
|
+
*/
|
|
59
|
+
export declare function crossingVertexPenetration(mesh: TriMesh, other: TriMesh, crossFlags: Uint8Array): number;
|
|
60
|
+
/**
|
|
61
|
+
* Turns the candidate penetration depths into the final `(status,
|
|
62
|
+
* distanceKind, distance)` triple. This is the ONLY place in the narrow
|
|
63
|
+
* phase allowed to build a `'mesh'`- or `'estimate'`-labelled `hard` result
|
|
64
|
+
* off a depth number — every branch in `narrow.ts` that can label a result
|
|
65
|
+
* `'mesh'` off `boxPenetration` (or its AABB-estimate fallback) MUST route
|
|
66
|
+
* through here rather than building the `NarrowResult` literal itself. That
|
|
67
|
+
* is what makes the f32 floor apply to all of them, and what enforces its
|
|
68
|
+
* precedence.
|
|
69
|
+
*
|
|
70
|
+
* THE FLOOR WINS (#2536 rebase decision): a pair below the f32 noise floor
|
|
71
|
+
* is `touch` regardless of how its depth was derived — at that magnitude the
|
|
72
|
+
* number is not measurable either way — so the floor is tested against EVERY
|
|
73
|
+
* candidate depth the pair has, not against whichever one the estimate-vs-
|
|
74
|
+
* mesh selection would report. Three candidates exist:
|
|
75
|
+
*
|
|
76
|
+
* - the AABB `estimate` (always present);
|
|
77
|
+
* - the box MTD, when both elements are certified boxes (`box`);
|
|
78
|
+
* - the crossing-vertex penetration, for a CONTAINED pair with a crossing
|
|
79
|
+
* vertex inside the other solid (`meshEvidence`) — evidence for this gate
|
|
80
|
+
* only, never a reported depth (see `crossingVertexPenetration`).
|
|
81
|
+
*
|
|
82
|
+
* The pair is `hard` only when the SMALLEST available candidate clears the
|
|
83
|
+
* floor. That is what makes the floor unreachable by depth-source selection:
|
|
84
|
+
* a sub-floor box MTD cannot be promoted by the through-penetration guard
|
|
85
|
+
* swapping in a larger AABB estimate; a sub-floor crossing-vertex
|
|
86
|
+
* penetration on a contained pair (surfaces authored flush — the eight
|
|
87
|
+
* Infra-Bridge pairs that moved #2594's 50-hard-clash pin to 58 when this
|
|
88
|
+
* PR's depth rework replaced their noise-scale mesh depth with the
|
|
89
|
+
* fabricated 4 m AABB estimate) cannot be promoted by that estimate; and a
|
|
90
|
+
* sub-floor AABB estimate cannot be promoted by a larger MTD (a through-
|
|
91
|
+
* penetration far from the origin, where the MTD is inflated by the
|
|
92
|
+
* piercing member's own extent). Only a pair already above the floor
|
|
93
|
+
* reaches the selection below, which then merely picks WHICH above-floor
|
|
94
|
+
* reportable number is used and how it is labelled — so a `hard` result's
|
|
95
|
+
* distance clears the floor by construction, whichever quantity it came
|
|
96
|
+
* from.
|
|
97
|
+
*/
|
|
98
|
+
export declare function depthClashResult(box: BoxPenetration | null, estimate: number, meshEvidence: number | null, elA: ClashElement, elB: ClashElement, rule: ClashRule, point: Vec3, bounds: AABB): NarrowResult | null;
|
|
99
|
+
//# sourceMappingURL=depth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"depth.d.ts","sourceRoot":"","sources":["../../src/engine-ts/depth.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC;IACZ;6DACyD;IACzD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAOpF;AAsDD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,CAevG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,cAAc,GAAG,IAAI,EAC1B,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,SAAS,EACf,KAAK,EAAE,IAAI,EACX,MAAM,EAAE,IAAI,GACX,YAAY,GAAG,IAAI,CAsBrB"}
|