@zkmake/three-audit 0.1.0 → 0.2.1
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/README.md +42 -0
- package/dist/audit-scene-D5ZaENGZ.js +425 -0
- package/dist/audit-scene-D5ZaENGZ.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +144 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +1 -411
- package/dist/index.js.map +1 -1
- package/dist/load-nqfCU3-k.js +216 -0
- package/dist/load-nqfCU3-k.js.map +1 -0
- package/dist/node.d.ts +15 -0
- package/dist/node.js +2 -0
- package/package.json +18 -1
package/README.md
CHANGED
|
@@ -33,6 +33,48 @@ A failure lists each pair of meshes with faces in one plane, facing the same way
|
|
|
33
33
|
[{ a: "windmill/plinth [stone] @ (0, 0.1, 0)", b: "windmill/wall [plaster] @ (0, 0.5, 0)", triangles: 4 }]
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## From the command line
|
|
37
|
+
|
|
38
|
+
Check exported models without writing any code:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npx @zkmake/three-audit assets/*.glb # or bunx
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
windmill.glb 4,812 triangles
|
|
46
|
+
✗ 1 z-fighting pair
|
|
47
|
+
windmill/plinth [stone] @ (0, 0.1, 0)
|
|
48
|
+
↔ windmill/wall [plaster] @ (0, 0.5, 0) (4 triangles)
|
|
49
|
+
✓ no NaN geometry
|
|
50
|
+
most triangles:
|
|
51
|
+
1,920 sails
|
|
52
|
+
…
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
It exits 1 when any model fails a check (2 when one can't be read), so it can gate CI. Options:
|
|
56
|
+
`--gap <m>`, `--self`, `--skip <regex>` (leave out objects by name, e.g. decals), `--top <n>`,
|
|
57
|
+
`--json`, `--no-fail`.
|
|
58
|
+
|
|
59
|
+
`.glb` and `.gltf` (buffers beside it) both load. Textures are stripped first: no check reads them.
|
|
60
|
+
Draco models need `draco3dgltf` installed and Meshopt models `meshoptimizer`
|
|
61
|
+
(`npx -p @zkmake/three-audit -p draco3dgltf three-audit model.glb`).
|
|
62
|
+
|
|
63
|
+
In a test, load a model the same way:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { findZFighting } from "@zkmake/three-audit";
|
|
67
|
+
import { loadModel } from "@zkmake/three-audit/node";
|
|
68
|
+
|
|
69
|
+
test("windmill.glb has no z-fighting", async () => {
|
|
70
|
+
expect(findZFighting(await loadModel("assets/windmill.glb"))).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
glTF is in metres, which is what `--gap`'s default assumes. Instanced meshes
|
|
75
|
+
(`EXT_mesh_gpu_instancing`) aren't checked for z-fighting; skinned and morphing meshes are
|
|
76
|
+
checked in their rest pose.
|
|
77
|
+
|
|
36
78
|
## In the console
|
|
37
79
|
|
|
38
80
|
```ts
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
//#region src/scene.ts
|
|
2
|
+
/** Depth-first, in child order. A skipped (or, with `visibleOnly`, hidden) object prunes its subtree. */
|
|
3
|
+
const walk = (root, options, visit) => {
|
|
4
|
+
const stack = [root];
|
|
5
|
+
while (stack.length > 0) {
|
|
6
|
+
const object = stack.pop();
|
|
7
|
+
if (options.visibleOnly === true && !object.visible || options.skip?.(object) === true) continue;
|
|
8
|
+
visit(object);
|
|
9
|
+
for (let i = object.children.length - 1; i >= 0; i -= 1) stack.push(object.children[i]);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const isMesh = (object) => object.isMesh === true;
|
|
13
|
+
const isInstanced = (mesh) => mesh.isInstancedMesh === true;
|
|
14
|
+
const isBatched = (mesh) => mesh.isBatchedMesh === true;
|
|
15
|
+
/** Triangles one draw of this geometry makes: indexed or not, within its draw range. */
|
|
16
|
+
const triangleCount = (geometry) => {
|
|
17
|
+
const elements = geometry.index?.count ?? geometry.attributes.position?.count ?? 0;
|
|
18
|
+
const { start, count } = geometry.drawRange;
|
|
19
|
+
const drawn = Math.min(elements, start + count) - start;
|
|
20
|
+
return Math.max(0, Math.floor(drawn / 3));
|
|
21
|
+
};
|
|
22
|
+
/** Triangles a batch draws across its visible instances, and how many instances that is. */
|
|
23
|
+
const batchedTriangles = (batch) => {
|
|
24
|
+
let triangles = 0;
|
|
25
|
+
let instances = 0;
|
|
26
|
+
for (let id = 0; id < batch.maxInstanceCount; id += 1) {
|
|
27
|
+
let geometryId;
|
|
28
|
+
try {
|
|
29
|
+
if (!batch.getVisibleAt(id)) continue;
|
|
30
|
+
geometryId = batch.getGeometryIdAt(id);
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const range = batch.getGeometryRangeAt(geometryId);
|
|
35
|
+
const elements = range.count ?? (batch.geometry.index ? range.indexCount : range.vertexCount);
|
|
36
|
+
triangles += Math.floor(elements / 3);
|
|
37
|
+
instances += 1;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
instances,
|
|
41
|
+
triangles
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/** The tag of the nearest tagged ancestor (the object itself included), or `null`. */
|
|
45
|
+
const tagOf = (object, tagKey) => {
|
|
46
|
+
for (let at = object; at; at = at.parent) {
|
|
47
|
+
const tag = at.userData[tagKey];
|
|
48
|
+
if (tag !== void 0 && tag !== null) return String(tag);
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
52
|
+
const hasTag = (root, tagKey, skip) => {
|
|
53
|
+
let found = false;
|
|
54
|
+
walk(root, { skip }, (object) => {
|
|
55
|
+
found ||= object.userData[tagKey] !== void 0 && object.userData[tagKey] !== null;
|
|
56
|
+
});
|
|
57
|
+
return found;
|
|
58
|
+
};
|
|
59
|
+
const meshName = (mesh) => mesh.name || mesh.geometry.name || mesh.geometry.type;
|
|
60
|
+
const materialName = (mesh) => {
|
|
61
|
+
const material = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
62
|
+
return material ? material.name || material.type : "none";
|
|
63
|
+
};
|
|
64
|
+
/** Adds a property JSON and `console.table` leave out: a live object that would serialise the scene. */
|
|
65
|
+
const withHidden = (row, key, value) => Object.defineProperty(row, key, {
|
|
66
|
+
value,
|
|
67
|
+
enumerable: false
|
|
68
|
+
});
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/bad-geometry.ts
|
|
71
|
+
const corner = (geometry, k) => geometry.index?.getX(k) ?? k;
|
|
72
|
+
const check = (geometry) => {
|
|
73
|
+
const position = geometry.attributes.position;
|
|
74
|
+
const normal = geometry.attributes.normal;
|
|
75
|
+
const flagged = new Uint8Array(position.count);
|
|
76
|
+
let nonFinite = 0;
|
|
77
|
+
let zeroNormals = 0;
|
|
78
|
+
for (let v = 0; v < position.count; v += 1) if (!(Number.isFinite(position.getX(v)) && Number.isFinite(position.getY(v)) && Number.isFinite(position.getZ(v)) && (!normal || Number.isFinite(normal.getX(v)) && Number.isFinite(normal.getY(v)) && Number.isFinite(normal.getZ(v))))) {
|
|
79
|
+
flagged[v] = 1;
|
|
80
|
+
nonFinite += 1;
|
|
81
|
+
}
|
|
82
|
+
if (!normal) return {
|
|
83
|
+
nonFinite,
|
|
84
|
+
zeroNormals
|
|
85
|
+
};
|
|
86
|
+
const corners = geometry.index?.count ?? position.count;
|
|
87
|
+
for (let k = 0; k + 2 < corners; k += 3) {
|
|
88
|
+
const a = corner(geometry, k);
|
|
89
|
+
const b = corner(geometry, k + 1);
|
|
90
|
+
const c = corner(geometry, k + 2);
|
|
91
|
+
const ux = position.getX(b) - position.getX(a);
|
|
92
|
+
const uy = position.getY(b) - position.getY(a);
|
|
93
|
+
const uz = position.getZ(b) - position.getZ(a);
|
|
94
|
+
const vx = position.getX(c) - position.getX(a);
|
|
95
|
+
const vy = position.getY(c) - position.getY(a);
|
|
96
|
+
const vz = position.getZ(c) - position.getZ(a);
|
|
97
|
+
if (!(Math.hypot(uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx) > 1e-8)) continue;
|
|
98
|
+
for (const v of [
|
|
99
|
+
a,
|
|
100
|
+
b,
|
|
101
|
+
c
|
|
102
|
+
]) if (flagged[v] === 0 && Math.hypot(normal.getX(v), normal.getY(v), normal.getZ(v)) < 1e-4) {
|
|
103
|
+
flagged[v] = 1;
|
|
104
|
+
zeroNormals += 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
nonFinite,
|
|
109
|
+
zeroNormals
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
/** Meshes with geometry that renders NaN, hidden ones included. `[]` is a pass. */
|
|
113
|
+
const findBadGeometry = (root, options = {}) => {
|
|
114
|
+
const tagKey = options.tagKey ?? "studioObject";
|
|
115
|
+
const rows = [];
|
|
116
|
+
walk(root, { skip: options.skip }, (object) => {
|
|
117
|
+
if (!isMesh(object) || !object.geometry.attributes.position) return;
|
|
118
|
+
const { nonFinite, zeroNormals } = check(object.geometry);
|
|
119
|
+
if (nonFinite > 0 || zeroNormals > 0) {
|
|
120
|
+
const under = tagOf(object, tagKey) ?? object.parent?.name ?? "";
|
|
121
|
+
rows.push(withHidden({
|
|
122
|
+
mesh: meshName(object),
|
|
123
|
+
under,
|
|
124
|
+
nonFinite,
|
|
125
|
+
zeroNormals
|
|
126
|
+
}, "object", object));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return rows;
|
|
130
|
+
};
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/triangles.ts
|
|
133
|
+
const cost = (mesh) => {
|
|
134
|
+
if (isBatched(mesh)) return batchedTriangles(mesh);
|
|
135
|
+
const instances = isInstanced(mesh) ? mesh.count : 1;
|
|
136
|
+
return {
|
|
137
|
+
instances,
|
|
138
|
+
triangles: triangleCount(mesh.geometry) * instances
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Triangles one pass draws: visible meshes, instances counted, no frustum culling. A shadow pass
|
|
143
|
+
* adds roughly as much again for each shadow-casting light.
|
|
144
|
+
*/
|
|
145
|
+
const countTriangles = (root, options = {}) => {
|
|
146
|
+
let total = 0;
|
|
147
|
+
walk(root, {
|
|
148
|
+
skip: options.skip,
|
|
149
|
+
visibleOnly: true
|
|
150
|
+
}, (object) => {
|
|
151
|
+
if (isMesh(object)) total += cost(object).triangles;
|
|
152
|
+
});
|
|
153
|
+
return total;
|
|
154
|
+
};
|
|
155
|
+
/** Every visible mesh, most triangles drawn first. */
|
|
156
|
+
const listMeshes = (root, options = {}) => {
|
|
157
|
+
const rows = [];
|
|
158
|
+
walk(root, {
|
|
159
|
+
skip: options.skip,
|
|
160
|
+
visibleOnly: true
|
|
161
|
+
}, (object) => {
|
|
162
|
+
if (!isMesh(object)) return;
|
|
163
|
+
const { instances, triangles } = cost(object);
|
|
164
|
+
const row = {
|
|
165
|
+
name: meshName(object),
|
|
166
|
+
triangles: instances > 0 ? Math.round(triangles / instances) : 0,
|
|
167
|
+
instances,
|
|
168
|
+
total: triangles
|
|
169
|
+
};
|
|
170
|
+
rows.push(withHidden(row, "mesh", object));
|
|
171
|
+
});
|
|
172
|
+
return rows.sort((a, b) => b.total - a.total);
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Geometries by triangles drawn, hidden meshes included, most first. Name geometries
|
|
176
|
+
* (`geometry.name`) to make this readable. Batches are left out: their shared buffer is not one
|
|
177
|
+
* geometry; `listMeshes` counts them.
|
|
178
|
+
*/
|
|
179
|
+
const geometryCensus = (root, options = {}) => {
|
|
180
|
+
const rows = /* @__PURE__ */ new Map();
|
|
181
|
+
walk(root, { skip: options.skip }, (object) => {
|
|
182
|
+
if (!isMesh(object) || isBatched(object) || !object.geometry.attributes.position) return;
|
|
183
|
+
const geometry = object.geometry;
|
|
184
|
+
const key = geometry.name || `${geometry.type}:${geometry.uuid.slice(0, 6)}`;
|
|
185
|
+
const triangles = triangleCount(geometry);
|
|
186
|
+
const uses = isInstanced(object) ? object.count : 1;
|
|
187
|
+
const row = rows.get(key) ?? {
|
|
188
|
+
geometry: key,
|
|
189
|
+
uses: 0,
|
|
190
|
+
triangles,
|
|
191
|
+
total: 0
|
|
192
|
+
};
|
|
193
|
+
row.uses += uses;
|
|
194
|
+
row.total += triangles * uses;
|
|
195
|
+
rows.set(key, row);
|
|
196
|
+
});
|
|
197
|
+
return [...rows.values()].sort((a, b) => b.total - a.total);
|
|
198
|
+
};
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/z-fighting.ts
|
|
201
|
+
/** Normals quantised to 1/40: neighbouring buckets are searched, so this only sets bucket size. */
|
|
202
|
+
const QUANT = 40;
|
|
203
|
+
/** Normals this aligned count as parallel (about 2.6°). */
|
|
204
|
+
const PARALLEL = .999;
|
|
205
|
+
const round = (value) => Math.round(value * 100) / 100;
|
|
206
|
+
const labelOf = (part) => {
|
|
207
|
+
const centre = part.min.map((low, axis) => round((low + part.max[axis]) / 2));
|
|
208
|
+
return `${part.tag === null ? "" : `${part.tag}/`}${meshName(part.mesh)} [${materialName(part.mesh)}] @ (${centre.join(", ")})`;
|
|
209
|
+
};
|
|
210
|
+
/** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */
|
|
211
|
+
const findZFighting = (root, options = {}) => {
|
|
212
|
+
const gap = options.gap ?? .004;
|
|
213
|
+
const self = options.self ?? false;
|
|
214
|
+
const tagKey = options.tagKey ?? "studioObject";
|
|
215
|
+
const tagged = options.tagged ?? hasTag(root, tagKey, options.skip);
|
|
216
|
+
root.updateWorldMatrix(true, true);
|
|
217
|
+
const parts = [];
|
|
218
|
+
const owner = [];
|
|
219
|
+
const planes = [];
|
|
220
|
+
const corners = [];
|
|
221
|
+
const keys = [];
|
|
222
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
223
|
+
const keyOf = (x, y, z, d) => `${x},${y},${z},${d}`;
|
|
224
|
+
walk(root, { skip: options.skip }, (object) => {
|
|
225
|
+
if (!isMesh(object) || isInstanced(object) || isBatched(object) || !object.geometry.attributes.position) return;
|
|
226
|
+
const tag = tagOf(object, tagKey);
|
|
227
|
+
if (tagged && tag === null) return;
|
|
228
|
+
const part = parts.length;
|
|
229
|
+
const { index, attributes } = object.geometry;
|
|
230
|
+
const position = attributes.position;
|
|
231
|
+
const e = object.matrixWorld.elements;
|
|
232
|
+
const min = [
|
|
233
|
+
Infinity,
|
|
234
|
+
Infinity,
|
|
235
|
+
Infinity
|
|
236
|
+
];
|
|
237
|
+
const max = [
|
|
238
|
+
-Infinity,
|
|
239
|
+
-Infinity,
|
|
240
|
+
-Infinity
|
|
241
|
+
];
|
|
242
|
+
const count = index?.count ?? position.count;
|
|
243
|
+
const world = [
|
|
244
|
+
0,
|
|
245
|
+
0,
|
|
246
|
+
0,
|
|
247
|
+
0,
|
|
248
|
+
0,
|
|
249
|
+
0,
|
|
250
|
+
0,
|
|
251
|
+
0,
|
|
252
|
+
0
|
|
253
|
+
];
|
|
254
|
+
parts.push({
|
|
255
|
+
mesh: object,
|
|
256
|
+
tag,
|
|
257
|
+
min,
|
|
258
|
+
max
|
|
259
|
+
});
|
|
260
|
+
for (let k = 0; k + 2 < count; k += 3) {
|
|
261
|
+
for (let c = 0; c < 3; c += 1) {
|
|
262
|
+
const v = index?.getX(k + c) ?? k + c;
|
|
263
|
+
const x = position.getX(v);
|
|
264
|
+
const y = position.getY(v);
|
|
265
|
+
const z = position.getZ(v);
|
|
266
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
267
|
+
const value = e[axis] * x + e[axis + 4] * y + e[axis + 8] * z + e[axis + 12];
|
|
268
|
+
world[c * 3 + axis] = value;
|
|
269
|
+
min[axis] = Math.min(min[axis], value);
|
|
270
|
+
max[axis] = Math.max(max[axis], value);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const [ax, ay, az, bx, by, bz, cx, cy, cz] = world;
|
|
274
|
+
const ux = bx - ax;
|
|
275
|
+
const uy = by - ay;
|
|
276
|
+
const uz = bz - az;
|
|
277
|
+
const vx = cx - ax;
|
|
278
|
+
const vy = cy - ay;
|
|
279
|
+
const vz = cz - az;
|
|
280
|
+
let nx = uy * vz - uz * vy;
|
|
281
|
+
let ny = uz * vx - ux * vz;
|
|
282
|
+
let nz = ux * vy - uy * vx;
|
|
283
|
+
const length = Math.hypot(nx, ny, nz);
|
|
284
|
+
if (!(length / 2 >= 1e-5)) continue;
|
|
285
|
+
nx /= length;
|
|
286
|
+
ny /= length;
|
|
287
|
+
nz /= length;
|
|
288
|
+
const d = nx * ax + ny * ay + nz * az;
|
|
289
|
+
const id = owner.length;
|
|
290
|
+
const key = [
|
|
291
|
+
Math.round(nx * QUANT),
|
|
292
|
+
Math.round(ny * QUANT),
|
|
293
|
+
Math.round(nz * QUANT),
|
|
294
|
+
Math.round(d / gap)
|
|
295
|
+
];
|
|
296
|
+
const bucket = buckets.get(keyOf(...key));
|
|
297
|
+
owner.push(part);
|
|
298
|
+
planes.push(nx, ny, nz, d);
|
|
299
|
+
corners.push(...world);
|
|
300
|
+
keys.push(key);
|
|
301
|
+
if (bucket) bucket.push(id);
|
|
302
|
+
else buckets.set(keyOf(...key), [id]);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
const margin = gap / 2;
|
|
306
|
+
const flat = [
|
|
307
|
+
0,
|
|
308
|
+
0,
|
|
309
|
+
0,
|
|
310
|
+
0,
|
|
311
|
+
0,
|
|
312
|
+
0,
|
|
313
|
+
0,
|
|
314
|
+
0,
|
|
315
|
+
0,
|
|
316
|
+
0,
|
|
317
|
+
0,
|
|
318
|
+
0
|
|
319
|
+
];
|
|
320
|
+
const overlaps = (s, t) => {
|
|
321
|
+
const S = s * 9;
|
|
322
|
+
const T = t * 9;
|
|
323
|
+
let ux = corners[S + 3] - corners[S];
|
|
324
|
+
let uy = corners[S + 4] - corners[S + 1];
|
|
325
|
+
let uz = corners[S + 5] - corners[S + 2];
|
|
326
|
+
const length = Math.hypot(ux, uy, uz) || 1;
|
|
327
|
+
ux /= length;
|
|
328
|
+
uy /= length;
|
|
329
|
+
uz /= length;
|
|
330
|
+
const nx = planes[s * 4];
|
|
331
|
+
const ny = planes[s * 4 + 1];
|
|
332
|
+
const nz = planes[s * 4 + 2];
|
|
333
|
+
const vx = ny * uz - nz * uy;
|
|
334
|
+
const vy = nz * ux - nx * uz;
|
|
335
|
+
const vz = nx * uy - ny * ux;
|
|
336
|
+
for (let i = 0; i < 6; i += 1) {
|
|
337
|
+
const at = i < 3 ? S + i * 3 : T + (i - 3) * 3;
|
|
338
|
+
const px = corners[at];
|
|
339
|
+
const py = corners[at + 1];
|
|
340
|
+
const pz = corners[at + 2];
|
|
341
|
+
flat[i * 2] = px * ux + py * uy + pz * uz;
|
|
342
|
+
flat[i * 2 + 1] = px * vx + py * vy + pz * vz;
|
|
343
|
+
}
|
|
344
|
+
for (let edge = 0; edge < 6; edge += 1) {
|
|
345
|
+
const from = edge;
|
|
346
|
+
const to = edge < 3 ? (edge + 1) % 3 : 3 + (edge + 1) % 3;
|
|
347
|
+
const ex = flat[to * 2] - flat[from * 2];
|
|
348
|
+
const ey = flat[to * 2 + 1] - flat[from * 2 + 1];
|
|
349
|
+
const edgeLength = Math.hypot(ex, ey) || 1;
|
|
350
|
+
const axisX = -ey / edgeLength;
|
|
351
|
+
const axisY = ex / edgeLength;
|
|
352
|
+
let p0 = Infinity;
|
|
353
|
+
let p1 = -Infinity;
|
|
354
|
+
let q0 = Infinity;
|
|
355
|
+
let q1 = -Infinity;
|
|
356
|
+
for (let i = 0; i < 6; i += 1) {
|
|
357
|
+
const value = flat[i * 2] * axisX + flat[i * 2 + 1] * axisY;
|
|
358
|
+
if (i < 3) {
|
|
359
|
+
p0 = Math.min(p0, value);
|
|
360
|
+
p1 = Math.max(p1, value);
|
|
361
|
+
} else {
|
|
362
|
+
q0 = Math.min(q0, value);
|
|
363
|
+
q1 = Math.max(q1, value);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (Math.min(p1, q1) - Math.max(p0, q0) < margin) return false;
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
};
|
|
370
|
+
const onPlane = (s, t) => {
|
|
371
|
+
const nx = planes[s * 4];
|
|
372
|
+
const ny = planes[s * 4 + 1];
|
|
373
|
+
const nz = planes[s * 4 + 2];
|
|
374
|
+
const d = planes[s * 4 + 3];
|
|
375
|
+
for (let c = 0; c < 3; c += 1) {
|
|
376
|
+
const at = t * 9 + c * 3;
|
|
377
|
+
if (Math.abs(nx * corners[at] + ny * corners[at + 1] + nz * corners[at + 2] - d) > gap) return false;
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
};
|
|
381
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
382
|
+
for (let s = 0; s < owner.length; s += 1) {
|
|
383
|
+
const [kx, ky, kz, kd] = keys[s];
|
|
384
|
+
for (let i = -1; i <= 1; i += 1) for (let j = -1; j <= 1; j += 1) for (let k = -1; k <= 1; k += 1) for (let l = -1; l <= 1; l += 1) for (const t of buckets.get(keyOf(kx + i, ky + j, kz + k, kd + l)) ?? []) {
|
|
385
|
+
const a = owner[s];
|
|
386
|
+
const b = owner[t];
|
|
387
|
+
if (t <= s || a === b && !self || planes[s * 4] * planes[t * 4] + planes[s * 4 + 1] * planes[t * 4 + 1] + planes[s * 4 + 2] * planes[t * 4 + 2] < PARALLEL || !onPlane(s, t) || !onPlane(t, s) || !overlaps(s, t)) continue;
|
|
388
|
+
const key = a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
389
|
+
const pair = pairs.get(key) ?? {
|
|
390
|
+
a: Math.min(a, b),
|
|
391
|
+
b: Math.max(a, b),
|
|
392
|
+
triangles: 0
|
|
393
|
+
};
|
|
394
|
+
pair.triangles += 1;
|
|
395
|
+
pairs.set(key, pair);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return [...pairs.values()].sort((x, y) => y.triangles - x.triangles).map(({ a, b, triangles }) => withHidden({
|
|
399
|
+
a: labelOf(parts[a]),
|
|
400
|
+
b: labelOf(parts[b]),
|
|
401
|
+
triangles
|
|
402
|
+
}, "meshes", [parts[a].mesh, parts[b].mesh]));
|
|
403
|
+
};
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/audit-scene.ts
|
|
406
|
+
/**
|
|
407
|
+
* Every check at once, JSON-safe: what a test asserts on or a headless browser prints. A clean
|
|
408
|
+
* scene has `zFighting` and `badGeometry` both `[]`.
|
|
409
|
+
*/
|
|
410
|
+
const auditScene = (root, options = {}) => {
|
|
411
|
+
const { skip, tagKey } = options;
|
|
412
|
+
return {
|
|
413
|
+
triangles: countTriangles(root, { skip }),
|
|
414
|
+
zFighting: findZFighting(root, options),
|
|
415
|
+
badGeometry: findBadGeometry(root, tagKey === void 0 ? { skip } : {
|
|
416
|
+
skip,
|
|
417
|
+
tagKey
|
|
418
|
+
}),
|
|
419
|
+
meshes: listMeshes(root, { skip }).slice(0, options.top ?? 10)
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
//#endregion
|
|
423
|
+
export { listMeshes as a, geometryCensus as i, findZFighting as n, findBadGeometry as o, countTriangles as r, triangleCount as s, auditScene as t };
|
|
424
|
+
|
|
425
|
+
//# sourceMappingURL=audit-scene-D5ZaENGZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-scene-D5ZaENGZ.js","names":[],"sources":["../src/scene.ts","../src/bad-geometry.ts","../src/triangles.ts","../src/z-fighting.ts","../src/audit-scene.ts"],"sourcesContent":["/**\n * Scene-graph plumbing every check shares: the walk, mesh kinds, triangle counts, tags and names.\n *\n * Nothing here imports three at runtime; objects are recognised by three's own `is*` flags, so the\n * checks work on whatever copy of three the app bundles.\n */\nimport type { BatchedMesh, BufferGeometry, InstancedMesh, Material, Mesh, Object3D } from \"three\";\n\n/** Leave an object and everything under it out of a check. */\nexport type Skip = (object: Object3D) => boolean;\n\nexport type WalkOptions = {\n skip?: Skip | undefined;\n /** Prune hidden objects and their subtrees, as the renderer does. */\n visibleOnly?: boolean;\n};\n\n/** Depth-first, in child order. A skipped (or, with `visibleOnly`, hidden) object prunes its subtree. */\nexport const walk = (root: Object3D, options: WalkOptions, visit: (object: Object3D) => void) => {\n const stack = [root];\n\n while (stack.length > 0) {\n const object = stack.pop()!;\n\n if ((options.visibleOnly === true && !object.visible) || options.skip?.(object) === true) {\n continue;\n }\n\n visit(object);\n\n for (let i = object.children.length - 1; i >= 0; i -= 1) {\n stack.push(object.children[i]!);\n }\n }\n};\n\nexport const isMesh = (object: Object3D): object is Mesh => (object as Mesh).isMesh === true;\n\nexport const isInstanced = (mesh: Mesh): mesh is InstancedMesh =>\n (mesh as InstancedMesh).isInstancedMesh === true;\n\nexport const isBatched = (mesh: Mesh): mesh is BatchedMesh =>\n (mesh as unknown as BatchedMesh).isBatchedMesh === true;\n\n/** Triangles one draw of this geometry makes: indexed or not, within its draw range. */\nexport const triangleCount = (geometry: BufferGeometry) => {\n const elements = geometry.index?.count ?? geometry.attributes.position?.count ?? 0;\n const { start, count } = geometry.drawRange;\n const drawn = Math.min(elements, start + count) - start;\n\n return Math.max(0, Math.floor(drawn / 3));\n};\n\n/** Triangles a batch draws across its visible instances, and how many instances that is. */\nexport const batchedTriangles = (batch: BatchedMesh) => {\n let triangles = 0;\n let instances = 0;\n\n for (let id = 0; id < batch.maxInstanceCount; id += 1) {\n let geometryId: number;\n\n // Deleted and never-used ids throw.\n try {\n if (!batch.getVisibleAt(id)) {\n continue;\n }\n\n geometryId = batch.getGeometryIdAt(id);\n } catch {\n continue;\n }\n\n const range = batch.getGeometryRangeAt(geometryId) as {\n count?: number;\n indexCount: number;\n vertexCount: number;\n };\n const elements = range.count ?? (batch.geometry.index ? range.indexCount : range.vertexCount);\n\n triangles += Math.floor(elements / 3);\n instances += 1;\n }\n\n return { instances, triangles };\n};\n\n/** The tag of the nearest tagged ancestor (the object itself included), or `null`. */\nexport const tagOf = (object: Object3D, tagKey: string) => {\n for (let at: Object3D | null = object; at; at = at.parent) {\n const tag: unknown = at.userData[tagKey];\n\n if (tag !== undefined && tag !== null) {\n return String(tag);\n }\n }\n\n return null;\n};\n\nexport const hasTag = (root: Object3D, tagKey: string, skip?: Skip) => {\n let found = false;\n\n walk(root, { skip }, (object) => {\n found ||= object.userData[tagKey] !== undefined && object.userData[tagKey] !== null;\n });\n\n return found;\n};\n\nexport const meshName = (mesh: Mesh) => mesh.name || mesh.geometry.name || mesh.geometry.type;\n\nexport const materialName = (mesh: Mesh) => {\n const material = (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material) as\n | Material\n | undefined;\n\n return material ? material.name || material.type : \"none\";\n};\n\n/** Adds a property JSON and `console.table` leave out: a live object that would serialise the scene. */\nexport const withHidden = <Row extends object, Key extends string, Value>(\n row: Row,\n key: Key,\n value: Value,\n) => Object.defineProperty(row, key, { value, enumerable: false }) as Row & Record<Key, Value>;\n","/**\n * Geometry that lights a pixel NaN: a non-finite position or normal, or a zero-length normal on a\n * triangle with area. Bloom and other blurs spread one NaN pixel into a black block.\n */\nimport type { BufferGeometry, Mesh, Object3D } from \"three\";\n\nimport { isMesh, meshName, type Skip, tagOf, walk, withHidden } from \"./scene.ts\";\n\nexport type BadGeometryOptions = {\n skip?: Skip | undefined;\n /** `userData` key naming the part a mesh belongs to, for the `under` column. */\n tagKey?: string;\n};\n\nexport type BadGeometryRow = {\n mesh: string;\n /** The tagged part it sits in, or its parent's name. */\n under: string;\n /** Vertices with a NaN or infinite position or normal component. */\n nonFinite: number;\n /** Vertices of triangles with area whose normal has no length. */\n zeroNormals: number;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly object: Mesh;\n};\n\nconst corner = (geometry: BufferGeometry, k: number) => geometry.index?.getX(k) ?? k;\n\nconst check = (geometry: BufferGeometry) => {\n const position = geometry.attributes.position!;\n const normal = geometry.attributes.normal;\n const flagged = new Uint8Array(position.count);\n let nonFinite = 0;\n let zeroNormals = 0;\n\n for (let v = 0; v < position.count; v += 1) {\n const finite =\n Number.isFinite(position.getX(v)) &&\n Number.isFinite(position.getY(v)) &&\n Number.isFinite(position.getZ(v)) &&\n (!normal ||\n (Number.isFinite(normal.getX(v)) &&\n Number.isFinite(normal.getY(v)) &&\n Number.isFinite(normal.getZ(v))));\n\n if (!finite) {\n flagged[v] = 1;\n nonFinite += 1;\n }\n }\n\n if (!normal) {\n return { nonFinite, zeroNormals };\n }\n\n const corners = geometry.index?.count ?? position.count;\n\n for (let k = 0; k + 2 < corners; k += 3) {\n const a = corner(geometry, k);\n const b = corner(geometry, k + 1);\n const c = corner(geometry, k + 2);\n const ux = position.getX(b) - position.getX(a);\n const uy = position.getY(b) - position.getY(a);\n const uz = position.getZ(b) - position.getZ(a);\n const vx = position.getX(c) - position.getX(a);\n const vy = position.getY(c) - position.getY(a);\n const vz = position.getZ(c) - position.getZ(a);\n const area = Math.hypot(uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx);\n\n if (!(area > 1e-8)) {\n continue;\n }\n\n for (const v of [a, b, c]) {\n if (flagged[v] === 0 && Math.hypot(normal.getX(v), normal.getY(v), normal.getZ(v)) < 1e-4) {\n flagged[v] = 1;\n zeroNormals += 1;\n }\n }\n }\n\n return { nonFinite, zeroNormals };\n};\n\n/** Meshes with geometry that renders NaN, hidden ones included. `[]` is a pass. */\nexport const findBadGeometry = (\n root: Object3D,\n options: BadGeometryOptions = {},\n): BadGeometryRow[] => {\n const tagKey = options.tagKey ?? \"studioObject\";\n const rows: BadGeometryRow[] = [];\n\n walk(root, { skip: options.skip }, (object) => {\n if (!isMesh(object) || !object.geometry.attributes.position) {\n return;\n }\n\n const { nonFinite, zeroNormals } = check(object.geometry);\n\n if (nonFinite > 0 || zeroNormals > 0) {\n const under = tagOf(object, tagKey) ?? object.parent?.name ?? \"\";\n\n rows.push(\n withHidden({ mesh: meshName(object), under, nonFinite, zeroNormals }, \"object\", object),\n );\n }\n });\n\n return rows;\n};\n","/**\n * Where a triangle budget goes: the total one pass draws, the meshes drawing it, and the\n * geometries behind them.\n */\nimport type { Mesh, Object3D } from \"three\";\n\nimport {\n batchedTriangles,\n isBatched,\n isInstanced,\n isMesh,\n meshName,\n type Skip,\n triangleCount,\n walk,\n withHidden,\n} from \"./scene.ts\";\n\nexport type TriangleOptions = { skip?: Skip | undefined };\n\nexport type MeshRow = {\n name: string;\n /** Triangles in one copy (a batch's average). */\n triangles: number;\n instances: number;\n total: number;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly mesh: Mesh;\n};\n\nexport type GeometryRow = {\n /** `geometry.name`, or its type and the start of its uuid. */\n geometry: string;\n /** Copies drawn: every mesh using it, instances counted. */\n uses: number;\n triangles: number;\n total: number;\n};\n\nconst cost = (mesh: Mesh) => {\n if (isBatched(mesh)) {\n return batchedTriangles(mesh);\n }\n\n const instances = isInstanced(mesh) ? mesh.count : 1;\n\n return { instances, triangles: triangleCount(mesh.geometry) * instances };\n};\n\n/**\n * Triangles one pass draws: visible meshes, instances counted, no frustum culling. A shadow pass\n * adds roughly as much again for each shadow-casting light.\n */\nexport const countTriangles = (root: Object3D, options: TriangleOptions = {}) => {\n let total = 0;\n\n walk(root, { skip: options.skip, visibleOnly: true }, (object) => {\n if (isMesh(object)) {\n total += cost(object).triangles;\n }\n });\n\n return total;\n};\n\n/** Every visible mesh, most triangles drawn first. */\nexport const listMeshes = (root: Object3D, options: TriangleOptions = {}): MeshRow[] => {\n const rows: MeshRow[] = [];\n\n walk(root, { skip: options.skip, visibleOnly: true }, (object) => {\n if (!isMesh(object)) {\n return;\n }\n\n const { instances, triangles } = cost(object);\n const row = {\n name: meshName(object),\n triangles: instances > 0 ? Math.round(triangles / instances) : 0,\n instances,\n total: triangles,\n };\n\n rows.push(withHidden(row, \"mesh\", object));\n });\n\n return rows.sort((a, b) => b.total - a.total);\n};\n\n/**\n * Geometries by triangles drawn, hidden meshes included, most first. Name geometries\n * (`geometry.name`) to make this readable. Batches are left out: their shared buffer is not one\n * geometry; `listMeshes` counts them.\n */\nexport const geometryCensus = (root: Object3D, options: TriangleOptions = {}): GeometryRow[] => {\n const rows = new Map<string, GeometryRow>();\n\n walk(root, { skip: options.skip }, (object) => {\n if (!isMesh(object) || isBatched(object) || !object.geometry.attributes.position) {\n return;\n }\n\n const geometry = object.geometry;\n const key = geometry.name || `${geometry.type}:${geometry.uuid.slice(0, 6)}`;\n const triangles = triangleCount(geometry);\n const uses = isInstanced(object) ? object.count : 1;\n const row = rows.get(key) ?? { geometry: key, uses: 0, triangles, total: 0 };\n\n row.uses += uses;\n row.total += triangles * uses;\n rows.set(key, row);\n });\n\n return [...rows.values()].sort((a, b) => b.total - a.total);\n};\n","/**\n * Z-fighting found by geometry instead of by eye: pairs of meshes with triangles in (nearly) one\n * plane, facing the same way, overlapping. Those flicker wherever depth precision runs out.\n *\n * Triangles are bucketed by their quantised plane (normal and offset), so only triangles in\n * neighbouring buckets are compared. A pair counts only when each triangle's corners lie within\n * `gap` of the other's plane, measured at the triangles: the offsets are measured from the world\n * origin, and far from it two faces a degree or two apart can share one while metres apart where\n * they stand. Overlap in the shared plane is a separating-axis test.\n */\nimport type { Mesh, Object3D } from \"three\";\n\nimport {\n hasTag,\n isBatched,\n isInstanced,\n isMesh,\n materialName,\n meshName,\n type Skip,\n tagOf,\n walk,\n withHidden,\n} from \"./scene.ts\";\n\nexport type ZFightingOptions = {\n /**\n * How close two planes count as one, in world units. 0.004 (4 mm at 1 unit = 1 m) is about what\n * depth precision loses with a camera ~50 m out. Scale it with your units.\n */\n gap?: number;\n /** Also compare triangles within one mesh: for merged geometry whose parts share a buffer. */\n self?: boolean;\n /**\n * Only meshes inside a tagged object (`userData[tagKey]`), so a studio's ground and props stay\n * out. Defaults to true when anything under the root is tagged, false otherwise.\n */\n tagged?: boolean;\n tagKey?: string;\n skip?: Skip | undefined;\n};\n\nexport type ZFightingRow = {\n /** `tag/mesh [material] @ (x, y, z)`, the centre in world space. */\n a: string;\n b: string;\n /** Overlapping triangle pairs between the two. */\n triangles: number;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly meshes: readonly [Mesh, Mesh];\n};\n\n/** Normals quantised to 1/40: neighbouring buckets are searched, so this only sets bucket size. */\nconst QUANT = 40;\n/** Normals this aligned count as parallel (about 2.6°). */\nconst PARALLEL = 0.999;\n\ntype Part = { mesh: Mesh; tag: string | null; min: number[]; max: number[] };\n\nconst round = (value: number) => Math.round(value * 100) / 100;\n\nconst labelOf = (part: Part) => {\n const centre = part.min.map((low, axis) => round((low + part.max[axis]!) / 2));\n const tag = part.tag === null ? \"\" : `${part.tag}/`;\n\n return `${tag}${meshName(part.mesh)} [${materialName(part.mesh)}] @ (${centre.join(\", \")})`;\n};\n\n/** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */\nexport const findZFighting = (root: Object3D, options: ZFightingOptions = {}): ZFightingRow[] => {\n const gap = options.gap ?? 0.004;\n const self = options.self ?? false;\n const tagKey = options.tagKey ?? \"studioObject\";\n const tagged = options.tagged ?? hasTag(root, tagKey, options.skip);\n\n root.updateWorldMatrix(true, true);\n\n const parts: Part[] = [];\n // Per triangle: its part, plane (nx, ny, nz, d) and world corners (9 numbers).\n const owner: number[] = [];\n const planes: number[] = [];\n const corners: number[] = [];\n const keys: [number, number, number, number][] = [];\n const buckets = new Map<string, number[]>();\n const keyOf = (x: number, y: number, z: number, d: number) => `${x},${y},${z},${d}`;\n\n walk(root, { skip: options.skip }, (object) => {\n if (\n !isMesh(object) ||\n isInstanced(object) ||\n isBatched(object) ||\n !object.geometry.attributes.position\n ) {\n return;\n }\n\n const tag = tagOf(object, tagKey);\n\n if (tagged && tag === null) {\n return;\n }\n\n const part = parts.length;\n const { index, attributes } = object.geometry;\n const position = attributes.position!;\n const e = object.matrixWorld.elements;\n const min = [Infinity, Infinity, Infinity];\n const max = [-Infinity, -Infinity, -Infinity];\n const count = index?.count ?? position.count;\n const world = [0, 0, 0, 0, 0, 0, 0, 0, 0];\n\n parts.push({ mesh: object, tag, min, max });\n\n for (let k = 0; k + 2 < count; k += 3) {\n for (let c = 0; c < 3; c += 1) {\n const v = index?.getX(k + c) ?? k + c;\n const x = position.getX(v);\n const y = position.getY(v);\n const z = position.getZ(v);\n\n for (let axis = 0; axis < 3; axis += 1) {\n const value = e[axis]! * x + e[axis + 4]! * y + e[axis + 8]! * z + e[axis + 12]!;\n\n world[c * 3 + axis] = value;\n min[axis] = Math.min(min[axis]!, value);\n max[axis] = Math.max(max[axis]!, value);\n }\n }\n\n const [ax, ay, az, bx, by, bz, cx, cy, cz] = world as [\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n ];\n const ux = bx - ax;\n const uy = by - ay;\n const uz = bz - az;\n const vx = cx - ax;\n const vy = cy - ay;\n const vz = cz - az;\n let nx = uy * vz - uz * vy;\n let ny = uz * vx - ux * vz;\n let nz = ux * vy - uy * vx;\n const length = Math.hypot(nx, ny, nz);\n\n // Slivers under ~1e-5 m² show nothing.\n if (!(length / 2 >= 1e-5)) {\n continue;\n }\n\n nx /= length;\n ny /= length;\n nz /= length;\n\n const d = nx * ax + ny * ay + nz * az;\n const id = owner.length;\n const key: [number, number, number, number] = [\n Math.round(nx * QUANT),\n Math.round(ny * QUANT),\n Math.round(nz * QUANT),\n Math.round(d / gap),\n ];\n const bucket = buckets.get(keyOf(...key));\n\n owner.push(part);\n planes.push(nx, ny, nz, d);\n corners.push(...world);\n keys.push(key);\n\n if (bucket) {\n bucket.push(id);\n } else {\n buckets.set(keyOf(...key), [id]);\n }\n }\n });\n\n // Overlap in s's plane, with a margin so triangles merely sharing an edge don't count.\n const margin = gap / 2;\n const flat = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];\n\n const overlaps = (s: number, t: number) => {\n const S = s * 9;\n const T = t * 9;\n let ux = corners[S + 3]! - corners[S]!;\n let uy = corners[S + 4]! - corners[S + 1]!;\n let uz = corners[S + 5]! - corners[S + 2]!;\n const length = Math.hypot(ux, uy, uz) || 1;\n\n ux /= length;\n uy /= length;\n uz /= length;\n\n const nx = planes[s * 4]!;\n const ny = planes[s * 4 + 1]!;\n const nz = planes[s * 4 + 2]!;\n const vx = ny * uz - nz * uy;\n const vy = nz * ux - nx * uz;\n const vz = nx * uy - ny * ux;\n\n // Corners 0–2 are s, 3–5 are t, as (x, y) in the plane.\n for (let i = 0; i < 6; i += 1) {\n const at = i < 3 ? S + i * 3 : T + (i - 3) * 3;\n const px = corners[at]!;\n const py = corners[at + 1]!;\n const pz = corners[at + 2]!;\n\n flat[i * 2] = px * ux + py * uy + pz * uz;\n flat[i * 2 + 1] = px * vx + py * vy + pz * vz;\n }\n\n for (let edge = 0; edge < 6; edge += 1) {\n const from = edge;\n const to = edge < 3 ? (edge + 1) % 3 : 3 + ((edge + 1) % 3);\n const ex = flat[to * 2]! - flat[from * 2]!;\n const ey = flat[to * 2 + 1]! - flat[from * 2 + 1]!;\n const edgeLength = Math.hypot(ex, ey) || 1;\n const axisX = -ey / edgeLength;\n const axisY = ex / edgeLength;\n let p0 = Infinity;\n let p1 = -Infinity;\n let q0 = Infinity;\n let q1 = -Infinity;\n\n for (let i = 0; i < 6; i += 1) {\n const value = flat[i * 2]! * axisX + flat[i * 2 + 1]! * axisY;\n\n if (i < 3) {\n p0 = Math.min(p0, value);\n p1 = Math.max(p1, value);\n } else {\n q0 = Math.min(q0, value);\n q1 = Math.max(q1, value);\n }\n }\n\n if (Math.min(p1, q1) - Math.max(p0, q0) < margin) {\n return false;\n }\n }\n\n return true;\n };\n\n // Every corner of t within `gap` of s's plane, measured where t stands.\n const onPlane = (s: number, t: number) => {\n const nx = planes[s * 4]!;\n const ny = planes[s * 4 + 1]!;\n const nz = planes[s * 4 + 2]!;\n const d = planes[s * 4 + 3]!;\n\n for (let c = 0; c < 3; c += 1) {\n const at = t * 9 + c * 3;\n\n if (Math.abs(nx * corners[at]! + ny * corners[at + 1]! + nz * corners[at + 2]! - d) > gap) {\n return false;\n }\n }\n\n return true;\n };\n\n const pairs = new Map<string, { a: number; b: number; triangles: number }>();\n\n // Every neighbouring bucket, so near-equal planes that round apart still meet.\n for (let s = 0; s < owner.length; s += 1) {\n const [kx, ky, kz, kd] = keys[s]!;\n\n for (let i = -1; i <= 1; i += 1) {\n for (let j = -1; j <= 1; j += 1) {\n for (let k = -1; k <= 1; k += 1) {\n for (let l = -1; l <= 1; l += 1) {\n for (const t of buckets.get(keyOf(kx + i, ky + j, kz + k, kd + l)) ?? []) {\n const a = owner[s]!;\n const b = owner[t]!;\n\n if (\n t <= s ||\n (a === b && !self) ||\n planes[s * 4]! * planes[t * 4]! +\n planes[s * 4 + 1]! * planes[t * 4 + 1]! +\n planes[s * 4 + 2]! * planes[t * 4 + 2]! <\n PARALLEL ||\n !onPlane(s, t) ||\n !onPlane(t, s) ||\n !overlaps(s, t)\n ) {\n continue;\n }\n\n const key = a < b ? `${a}:${b}` : `${b}:${a}`;\n const pair = pairs.get(key) ?? { a: Math.min(a, b), b: Math.max(a, b), triangles: 0 };\n\n pair.triangles += 1;\n pairs.set(key, pair);\n }\n }\n }\n }\n }\n }\n\n return [...pairs.values()]\n .sort((x, y) => y.triangles - x.triangles)\n .map(({ a, b, triangles }) =>\n withHidden({ a: labelOf(parts[a]!), b: labelOf(parts[b]!), triangles }, \"meshes\", [\n parts[a]!.mesh,\n parts[b]!.mesh,\n ] as const),\n );\n};\n","import type { Object3D } from \"three\";\n\nimport { type BadGeometryRow, findBadGeometry } from \"./bad-geometry.ts\";\nimport type { Skip } from \"./scene.ts\";\nimport { countTriangles, listMeshes, type MeshRow } from \"./triangles.ts\";\nimport { findZFighting, type ZFightingOptions, type ZFightingRow } from \"./z-fighting.ts\";\n\nexport type AuditSceneOptions = Pick<ZFightingOptions, \"gap\" | \"self\" | \"tagged\"> & {\n skip?: Skip | undefined;\n tagKey?: string;\n /** How many of the costliest meshes to list. */\n top?: number;\n};\n\nexport type AuditReport = {\n triangles: number;\n zFighting: ZFightingRow[];\n badGeometry: BadGeometryRow[];\n /** The costliest meshes, most triangles first. */\n meshes: MeshRow[];\n};\n\n/**\n * Every check at once, JSON-safe: what a test asserts on or a headless browser prints. A clean\n * scene has `zFighting` and `badGeometry` both `[]`.\n */\nexport const auditScene = (root: Object3D, options: AuditSceneOptions = {}): AuditReport => {\n const { skip, tagKey } = options;\n\n return {\n triangles: countTriangles(root, { skip }),\n zFighting: findZFighting(root, options),\n badGeometry: findBadGeometry(root, tagKey === undefined ? { skip } : { skip, tagKey }),\n meshes: listMeshes(root, { skip }).slice(0, options.top ?? 10),\n };\n};\n"],"mappings":";;AAkBA,MAAa,QAAQ,MAAgB,SAAsB,UAAsC;CAC/F,MAAM,QAAQ,CAAC,IAAI;CAEnB,OAAO,MAAM,SAAS,GAAG;EACvB,MAAM,SAAS,MAAM,IAAI;EAEzB,IAAK,QAAQ,gBAAgB,QAAQ,CAAC,OAAO,WAAY,QAAQ,OAAO,MAAM,MAAM,MAClF;EAGF,MAAM,MAAM;EAEZ,KAAK,IAAI,IAAI,OAAO,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK,GACpD,MAAM,KAAK,OAAO,SAAS,EAAG;CAElC;AACF;AAEA,MAAa,UAAU,WAAsC,OAAgB,WAAW;AAExF,MAAa,eAAe,SACzB,KAAuB,oBAAoB;AAE9C,MAAa,aAAa,SACvB,KAAgC,kBAAkB;;AAGrD,MAAa,iBAAiB,aAA6B;CACzD,MAAM,WAAW,SAAS,OAAO,SAAS,SAAS,WAAW,UAAU,SAAS;CACjF,MAAM,EAAE,OAAO,UAAU,SAAS;CAClC,MAAM,QAAQ,KAAK,IAAI,UAAU,QAAQ,KAAK,IAAI;CAElD,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,CAAC,CAAC;AAC1C;;AAGA,MAAa,oBAAoB,UAAuB;CACtD,IAAI,YAAY;CAChB,IAAI,YAAY;CAEhB,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,kBAAkB,MAAM,GAAG;EACrD,IAAI;EAGJ,IAAI;GACF,IAAI,CAAC,MAAM,aAAa,EAAE,GACxB;GAGF,aAAa,MAAM,gBAAgB,EAAE;EACvC,QAAQ;GACN;EACF;EAEA,MAAM,QAAQ,MAAM,mBAAmB,UAAU;EAKjD,MAAM,WAAW,MAAM,UAAU,MAAM,SAAS,QAAQ,MAAM,aAAa,MAAM;EAEjF,aAAa,KAAK,MAAM,WAAW,CAAC;EACpC,aAAa;CACf;CAEA,OAAO;EAAE;EAAW;CAAU;AAChC;;AAGA,MAAa,SAAS,QAAkB,WAAmB;CACzD,KAAK,IAAI,KAAsB,QAAQ,IAAI,KAAK,GAAG,QAAQ;EACzD,MAAM,MAAe,GAAG,SAAS;EAEjC,IAAI,QAAQ,KAAA,KAAa,QAAQ,MAC/B,OAAO,OAAO,GAAG;CAErB;CAEA,OAAO;AACT;AAEA,MAAa,UAAU,MAAgB,QAAgB,SAAgB;CACrE,IAAI,QAAQ;CAEZ,KAAK,MAAM,EAAE,KAAK,IAAI,WAAW;EAC/B,UAAU,OAAO,SAAS,YAAY,KAAA,KAAa,OAAO,SAAS,YAAY;CACjF,CAAC;CAED,OAAO;AACT;AAEA,MAAa,YAAY,SAAe,KAAK,QAAQ,KAAK,SAAS,QAAQ,KAAK,SAAS;AAEzF,MAAa,gBAAgB,SAAe;CAC1C,MAAM,WAAY,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,SAAS,KAAK,KAAK;CAIzE,OAAO,WAAW,SAAS,QAAQ,SAAS,OAAO;AACrD;;AAGA,MAAa,cACX,KACA,KACA,UACG,OAAO,eAAe,KAAK,KAAK;CAAE;CAAO,YAAY;AAAM,CAAC;;;AClGjE,MAAM,UAAU,UAA0B,MAAc,SAAS,OAAO,KAAK,CAAC,KAAK;AAEnF,MAAM,SAAS,aAA6B;CAC1C,MAAM,WAAW,SAAS,WAAW;CACrC,MAAM,SAAS,SAAS,WAAW;CACnC,MAAM,UAAU,IAAI,WAAW,SAAS,KAAK;CAC7C,IAAI,YAAY;CAChB,IAAI,cAAc;CAElB,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,OAAO,KAAK,GAUvC,IAAI,EARF,OAAO,SAAS,SAAS,KAAK,CAAC,CAAC,KAChC,OAAO,SAAS,SAAS,KAAK,CAAC,CAAC,KAChC,OAAO,SAAS,SAAS,KAAK,CAAC,CAAC,MAC/B,CAAC,UACC,OAAO,SAAS,OAAO,KAAK,CAAC,CAAC,KAC7B,OAAO,SAAS,OAAO,KAAK,CAAC,CAAC,KAC9B,OAAO,SAAS,OAAO,KAAK,CAAC,CAAC,KAEvB;EACX,QAAQ,KAAK;EACb,aAAa;CACf;CAGF,IAAI,CAAC,QACH,OAAO;EAAE;EAAW;CAAY;CAGlC,MAAM,UAAU,SAAS,OAAO,SAAS,SAAS;CAElD,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,SAAS,KAAK,GAAG;EACvC,MAAM,IAAI,OAAO,UAAU,CAAC;EAC5B,MAAM,IAAI,OAAO,UAAU,IAAI,CAAC;EAChC,MAAM,IAAI,OAAO,UAAU,IAAI,CAAC;EAChC,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAG7C,IAAI,EAFS,KAAK,MAAM,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,EAEpE,IAAI,OACX;EAGF,KAAK,MAAM,KAAK;GAAC;GAAG;GAAG;EAAC,GACtB,IAAI,QAAQ,OAAO,KAAK,KAAK,MAAM,OAAO,KAAK,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,IAAI,MAAM;GACzF,QAAQ,KAAK;GACb,eAAe;EACjB;CAEJ;CAEA,OAAO;EAAE;EAAW;CAAY;AAClC;;AAGA,MAAa,mBACX,MACA,UAA8B,CAAC,MACV;CACrB,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,OAAyB,CAAC;CAEhC,KAAK,MAAM,EAAE,MAAM,QAAQ,KAAK,IAAI,WAAW;EAC7C,IAAI,CAAC,OAAO,MAAM,KAAK,CAAC,OAAO,SAAS,WAAW,UACjD;EAGF,MAAM,EAAE,WAAW,gBAAgB,MAAM,OAAO,QAAQ;EAExD,IAAI,YAAY,KAAK,cAAc,GAAG;GACpC,MAAM,QAAQ,MAAM,QAAQ,MAAM,KAAK,OAAO,QAAQ,QAAQ;GAE9D,KAAK,KACH,WAAW;IAAE,MAAM,SAAS,MAAM;IAAG;IAAO;IAAW;GAAY,GAAG,UAAU,MAAM,CACxF;EACF;CACF,CAAC;CAED,OAAO;AACT;;;ACtEA,MAAM,QAAQ,SAAe;CAC3B,IAAI,UAAU,IAAI,GAChB,OAAO,iBAAiB,IAAI;CAG9B,MAAM,YAAY,YAAY,IAAI,IAAI,KAAK,QAAQ;CAEnD,OAAO;EAAE;EAAW,WAAW,cAAc,KAAK,QAAQ,IAAI;CAAU;AAC1E;;;;;AAMA,MAAa,kBAAkB,MAAgB,UAA2B,CAAC,MAAM;CAC/E,IAAI,QAAQ;CAEZ,KAAK,MAAM;EAAE,MAAM,QAAQ;EAAM,aAAa;CAAK,IAAI,WAAW;EAChE,IAAI,OAAO,MAAM,GACf,SAAS,KAAK,MAAM,CAAC,CAAC;CAE1B,CAAC;CAED,OAAO;AACT;;AAGA,MAAa,cAAc,MAAgB,UAA2B,CAAC,MAAiB;CACtF,MAAM,OAAkB,CAAC;CAEzB,KAAK,MAAM;EAAE,MAAM,QAAQ;EAAM,aAAa;CAAK,IAAI,WAAW;EAChE,IAAI,CAAC,OAAO,MAAM,GAChB;EAGF,MAAM,EAAE,WAAW,cAAc,KAAK,MAAM;EAC5C,MAAM,MAAM;GACV,MAAM,SAAS,MAAM;GACrB,WAAW,YAAY,IAAI,KAAK,MAAM,YAAY,SAAS,IAAI;GAC/D;GACA,OAAO;EACT;EAEA,KAAK,KAAK,WAAW,KAAK,QAAQ,MAAM,CAAC;CAC3C,CAAC;CAED,OAAO,KAAK,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC9C;;;;;;AAOA,MAAa,kBAAkB,MAAgB,UAA2B,CAAC,MAAqB;CAC9F,MAAM,uBAAO,IAAI,IAAyB;CAE1C,KAAK,MAAM,EAAE,MAAM,QAAQ,KAAK,IAAI,WAAW;EAC7C,IAAI,CAAC,OAAO,MAAM,KAAK,UAAU,MAAM,KAAK,CAAC,OAAO,SAAS,WAAW,UACtE;EAGF,MAAM,WAAW,OAAO;EACxB,MAAM,MAAM,SAAS,QAAQ,GAAG,SAAS,KAAK,GAAG,SAAS,KAAK,MAAM,GAAG,CAAC;EACzE,MAAM,YAAY,cAAc,QAAQ;EACxC,MAAM,OAAO,YAAY,MAAM,IAAI,OAAO,QAAQ;EAClD,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK;GAAE,UAAU;GAAK,MAAM;GAAG;GAAW,OAAO;EAAE;EAE3E,IAAI,QAAQ;EACZ,IAAI,SAAS,YAAY;EACzB,KAAK,IAAI,KAAK,GAAG;CACnB,CAAC;CAED,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC5D;;;;AC5DA,MAAM,QAAQ;;AAEd,MAAM,WAAW;AAIjB,MAAM,SAAS,UAAkB,KAAK,MAAM,QAAQ,GAAG,IAAI;AAE3D,MAAM,WAAW,SAAe;CAC9B,MAAM,SAAS,KAAK,IAAI,KAAK,KAAK,SAAS,OAAO,MAAM,KAAK,IAAI,SAAU,CAAC,CAAC;CAG7E,OAAO,GAFK,KAAK,QAAQ,OAAO,KAAK,GAAG,KAAK,IAAI,KAEjC,SAAS,KAAK,IAAI,EAAE,IAAI,aAAa,KAAK,IAAI,EAAE,OAAO,OAAO,KAAK,IAAI,EAAE;AAC3F;;AAGA,MAAa,iBAAiB,MAAgB,UAA4B,CAAC,MAAsB;CAC/F,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,SAAS,QAAQ,UAAU,OAAO,MAAM,QAAQ,QAAQ,IAAI;CAElE,KAAK,kBAAkB,MAAM,IAAI;CAEjC,MAAM,QAAgB,CAAC;CAEvB,MAAM,QAAkB,CAAC;CACzB,MAAM,SAAmB,CAAC;CAC1B,MAAM,UAAoB,CAAC;CAC3B,MAAM,OAA2C,CAAC;CAClD,MAAM,0BAAU,IAAI,IAAsB;CAC1C,MAAM,SAAS,GAAW,GAAW,GAAW,MAAc,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;CAEhF,KAAK,MAAM,EAAE,MAAM,QAAQ,KAAK,IAAI,WAAW;EAC7C,IACE,CAAC,OAAO,MAAM,KACd,YAAY,MAAM,KAClB,UAAU,MAAM,KAChB,CAAC,OAAO,SAAS,WAAW,UAE5B;EAGF,MAAM,MAAM,MAAM,QAAQ,MAAM;EAEhC,IAAI,UAAU,QAAQ,MACpB;EAGF,MAAM,OAAO,MAAM;EACnB,MAAM,EAAE,OAAO,eAAe,OAAO;EACrC,MAAM,WAAW,WAAW;EAC5B,MAAM,IAAI,OAAO,YAAY;EAC7B,MAAM,MAAM;GAAC;GAAU;GAAU;EAAQ;EACzC,MAAM,MAAM;GAAC;GAAW;GAAW;EAAS;EAC5C,MAAM,QAAQ,OAAO,SAAS,SAAS;EACvC,MAAM,QAAQ;GAAC;GAAG;GAAG;GAAG;GAAG;GAAG;GAAG;GAAG;GAAG;EAAC;EAExC,MAAM,KAAK;GAAE,MAAM;GAAQ;GAAK;GAAK;EAAI,CAAC;EAE1C,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,OAAO,KAAK,GAAG;GACrC,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;IAC7B,MAAM,IAAI,OAAO,KAAK,IAAI,CAAC,KAAK,IAAI;IACpC,MAAM,IAAI,SAAS,KAAK,CAAC;IACzB,MAAM,IAAI,SAAS,KAAK,CAAC;IACzB,MAAM,IAAI,SAAS,KAAK,CAAC;IAEzB,KAAK,IAAI,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG;KACtC,MAAM,QAAQ,EAAE,QAAS,IAAI,EAAE,OAAO,KAAM,IAAI,EAAE,OAAO,KAAM,IAAI,EAAE,OAAO;KAE5E,MAAM,IAAI,IAAI,QAAQ;KACtB,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAQ,KAAK;KACtC,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAQ,KAAK;IACxC;GACF;GAEA,MAAM,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,MAAM;GAW7C,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,IAAI,KAAK,KAAK,KAAK,KAAK;GACxB,IAAI,KAAK,KAAK,KAAK,KAAK;GACxB,IAAI,KAAK,KAAK,KAAK,KAAK;GACxB,MAAM,SAAS,KAAK,MAAM,IAAI,IAAI,EAAE;GAGpC,IAAI,EAAE,SAAS,KAAK,OAClB;GAGF,MAAM;GACN,MAAM;GACN,MAAM;GAEN,MAAM,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK;GACnC,MAAM,KAAK,MAAM;GACjB,MAAM,MAAwC;IAC5C,KAAK,MAAM,KAAK,KAAK;IACrB,KAAK,MAAM,KAAK,KAAK;IACrB,KAAK,MAAM,KAAK,KAAK;IACrB,KAAK,MAAM,IAAI,GAAG;GACpB;GACA,MAAM,SAAS,QAAQ,IAAI,MAAM,GAAG,GAAG,CAAC;GAExC,MAAM,KAAK,IAAI;GACf,OAAO,KAAK,IAAI,IAAI,IAAI,CAAC;GACzB,QAAQ,KAAK,GAAG,KAAK;GACrB,KAAK,KAAK,GAAG;GAEb,IAAI,QACF,OAAO,KAAK,EAAE;QAEd,QAAQ,IAAI,MAAM,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC;EAEnC;CACF,CAAC;CAGD,MAAM,SAAS,MAAM;CACrB,MAAM,OAAO;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAC;CAEhD,MAAM,YAAY,GAAW,MAAc;EACzC,MAAM,IAAI,IAAI;EACd,MAAM,IAAI,IAAI;EACd,IAAI,KAAK,QAAQ,IAAI,KAAM,QAAQ;EACnC,IAAI,KAAK,QAAQ,IAAI,KAAM,QAAQ,IAAI;EACvC,IAAI,KAAK,QAAQ,IAAI,KAAM,QAAQ,IAAI;EACvC,MAAM,SAAS,KAAK,MAAM,IAAI,IAAI,EAAE,KAAK;EAEzC,MAAM;EACN,MAAM;EACN,MAAM;EAEN,MAAM,KAAK,OAAO,IAAI;EACtB,MAAM,KAAK,OAAO,IAAI,IAAI;EAC1B,MAAM,KAAK,OAAO,IAAI,IAAI;EAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;EAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;EAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;EAG1B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;GAC7B,MAAM,KAAK,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI,KAAK;GAC7C,MAAM,KAAK,QAAQ;GACnB,MAAM,KAAK,QAAQ,KAAK;GACxB,MAAM,KAAK,QAAQ,KAAK;GAExB,KAAK,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK;GACvC,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK;EAC7C;EAEA,KAAK,IAAI,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG;GACtC,MAAM,OAAO;GACb,MAAM,KAAK,OAAO,KAAK,OAAO,KAAK,IAAI,KAAM,OAAO,KAAK;GACzD,MAAM,KAAK,KAAK,KAAK,KAAM,KAAK,OAAO;GACvC,MAAM,KAAK,KAAK,KAAK,IAAI,KAAM,KAAK,OAAO,IAAI;GAC/C,MAAM,aAAa,KAAK,MAAM,IAAI,EAAE,KAAK;GACzC,MAAM,QAAQ,CAAC,KAAK;GACpB,MAAM,QAAQ,KAAK;GACnB,IAAI,KAAK;GACT,IAAI,KAAK;GACT,IAAI,KAAK;GACT,IAAI,KAAK;GAET,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;IAC7B,MAAM,QAAQ,KAAK,IAAI,KAAM,QAAQ,KAAK,IAAI,IAAI,KAAM;IAExD,IAAI,IAAI,GAAG;KACT,KAAK,KAAK,IAAI,IAAI,KAAK;KACvB,KAAK,KAAK,IAAI,IAAI,KAAK;IACzB,OAAO;KACL,KAAK,KAAK,IAAI,IAAI,KAAK;KACvB,KAAK,KAAK,IAAI,IAAI,KAAK;IACzB;GACF;GAEA,IAAI,KAAK,IAAI,IAAI,EAAE,IAAI,KAAK,IAAI,IAAI,EAAE,IAAI,QACxC,OAAO;EAEX;EAEA,OAAO;CACT;CAGA,MAAM,WAAW,GAAW,MAAc;EACxC,MAAM,KAAK,OAAO,IAAI;EACtB,MAAM,KAAK,OAAO,IAAI,IAAI;EAC1B,MAAM,KAAK,OAAO,IAAI,IAAI;EAC1B,MAAM,IAAI,OAAO,IAAI,IAAI;EAEzB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;GAC7B,MAAM,KAAK,IAAI,IAAI,IAAI;GAEvB,IAAI,KAAK,IAAI,KAAK,QAAQ,MAAO,KAAK,QAAQ,KAAK,KAAM,KAAK,QAAQ,KAAK,KAAM,CAAC,IAAI,KACpF,OAAO;EAEX;EAEA,OAAO;CACT;CAEA,MAAM,wBAAQ,IAAI,IAAyD;CAG3E,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxC,MAAM,CAAC,IAAI,IAAI,IAAI,MAAM,KAAK;EAE9B,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,KAAK,GAC5B,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,KAAK,GAC5B,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,KAAK,GAC5B,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,KAAK,GAC5B,KAAK,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG;GACxE,MAAM,IAAI,MAAM;GAChB,MAAM,IAAI,MAAM;GAEhB,IACE,KAAK,KACJ,MAAM,KAAK,CAAC,QACb,OAAO,IAAI,KAAM,OAAO,IAAI,KAC1B,OAAO,IAAI,IAAI,KAAM,OAAO,IAAI,IAAI,KACpC,OAAO,IAAI,IAAI,KAAM,OAAO,IAAI,IAAI,KACpC,YACF,CAAC,QAAQ,GAAG,CAAC,KACb,CAAC,QAAQ,GAAG,CAAC,KACb,CAAC,SAAS,GAAG,CAAC,GAEd;GAGF,MAAM,MAAM,IAAI,IAAI,GAAG,EAAE,GAAG,MAAM,GAAG,EAAE,GAAG;GAC1C,MAAM,OAAO,MAAM,IAAI,GAAG,KAAK;IAAE,GAAG,KAAK,IAAI,GAAG,CAAC;IAAG,GAAG,KAAK,IAAI,GAAG,CAAC;IAAG,WAAW;GAAE;GAEpF,KAAK,aAAa;GAClB,MAAM,IAAI,KAAK,IAAI;EACrB;CAKV;CAEA,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,CACvB,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS,CAAC,CACzC,KAAK,EAAE,GAAG,GAAG,gBACZ,WAAW;EAAE,GAAG,QAAQ,MAAM,EAAG;EAAG,GAAG,QAAQ,MAAM,EAAG;EAAG;CAAU,GAAG,UAAU,CAChF,MAAM,EAAE,CAAE,MACV,MAAM,EAAE,CAAE,IACZ,CAAU,CACZ;AACJ;;;;;;;AClSA,MAAa,cAAc,MAAgB,UAA6B,CAAC,MAAmB;CAC1F,MAAM,EAAE,MAAM,WAAW;CAEzB,OAAO;EACL,WAAW,eAAe,MAAM,EAAE,KAAK,CAAC;EACxC,WAAW,cAAc,MAAM,OAAO;EACtC,aAAa,gBAAgB,MAAM,WAAW,KAAA,IAAY,EAAE,KAAK,IAAI;GAAE;GAAM;EAAO,CAAC;EACrF,QAAQ,WAAW,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,QAAQ,OAAO,EAAE;CAC/D;AACF"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|