@ifc-lite/clash 1.6.8 → 1.7.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.
Files changed (54) hide show
  1. package/dist/adapters/step.d.ts.map +1 -1
  2. package/dist/adapters/step.js +19 -2
  3. package/dist/adapters/step.js.map +1 -1
  4. package/dist/analysis.d.ts +28 -1
  5. package/dist/analysis.d.ts.map +1 -1
  6. package/dist/analysis.js +19 -0
  7. package/dist/analysis.js.map +1 -1
  8. package/dist/contact/narrow-phase.d.ts +5 -1
  9. package/dist/contact/narrow-phase.d.ts.map +1 -1
  10. package/dist/contact/narrow-phase.js +47 -1
  11. package/dist/contact/narrow-phase.js.map +1 -1
  12. package/dist/contact/shared-faces.d.ts +5 -1
  13. package/dist/contact/shared-faces.d.ts.map +1 -1
  14. package/dist/contact/shared-faces.js +49 -1
  15. package/dist/contact/shared-faces.js.map +1 -1
  16. package/dist/contact/tri-tri.d.ts +4 -1
  17. package/dist/contact/tri-tri.d.ts.map +1 -1
  18. package/dist/contact/tri-tri.js +4 -1
  19. package/dist/contact/tri-tri.js.map +1 -1
  20. package/dist/duplicate-metric.d.ts +56 -0
  21. package/dist/duplicate-metric.d.ts.map +1 -0
  22. package/dist/duplicate-metric.js +99 -0
  23. package/dist/duplicate-metric.js.map +1 -0
  24. package/dist/duplicate-sets.d.ts +47 -0
  25. package/dist/duplicate-sets.d.ts.map +1 -0
  26. package/dist/duplicate-sets.js +124 -0
  27. package/dist/duplicate-sets.js.map +1 -0
  28. package/dist/duplicates.d.ts +90 -11
  29. package/dist/duplicates.d.ts.map +1 -1
  30. package/dist/duplicates.js +221 -62
  31. package/dist/duplicates.js.map +1 -1
  32. package/dist/engine-ts/orchestrator.d.ts.map +1 -1
  33. package/dist/engine-ts/orchestrator.js +7 -0
  34. package/dist/engine-ts/orchestrator.js.map +1 -1
  35. package/dist/grouping.d.ts +4 -0
  36. package/dist/grouping.d.ts.map +1 -1
  37. package/dist/grouping.js +18 -43
  38. package/dist/grouping.js.map +1 -1
  39. package/dist/index.d.ts +11 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +11 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/shape-signature.d.ts +41 -0
  44. package/dist/shape-signature.d.ts.map +1 -0
  45. package/dist/shape-signature.js +113 -0
  46. package/dist/shape-signature.js.map +1 -0
  47. package/dist/types.d.ts +21 -0
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/types.js.map +1 -1
  50. package/dist/union-find.d.ts +11 -0
  51. package/dist/union-find.d.ts.map +1 -0
  52. package/dist/union-find.js +44 -0
  53. package/dist/union-find.js.map +1 -0
  54. package/package.json +5 -5
@@ -0,0 +1,99 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ function aabbVolume(b) {
5
+ const dx = Math.max(0, b.max[0] - b.min[0]);
6
+ const dy = Math.max(0, b.max[1] - b.min[1]);
7
+ const dz = Math.max(0, b.max[2] - b.min[2]);
8
+ return dx * dy * dz;
9
+ }
10
+ /** Intersection-over-union of two AABBs (0 when disjoint). */
11
+ function aabbIoU(a, b) {
12
+ const ox = Math.min(a.max[0], b.max[0]) - Math.max(a.min[0], b.min[0]);
13
+ const oy = Math.min(a.max[1], b.max[1]) - Math.max(a.min[1], b.min[1]);
14
+ const oz = Math.min(a.max[2], b.max[2]) - Math.max(a.min[2], b.min[2]);
15
+ if (ox <= 0 || oy <= 0 || oz <= 0)
16
+ return 0;
17
+ const inter = ox * oy * oz;
18
+ const union = aabbVolume(a) + aabbVolume(b) - inter;
19
+ return union > 0 ? inter / union : 0;
20
+ }
21
+ function aabbApproxEqual(a, b, tol) {
22
+ for (let i = 0; i < 3; i += 1) {
23
+ if (Math.abs(a.min[i] - b.min[i]) > tol)
24
+ return false;
25
+ if (Math.abs(a.max[i] - b.max[i]) > tol)
26
+ return false;
27
+ }
28
+ return true;
29
+ }
30
+ /**
31
+ * Distance (m) between two boxes as *objects*: the largest distance any corner
32
+ * of `a` has to travel to reach the matching corner of `b`. Because the max over
33
+ * the eight corners picks, independently per axis, whichever face moved further,
34
+ * it reduces to the Euclidean norm of the three per-axis face offsets — one
35
+ * square root, no corner enumeration.
36
+ *
37
+ * Two properties make it the tolerance a user can reason about:
38
+ * - **Pure translation gives exactly the translation length.** Every corner
39
+ * moves by `d`, so the result is `|d|`, whatever the shape and whatever the
40
+ * direction. That is what makes it isotropic, where IoU was not.
41
+ * - **A size difference counts too.** Concentric boxes whose faces differ by δ
42
+ * are δ apart, so this is centre distance *and* a shape check in one number,
43
+ * with no second, dimensionless knob.
44
+ *
45
+ * It is still AABB-only: two elements with the same bounds and different solids
46
+ * inside them are indistinguishable *to this function*. In practice that costs
47
+ * less than it sounds, because real nesting has clearance and clearance is what
48
+ * this measures. An element nested with a gap of `g` on two axes is `g·√2`
49
+ * away, so anything looser than 10/√2 ≈ 7 mm of annulus is rejected outright at
50
+ * the default tolerance — a duct in its shaft and a pipe in a standard sleeve
51
+ * both are. What survives is nesting tighter than that, and the shape signature
52
+ * then keeps it out of `major`. See `findDuplicates` for the measured
53
+ * envelope.
54
+ */
55
+ export function boxDistance(a, b) {
56
+ let sum = 0;
57
+ for (let i = 0; i < 3; i += 1) {
58
+ const d = Math.max(Math.abs(a.min[i] - b.min[i]), Math.abs(a.max[i] - b.max[i]));
59
+ sum += d * d;
60
+ }
61
+ return Math.sqrt(sum);
62
+ }
63
+ /** Do the boxes touch or overlap on every axis? Two elements that are disjoint
64
+ * in space are two objects, however close. Without this an element SMALLER than
65
+ * the tolerance (a 5 mm fixing at a 10 mm tolerance) would pair with a
66
+ * neighbour it never intersects. Touching (zero gap) counts, so coincident
67
+ * planar and point geometry still qualifies.
68
+ *
69
+ * This deliberately excludes one pair the legacy IoU fallback reported: two
70
+ * zero-thickness sheets offset a few millimetres ALONG THEIR OWN NORMAL are
71
+ * disjoint (any gap on that axis fails this test), where the old
72
+ * `aabbApproxEqual` fallback called boxes within `positionTolerance` per axis
73
+ * the same place. Same reasoning as the 5 mm fixing: geometry with clear air
74
+ * between the surfaces is two objects, not one modelled twice. Pinned by the
75
+ * "does not pair two disjoint sheets offset along their own normal" test. */
76
+ export function boxesTouch(a, b) {
77
+ for (let i = 0; i < 3; i += 1) {
78
+ if (Math.min(a.max[i], b.max[i]) < Math.max(a.min[i], b.min[i]))
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+ /** Legacy IoU similarity in `[0,1]`, falling back to box-equality for degenerate
84
+ * (zero-volume / planar) elements where IoU is undefined. */
85
+ export function similarity(a, b, tol) {
86
+ const iou = aabbIoU(a, b);
87
+ if (iou > 0)
88
+ return iou;
89
+ // Both (near) degenerate: an exact box match still means "same place".
90
+ if (aabbVolume(a) <= 0 || aabbVolume(b) <= 0) {
91
+ return aabbApproxEqual(a, b, tol) ? 1 : 0;
92
+ }
93
+ return 0;
94
+ }
95
+ /** Shortest dimension of a box — the depth one element is embedded in another. */
96
+ export function minExtent(b) {
97
+ return Math.min(b.max[0] - b.min[0], b.max[1] - b.min[1], b.max[2] - b.min[2]);
98
+ }
99
+ //# sourceMappingURL=duplicate-metric.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicate-metric.js","sourceRoot":"","sources":["../src/duplicate-metric.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import type { ClashGroup, ClashResult } from './types.js';
2
+ /**
3
+ * Group a duplicate result into coincident **sets** — one finding per set of
4
+ * objects occupying the same place, rather than one per pair.
5
+ *
6
+ * `findDuplicates` reports pairwise. N coincident copies of one object therefore
7
+ * produce N(N−1)/2 rows and each copy is named in N−1 of them, so three
8
+ * triplicated columns read as nine findings with every object mentioned twice.
9
+ * No row is literally repeated, but the list badly overstates the problem.
10
+ *
11
+ * The partition here is the **connected components of the pair graph**: every
12
+ * reported clash is an edge between two model-qualified elements, and each
13
+ * component is one "these N objects are coincident" finding. Unlike
14
+ * `groupClashes({ by: 'cluster' })` this needs no epsilon and cannot fuse two
15
+ * unrelated duplicate sets that merely stand within a cluster radius of each
16
+ * other — it uses only the pairs the detector actually reported.
17
+ *
18
+ * Two properties worth stating plainly:
19
+ *
20
+ * - **Transitivity is approximated, knowingly.** A≈B and B≈C under
21
+ * `positionTolerance` (the default corner-distance gate) does not imply A≈C,
22
+ * yet connected components put all three in one
23
+ * set. We accept that: a chain of near-coincident objects is a single
24
+ * coordination issue, and the strict alternative (maximal cliques) is
25
+ * exponential and would put the same object back into several findings —
26
+ * the exact complaint this grouping exists to answer.
27
+ * - **Sets may span models.** Identity is `(model, key, ref)`, so the same
28
+ * object delivered in two files groups correctly, and two files that happen to
29
+ * reuse a key for different objects do not. `ref` is in there because a key
30
+ * can repeat *within* one model too — a file with duplicated GlobalIds, which
31
+ * is exactly the defect a duplicate hunt is looking for; on `(model, key)`
32
+ * alone those two elements collapsed into one node and the set they form
33
+ * counted "1 coincident object". Adding `ref` only ever splits nodes, never
34
+ * merges them.
35
+ *
36
+ * Components are computed per `rule`, so a mixed result never fuses a duplicate
37
+ * pair with a discipline clash — and only components of the `duplicates` rule
38
+ * are titled "coincident". Any other rule's component is titled as connected
39
+ * clashes, which is all its pair graph proves: those members intersect, they
40
+ * do not occupy the same place. Severity is the most severe member severity: a
41
+ * set containing any exact-duplicate (`major`) pair surfaces as `major`.
42
+ *
43
+ * `ClashResult` is untouched — the panel, grouping modes and BCF export keep
44
+ * consuming the pairwise shape; this is a reporting view over it.
45
+ */
46
+ export declare function groupDuplicateSets(result: ClashResult): ClashGroup[];
47
+ //# sourceMappingURL=duplicate-sets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicate-sets.d.ts","sourceRoot":"","sources":["../src/duplicate-sets.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAA0B,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,EAAE,CAqEpE"}
@@ -0,0 +1,124 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Coincident-set grouping for duplicate results — the reader-facing view over
6
+ * the pairwise `ClashResult` that `findDuplicates` produces. Split out of
7
+ * `grouping.ts` (which owns the generic BCF-topic groupings) to keep both
8
+ * under the ~400-line module limit; re-exported through the package index.
9
+ */
10
+ import { DUPLICATES_RULE } from './duplicates.js';
11
+ import { qualifiedKey } from './exclude.js';
12
+ import { makeGroup, sortGroups } from './grouping.js';
13
+ import { createUnionFind } from './union-find.js';
14
+ /**
15
+ * Group a duplicate result into coincident **sets** — one finding per set of
16
+ * objects occupying the same place, rather than one per pair.
17
+ *
18
+ * `findDuplicates` reports pairwise. N coincident copies of one object therefore
19
+ * produce N(N−1)/2 rows and each copy is named in N−1 of them, so three
20
+ * triplicated columns read as nine findings with every object mentioned twice.
21
+ * No row is literally repeated, but the list badly overstates the problem.
22
+ *
23
+ * The partition here is the **connected components of the pair graph**: every
24
+ * reported clash is an edge between two model-qualified elements, and each
25
+ * component is one "these N objects are coincident" finding. Unlike
26
+ * `groupClashes({ by: 'cluster' })` this needs no epsilon and cannot fuse two
27
+ * unrelated duplicate sets that merely stand within a cluster radius of each
28
+ * other — it uses only the pairs the detector actually reported.
29
+ *
30
+ * Two properties worth stating plainly:
31
+ *
32
+ * - **Transitivity is approximated, knowingly.** A≈B and B≈C under
33
+ * `positionTolerance` (the default corner-distance gate) does not imply A≈C,
34
+ * yet connected components put all three in one
35
+ * set. We accept that: a chain of near-coincident objects is a single
36
+ * coordination issue, and the strict alternative (maximal cliques) is
37
+ * exponential and would put the same object back into several findings —
38
+ * the exact complaint this grouping exists to answer.
39
+ * - **Sets may span models.** Identity is `(model, key, ref)`, so the same
40
+ * object delivered in two files groups correctly, and two files that happen to
41
+ * reuse a key for different objects do not. `ref` is in there because a key
42
+ * can repeat *within* one model too — a file with duplicated GlobalIds, which
43
+ * is exactly the defect a duplicate hunt is looking for; on `(model, key)`
44
+ * alone those two elements collapsed into one node and the set they form
45
+ * counted "1 coincident object". Adding `ref` only ever splits nodes, never
46
+ * merges them.
47
+ *
48
+ * Components are computed per `rule`, so a mixed result never fuses a duplicate
49
+ * pair with a discipline clash — and only components of the `duplicates` rule
50
+ * are titled "coincident". Any other rule's component is titled as connected
51
+ * clashes, which is all its pair graph proves: those members intersect, they
52
+ * do not occupy the same place. Severity is the most severe member severity: a
53
+ * set containing any exact-duplicate (`major`) pair surfaces as `major`.
54
+ *
55
+ * `ClashResult` is untouched — the panel, grouping modes and BCF export keep
56
+ * consuming the pairwise shape; this is a reporting view over it.
57
+ */
58
+ export function groupDuplicateSets(result) {
59
+ const byRule = new Map();
60
+ for (const clash of result.clashes) {
61
+ const bucket = byRule.get(clash.rule);
62
+ if (bucket) {
63
+ bucket.push(clash);
64
+ }
65
+ else {
66
+ byRule.set(clash.rule, [clash]);
67
+ }
68
+ }
69
+ const groups = [];
70
+ for (const bucket of byRule.values()) {
71
+ // Number the participating elements in first-seen order; every clash is one
72
+ // edge between two of those nodes.
73
+ const nodeOf = new Map();
74
+ const idOf = (ref) => {
75
+ const k = `${qualifiedKey(ref.model, ref.key)}#${ref.ref}`;
76
+ const existing = nodeOf.get(k);
77
+ if (existing !== undefined)
78
+ return existing;
79
+ const assigned = nodeOf.size;
80
+ nodeOf.set(k, assigned);
81
+ return assigned;
82
+ };
83
+ const edges = bucket.map((clash) => [idOf(clash.a), idOf(clash.b)]);
84
+ const { find, union } = createUnionFind(nodeOf.size);
85
+ for (const [a, b] of edges) {
86
+ union(a, b);
87
+ }
88
+ // Collect each component's member clashes and its distinct element count —
89
+ // the latter is what the reader cares about ("these 3 objects are the same"),
90
+ // and it is NOT the member count once a set grows past a pair.
91
+ const components = new Map();
92
+ for (let i = 0; i < bucket.length; i += 1) {
93
+ const clash = bucket[i];
94
+ const root = find(edges[i][0]);
95
+ let comp = components.get(root);
96
+ if (!comp) {
97
+ comp = { members: [], nodes: new Set(), tags: new Set() };
98
+ components.set(root, comp);
99
+ }
100
+ comp.members.push(clash);
101
+ comp.nodes.add(edges[i][0]);
102
+ comp.nodes.add(edges[i][1]);
103
+ comp.tags.add(clash.a.tag);
104
+ comp.tags.add(clash.b.tag);
105
+ }
106
+ for (const comp of components.values()) {
107
+ const count = comp.nodes.size;
108
+ const tag = comp.tags.size === 1 ? `${[...comp.tags][0]} ` : '';
109
+ const members = [...comp.members].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
110
+ // "Coincident" is a statement the `duplicates` rule actually made. For
111
+ // any other rule the members of a component merely clash transitively —
112
+ // intersecting-but-distinct elements — so the title must not upgrade
113
+ // that to coincidence (#2530 review: an SDK caller passing a discipline
114
+ // result here would otherwise get "7 coincident IfcPipeSegment objects").
115
+ const rule = comp.members[0].rule;
116
+ const title = rule === DUPLICATES_RULE.id
117
+ ? `${count} coincident ${tag}objects`
118
+ : `${count} ${tag}objects in connected ${rule} clashes`;
119
+ groups.push(makeGroup(members, title, undefined, 'duplicate-set'));
120
+ }
121
+ }
122
+ return sortGroups(groups);
123
+ }
124
+ //# sourceMappingURL=duplicate-sets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicate-sets.js","sourceRoot":"","sources":["../src/duplicate-sets.ts"],"names":[],"mappings":"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"}
@@ -1,26 +1,105 @@
1
1
  import type { ClashElement, ClashResult, ClashRule, ExclusionSet } from './types.js';
2
2
  export interface DuplicateOptions {
3
3
  /**
4
- * Minimum AABB intersection-over-union for a pair to count as overlapping.
5
- * `1` = identical boxes; the default catches near-coincident objects while
6
- * leaving merely-adjacent ones (a slab and its finish) alone.
4
+ * How far apart (m) two elements may be and still count as the same object.
5
+ * This is the primary control and the number reported in
6
+ * `ClashResult.settings.tolerance`. It bounds {@link boxDistance}: for two
7
+ * equally-sized boxes that is exactly the distance between their centres, and
8
+ * a difference in size adds to it. Default 10 mm.
9
+ */
10
+ positionTolerance?: number;
11
+ /** Distance (m) at/below which a same-shape pair is treated as an EXACT
12
+ * duplicate (severity `major`) rather than a candidate overlap (`minor`).
13
+ * Default 1 mm. */
14
+ exactTolerance?: number;
15
+ /**
16
+ * @deprecated Superseded by {@link positionTolerance}. Minimum AABB
17
+ * intersection-over-union for a pair to count as overlapping. IoU is a ratio,
18
+ * so it imposed no fixed physical tolerance: for two equal boxes offset by `d`
19
+ * along an axis of extent `e` the IoU is `(e − d) / (e + d)`, and the default
20
+ * 0.9 therefore allowed `d ≤ e / 19` — 5 mm across a DN100 pipe but 421 mm
21
+ * across an 8 m slab, from one setting. Passing this (or `exactThreshold`)
22
+ * restores the pre-1.7 IoU **matching gate** — which pairs are reported, and
23
+ * the old `settings.tolerance` reading — rather than silently reinterpreting
24
+ * a number that means nothing in the new metric. It does NOT restore the
25
+ * whole pre-1.7 behaviour: severity is still decided by the area+volume
26
+ * shape signature (not the old triangle count), and self-pair identity is
27
+ * still `(model, ref)` (not key-based), so a file with duplicated GlobalIds
28
+ * reports pairs the old code hid.
7
29
  */
8
30
  iouThreshold?: number;
9
- /** IoU at/above which a same-triangle-count pair is treated as an EXACT
10
- * duplicate (severity `major`) rather than a candidate overlap (`minor`). */
31
+ /** @deprecated Only meaningful in the legacy IoU mode selected by
32
+ * {@link iouThreshold}; use {@link exactTolerance}. */
11
33
  exactThreshold?: number;
12
- /** Centre-distance (m) under which two degenerate (planar) elements with no
13
- * AABB volume are still considered coincident. */
14
- positionTolerance?: number;
15
34
  /** Pairs whose element keys are in here are skipped (voids/hosts/assemblies). */
16
35
  exclusions?: ExclusionSet;
17
36
  }
18
37
  export declare const DUPLICATES_RULE: ClashRule;
19
38
  /**
20
39
  * Find duplicate / fully-overlapping elements. Returns a {@link ClashResult}
21
- * where each clash is a near-coincident pair: severity `major` for an exact
22
- * duplicate (same triangle count + near-identical box), `minor` for a looser
23
- * overlap candidate.
40
+ * where each clash is a near-coincident pair. `settings.tolerance` is
41
+ * `positionTolerance`, the value that actually decided the matches.
42
+ *
43
+ * Severity says exactly this much:
44
+ * - `major` — some pair of the two elements' boxes coincides within
45
+ * `exactTolerance` **and** the two elements' meshes — summed over the
46
+ * several parts a multi-material / CSG element emits — agree to within 5% on
47
+ * both surface area and enclosed volume. Read it as "the same object, in the
48
+ * same place"; it survives one copy being re-tessellated, and it does not
49
+ * depend on which of a multi-part element's part pairings the sweep visited
50
+ * first.
51
+ * - `minor` — near-coincident, but something differs: the boxes are further
52
+ * apart than `exactTolerance`, the shapes disagree, or one element carries no
53
+ * measurable geometry.
54
+ *
55
+ * What `major` still cannot tell you: the signature is two scalars over the
56
+ * surface, so two genuinely different solids that happen to agree in both area
57
+ * and volume to within 5% are indistinguishable here, as are an element and its
58
+ * mirror image.
59
+ *
60
+ * ## The nested-solids limit
61
+ *
62
+ * Matching remains AABB-only, so an element nested inside another — a pipe in a
63
+ * sleeve, a duct in a shaft, an assembly and the envelope drawn round it — is,
64
+ * in principle, something this pass cannot tell from a duplicate. Two gates
65
+ * already narrow that to a documented residual, and `duplicates.test.ts` pins
66
+ * each case:
67
+ *
68
+ * - Nesting has clearance, and {@link boxDistance} measures clearance. Anything
69
+ * looser than ~7 mm of annulus is more than `positionTolerance` away and is
70
+ * never reported at all: a DN100 pipe in a DN125 sleeve is 17.7 mm away, a
71
+ * 400×300 duct in a 500×400 shaft is 70.7 mm.
72
+ * - Tighter nesting *is* reported, but only ever as `minor`. Reaching `major`
73
+ * additionally requires the two meshes to agree on surface area and enclosed
74
+ * volume within 5%, which distinct solids sharing a box do not: a 50 mm and a
75
+ * 55 mm tube are 9.1% / 17.4% apart, a railing and its envelope box 73.2% /
76
+ * 88.4%.
77
+ *
78
+ * So the residual is exactly this: a pair nested within a few millimetres is
79
+ * listed as a `minor` candidate overlap. That is the intended reading of
80
+ * `minor` — "these two coincide, and this pass will not claim they are the same
81
+ * object" — and a reviewer still sees it, which for a coincidence hunt is the
82
+ * safe direction.
83
+ *
84
+ * Measured across five public models (duplex, AC20-FZK-Haus, Office_A_20110811,
85
+ * dental_clinic, Infra-Bridge), *no* reported pair was a nested one: all 33
86
+ * findings are same-type pairs of equal bounds, and the count of nested or
87
+ * cross-type pairs stays 0 even at a 250 mm tolerance, 25× the default. Note
88
+ * what that does not cover — none of the five carries pipe or duct segments, so
89
+ * the pipe-in-sleeve case is evidenced only by the constructed fixtures above.
90
+ *
91
+ * Closing the residual would need a narrow phase, which this pass deliberately
92
+ * does not run, and the cheap substitutes are worse rather than merely weaker.
93
+ * An area-weighted centroid plus surface inertia — the obvious candidate, and
94
+ * the same O(triangles) cost as the signature (3.6 ms vs 2.6 ms over
95
+ * dental_clinic's 236,795 triangles) — discriminates *less* on every nested
96
+ * case measured, contributes nothing at all when the nesting is concentric
97
+ * (identical centroids for pipe-in-sleeve and pipe-plus-insulation), and is not
98
+ * tessellation-invariant: it reads 39.6% apart on one box against a finer
99
+ * triangulation of the same box, which would break the exact-duplicate case
100
+ * this signature exists to keep together. A voxel occupancy sample would
101
+ * separate nested from coincident, but it needs a cell size — reintroducing the
102
+ * scale-dependent knob that replacing the IoU gate removed.
24
103
  */
25
104
  export declare function findDuplicates(elements: ClashElement[], options?: DuplicateOptions): ClashResult;
26
105
  //# sourceMappingURL=duplicates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"duplicates.d.ts","sourceRoot":"","sources":["../src/duplicates.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAEV,YAAY,EAEZ,WAAW,EACX,SAAS,EAGT,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;kFAC8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;uDACmD;IACnD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iFAAiF;IACjF,UAAU,CAAC,EAAE,YAAY,CAAC;CAC3B;AAQD,eAAO,MAAM,eAAe,EAAE,SAK7B,CAAC;AAkEF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,OAAO,GAAE,gBAAqB,GAAG,WAAW,CAqGpG"}
1
+ {"version":3,"file":"duplicates.d.ts","sourceRoot":"","sources":["../src/duplicates.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAEV,YAAY,EAEZ,WAAW,EACX,SAAS,EAGT,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;wBAEoB;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;;;;;OAcG;IAIH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;4DACwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,UAAU,CAAC,EAAE,YAAY,CAAC;CAC3B;AAWD,eAAO,MAAM,eAAe,EAAE,SAK7B,CAAC;AAmBF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,OAAO,GAAE,gBAAqB,GAAG,WAAW,CA4MpG"}