@zkmake/three-audit 0.2.0 → 0.2.2
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 +53 -10
- package/dist/{audit-scene-Cz1FZxhd.js → audit-scene-BYsJcco9.js} +200 -23
- package/dist/audit-scene-BYsJcco9.js.map +1 -0
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +63 -18
- package/dist/index.js +98 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/audit-scene-Cz1FZxhd.js.map +0 -1
package/README.md
CHANGED
|
@@ -27,10 +27,20 @@ test("windmill has no z-fighting and no NaN geometry", () => {
|
|
|
27
27
|
});
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
A failure lists each pair of meshes with faces in one plane, facing the same way, overlapping:
|
|
30
|
+
A failure lists each pair of meshes with faces in one plane, facing the same way, overlapping: the
|
|
31
|
+
meshes by their named ancestors, the plane they share, and where they overlap.
|
|
31
32
|
|
|
32
|
-
```
|
|
33
|
-
[
|
|
33
|
+
```ts
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
a: "windmill/plinth [stone] @ (0, 0.1, 0)",
|
|
37
|
+
b: "windmill/wall [plaster] @ (0, 0.5, 0)",
|
|
38
|
+
triangles: 4,
|
|
39
|
+
planes: ["y = 0.000, facing -y"],
|
|
40
|
+
overlap: { area: 1, centre: [0, 0, 0] },
|
|
41
|
+
count: 1,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
34
44
|
```
|
|
35
45
|
|
|
36
46
|
## From the command line
|
|
@@ -45,7 +55,8 @@ npx @zkmake/three-audit assets/*.glb # or bunx
|
|
|
45
55
|
windmill.glb 4,812 triangles
|
|
46
56
|
✗ 1 z-fighting pair
|
|
47
57
|
windmill/plinth [stone] @ (0, 0.1, 0)
|
|
48
|
-
↔ windmill/wall [plaster] @ (0, 0.5, 0)
|
|
58
|
+
↔ windmill/wall [plaster] @ (0, 0.5, 0)
|
|
59
|
+
4 triangles in y = 0.000, facing -y; overlap 1 m² at (0, 0, 0)
|
|
49
60
|
✓ no NaN geometry
|
|
50
61
|
most triangles:
|
|
51
62
|
1,920 sails
|
|
@@ -99,10 +110,19 @@ useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl])
|
|
|
99
110
|
| `audit()` | Meshes with NaN positions or normals, or zero-length normals |
|
|
100
111
|
| `zfight(gap?, self?)` | Pairs of meshes that z-fight |
|
|
101
112
|
| `report()` | All of the above in one JSON-safe object |
|
|
102
|
-
| `ledger()`
|
|
113
|
+
| `ledger(options?)` | One frame's draw calls by object and pass, printed as tables (WebGL) |
|
|
114
|
+
| `beginLedger()` | `{ end() }`: draw calls between the two, for any span you choose (WebGL) |
|
|
103
115
|
| `blackFrames(10)` | Frames in the next 10 s that came out black (WebGL) |
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
Each one is also on `threeAudit` (`threeAudit.report()`). Where short names like `report` collide
|
|
118
|
+
with other tooling, `installAuditHelpers(scene, { globals: false })` installs only `threeAudit`.
|
|
119
|
+
|
|
120
|
+
From a headless browser: `agent-browser eval 'JSON.stringify(threeAudit.report())'`.
|
|
121
|
+
|
|
122
|
+
A tab an agent drives is often in the background, where the browser pauses animation frames.
|
|
123
|
+
`ledger()` and `blackFrames()` fail after 2 s with an error that says so, instead of waiting for
|
|
124
|
+
ever. To record a background tab's draws, have the ledger record one call of your app's own
|
|
125
|
+
render: `ledger({ render: () => app.renderOnce() })`. Black frames need a visible tab.
|
|
106
126
|
|
|
107
127
|
## The checks
|
|
108
128
|
|
|
@@ -119,7 +139,19 @@ World space, so parent transforms count.
|
|
|
119
139
|
- `skip(object)`: leave an object and its subtree out, e.g. a merged copy whose sources are still in
|
|
120
140
|
the scene: `skip: (o) => o.userData.bakedResult === true`.
|
|
121
141
|
|
|
122
|
-
|
|
142
|
+
Each row names both meshes by their named ancestors (`bridge/deck/slab [concrete]`), the planes they
|
|
143
|
+
share (`z = -3.000, facing -z`; opposite-facing faces never pair), and the overlap's area in world
|
|
144
|
+
units² and its centre. Identical pairs, such as pooled copies of one prop parked at one spot, fold
|
|
145
|
+
into one row with a `count`.
|
|
146
|
+
|
|
147
|
+
Left out because they can't fight:
|
|
148
|
+
|
|
149
|
+
- hidden objects
|
|
150
|
+
- instanced and batched meshes, and `InstancedBufferGeometry`, which a shader positions. Check
|
|
151
|
+
their source parts as plain meshes.
|
|
152
|
+
- pairs where neither material writes depth, or one ignores depth entirely. A decal that doesn't
|
|
153
|
+
write depth, over a wall that does, still fights and is still listed.
|
|
154
|
+
- pairs where either material has a polygon offset, the usual decal fix
|
|
123
155
|
|
|
124
156
|
Typical fixes: stand the smaller part 5 mm off, shorten it inside the larger one, or fit panels
|
|
125
157
|
between each other instead of over each other's ends.
|
|
@@ -132,14 +164,19 @@ Either lights a pixel NaN, and bloom smears one NaN pixel into a black block.
|
|
|
132
164
|
### `countTriangles`, `listMeshes`, `geometryCensus`
|
|
133
165
|
|
|
134
166
|
Where a triangle budget goes. Name meshes and geometries (`mesh.name`, `geometry.name`) so the rows
|
|
135
|
-
say something.
|
|
167
|
+
say something. A mesh row's `name` is its named ancestors, then its own name, or its geometry's name
|
|
168
|
+
or type when it has none (`CharacterHands/left/BoxGeometry`). Draw ranges are respected; a shadow pass adds roughly as much again per light.
|
|
136
169
|
|
|
137
170
|
### `recordDrawLedger(renderer)` / `beginDrawLedger(renderer)`
|
|
138
171
|
|
|
139
172
|
Wraps `WebGLRenderer.renderBufferDirect` and counts every draw by object name, by group (the name up
|
|
140
173
|
to its first `:` or `#`), and by pass: `shadow`, then each `render()` call by scene name, or
|
|
141
|
-
`render 1`, `render 2`…
|
|
142
|
-
|
|
174
|
+
`render 1`, `render 2`… An unnamed object goes by its named ancestors, type and material
|
|
175
|
+
(`CharacterHands/Mesh [skin]`).
|
|
176
|
+
|
|
177
|
+
`recordDrawLedger` records exactly one animation frame, and fails after `timeout` (2 s) when no
|
|
178
|
+
frame comes. `{ render }` records one call of your own render instead, frames or not.
|
|
179
|
+
`beginDrawLedger` returns `{ end() }` for any span. `printDrawLedger` prints the tables.
|
|
143
180
|
|
|
144
181
|
### `watchBlackFrames(renderer)`
|
|
145
182
|
|
|
@@ -153,6 +190,12 @@ stalls the GPU, so watch, read `blackFrames`, `stop()`.
|
|
|
153
190
|
non-enumerable property (`row.mesh`, `row.meshes`), so they print and serialise without dragging the
|
|
154
191
|
scene along.
|
|
155
192
|
|
|
193
|
+
## Types
|
|
194
|
+
|
|
195
|
+
The checks take any object with three's `isObject3D` flag, and the renderer as only the methods
|
|
196
|
+
they call. An app on a newer `@types/three` than the one this package was built against passes its
|
|
197
|
+
scene and renderer without a cast.
|
|
198
|
+
|
|
156
199
|
## License
|
|
157
200
|
|
|
158
201
|
MIT
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
//#region src/scene.ts
|
|
2
|
+
/** The object as three's own type, for the walk. */
|
|
3
|
+
const asObject3D = (object) => object;
|
|
2
4
|
/** Depth-first, in child order. A skipped (or, with `visibleOnly`, hidden) object prunes its subtree. */
|
|
3
5
|
const walk = (root, options, visit) => {
|
|
4
6
|
const stack = [root];
|
|
@@ -57,6 +59,22 @@ const hasTag = (root, tagKey, skip) => {
|
|
|
57
59
|
return found;
|
|
58
60
|
};
|
|
59
61
|
const meshName = (mesh) => mesh.name || mesh.geometry.name || mesh.geometry.type;
|
|
62
|
+
/**
|
|
63
|
+
* The named ancestors above an object, outermost first, from `root` (included when named) down:
|
|
64
|
+
* at most the nearest `depth`, with `…` for the rest. An unnamed ancestor carrying `tagKey` goes
|
|
65
|
+
* by its tag.
|
|
66
|
+
*/
|
|
67
|
+
const pathOf = (object, root, tagKey, depth = 3) => {
|
|
68
|
+
const names = [];
|
|
69
|
+
for (let at = object.parent; at; at = at === root ? null : at.parent) {
|
|
70
|
+
const tag = tagKey === void 0 ? void 0 : at.userData[tagKey];
|
|
71
|
+
const name = at.name || (tag === void 0 || tag === null ? "" : String(tag));
|
|
72
|
+
if (name) names.unshift(name);
|
|
73
|
+
}
|
|
74
|
+
return names.length > depth ? ["…", ...names.slice(-depth)] : names;
|
|
75
|
+
};
|
|
76
|
+
/** `path/to/mesh [material]`: enough to find a mesh in the scene without walking it by hand. */
|
|
77
|
+
const meshLabel = (mesh, root, tagKey) => `${[...pathOf(mesh, root, tagKey), meshName(mesh)].join("/")} [${materialName(mesh)}]`;
|
|
60
78
|
const materialName = (mesh) => {
|
|
61
79
|
const material = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
|
|
62
80
|
return material ? material.name || material.type : "none";
|
|
@@ -113,7 +131,7 @@ const check = (geometry) => {
|
|
|
113
131
|
const findBadGeometry = (root, options = {}) => {
|
|
114
132
|
const tagKey = options.tagKey ?? "studioObject";
|
|
115
133
|
const rows = [];
|
|
116
|
-
walk(root, { skip: options.skip }, (object) => {
|
|
134
|
+
walk(asObject3D(root), { skip: options.skip }, (object) => {
|
|
117
135
|
if (!isMesh(object) || !object.geometry.attributes.position) return;
|
|
118
136
|
const { nonFinite, zeroNormals } = check(object.geometry);
|
|
119
137
|
if (nonFinite > 0 || zeroNormals > 0) {
|
|
@@ -144,7 +162,7 @@ const cost = (mesh) => {
|
|
|
144
162
|
*/
|
|
145
163
|
const countTriangles = (root, options = {}) => {
|
|
146
164
|
let total = 0;
|
|
147
|
-
walk(root, {
|
|
165
|
+
walk(asObject3D(root), {
|
|
148
166
|
skip: options.skip,
|
|
149
167
|
visibleOnly: true
|
|
150
168
|
}, (object) => {
|
|
@@ -153,7 +171,8 @@ const countTriangles = (root, options = {}) => {
|
|
|
153
171
|
return total;
|
|
154
172
|
};
|
|
155
173
|
/** Every visible mesh, most triangles drawn first. */
|
|
156
|
-
const listMeshes = (
|
|
174
|
+
const listMeshes = (scene, options = {}) => {
|
|
175
|
+
const root = asObject3D(scene);
|
|
157
176
|
const rows = [];
|
|
158
177
|
walk(root, {
|
|
159
178
|
skip: options.skip,
|
|
@@ -162,7 +181,8 @@ const listMeshes = (root, options = {}) => {
|
|
|
162
181
|
if (!isMesh(object)) return;
|
|
163
182
|
const { instances, triangles } = cost(object);
|
|
164
183
|
const row = {
|
|
165
|
-
name: meshName(object),
|
|
184
|
+
name: [...pathOf(object, root), meshName(object)].join("/"),
|
|
185
|
+
material: materialName(object),
|
|
166
186
|
triangles: instances > 0 ? Math.round(triangles / instances) : 0,
|
|
167
187
|
instances,
|
|
168
188
|
total: triangles
|
|
@@ -178,7 +198,7 @@ const listMeshes = (root, options = {}) => {
|
|
|
178
198
|
*/
|
|
179
199
|
const geometryCensus = (root, options = {}) => {
|
|
180
200
|
const rows = /* @__PURE__ */ new Map();
|
|
181
|
-
walk(root, { skip: options.skip }, (object) => {
|
|
201
|
+
walk(asObject3D(root), { skip: options.skip }, (object) => {
|
|
182
202
|
if (!isMesh(object) || isBatched(object) || !object.geometry.attributes.position) return;
|
|
183
203
|
const geometry = object.geometry;
|
|
184
204
|
const key = geometry.name || `${geometry.type}:${geometry.uuid.slice(0, 6)}`;
|
|
@@ -202,13 +222,31 @@ const geometryCensus = (root, options = {}) => {
|
|
|
202
222
|
const QUANT = 40;
|
|
203
223
|
/** Normals this aligned count as parallel (about 2.6°). */
|
|
204
224
|
const PARALLEL = .999;
|
|
205
|
-
const round = (value
|
|
225
|
+
const round = (value, places = 2) => {
|
|
226
|
+
const scale = 10 ** places;
|
|
227
|
+
const rounded = Math.round(value * scale) / scale;
|
|
228
|
+
return rounded === 0 ? 0 : rounded;
|
|
229
|
+
};
|
|
206
230
|
const labelOf = (part) => {
|
|
207
231
|
const centre = part.min.map((low, axis) => round((low + part.max[axis]) / 2));
|
|
208
|
-
return `${part.
|
|
232
|
+
return `${part.label} @ (${centre.join(", ")})`;
|
|
209
233
|
};
|
|
234
|
+
const fixed = (value) => round(value, 3).toFixed(3);
|
|
235
|
+
/** `z = -3.000, facing +z` for an axis-aligned plane, `(nx, ny, nz) · p = d` for any other. */
|
|
236
|
+
const planeLabel = (nx, ny, nz, d) => {
|
|
237
|
+
for (const [axis, value] of [
|
|
238
|
+
["x", nx],
|
|
239
|
+
["y", ny],
|
|
240
|
+
["z", nz]
|
|
241
|
+
]) if (Math.abs(value) > .9999) return `${axis} = ${fixed(d / value)}, facing ${value > 0 ? "+" : "-"}${axis}`;
|
|
242
|
+
return `(${fixed(nx)}, ${fixed(ny)}, ${fixed(nz)}) · p = ${fixed(d)}`;
|
|
243
|
+
};
|
|
244
|
+
const materialsOf = (mesh) => (Array.isArray(mesh.material) ? mesh.material : [mesh.material]).filter((material) => material !== void 0 && material !== null);
|
|
245
|
+
/** Whether two parts can fight: one writes depth the other tests, and neither is offset. */
|
|
246
|
+
const canFight = (a, b) => !a.offset && !b.offset && (a.writes && b.tests || b.writes && a.tests);
|
|
210
247
|
/** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */
|
|
211
|
-
const findZFighting = (
|
|
248
|
+
const findZFighting = (scene, options = {}) => {
|
|
249
|
+
const root = asObject3D(scene);
|
|
212
250
|
const gap = options.gap ?? .004;
|
|
213
251
|
const self = options.self ?? false;
|
|
214
252
|
const tagKey = options.tagKey ?? "studioObject";
|
|
@@ -221,10 +259,14 @@ const findZFighting = (root, options = {}) => {
|
|
|
221
259
|
const keys = [];
|
|
222
260
|
const buckets = /* @__PURE__ */ new Map();
|
|
223
261
|
const keyOf = (x, y, z, d) => `${x},${y},${z},${d}`;
|
|
224
|
-
walk(root, {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
262
|
+
walk(root, {
|
|
263
|
+
skip: options.skip,
|
|
264
|
+
visibleOnly: true
|
|
265
|
+
}, (object) => {
|
|
266
|
+
if (!isMesh(object) || isInstanced(object) || isBatched(object) || object.geometry.isInstancedBufferGeometry === true || !object.geometry.attributes.position) return;
|
|
267
|
+
if (tagged && tagOf(object, tagKey) === null) return;
|
|
268
|
+
const materials = materialsOf(object).filter((material) => material.visible !== false);
|
|
269
|
+
if (materials.length === 0) return;
|
|
228
270
|
const part = parts.length;
|
|
229
271
|
const { index, attributes } = object.geometry;
|
|
230
272
|
const position = attributes.position;
|
|
@@ -253,9 +295,12 @@ const findZFighting = (root, options = {}) => {
|
|
|
253
295
|
];
|
|
254
296
|
parts.push({
|
|
255
297
|
mesh: object,
|
|
256
|
-
|
|
298
|
+
label: meshLabel(object, root, tagKey),
|
|
257
299
|
min,
|
|
258
|
-
max
|
|
300
|
+
max,
|
|
301
|
+
writes: materials.some((material) => material.depthWrite !== false),
|
|
302
|
+
tests: materials.some((material) => material.depthTest !== false),
|
|
303
|
+
offset: materials.some((material) => material.polygonOffset === true && (material.polygonOffsetFactor !== 0 || material.polygonOffsetUnits !== 0))
|
|
259
304
|
});
|
|
260
305
|
for (let k = 0; k + 2 < count; k += 3) {
|
|
261
306
|
for (let c = 0; c < 3; c += 1) {
|
|
@@ -317,6 +362,14 @@ const findZFighting = (root, options = {}) => {
|
|
|
317
362
|
0,
|
|
318
363
|
0
|
|
319
364
|
];
|
|
365
|
+
const frame = [
|
|
366
|
+
0,
|
|
367
|
+
0,
|
|
368
|
+
0,
|
|
369
|
+
0,
|
|
370
|
+
0,
|
|
371
|
+
0
|
|
372
|
+
];
|
|
320
373
|
const overlaps = (s, t) => {
|
|
321
374
|
const S = s * 9;
|
|
322
375
|
const T = t * 9;
|
|
@@ -333,6 +386,12 @@ const findZFighting = (root, options = {}) => {
|
|
|
333
386
|
const vx = ny * uz - nz * uy;
|
|
334
387
|
const vy = nz * ux - nx * uz;
|
|
335
388
|
const vz = nx * uy - ny * ux;
|
|
389
|
+
frame[0] = ux;
|
|
390
|
+
frame[1] = uy;
|
|
391
|
+
frame[2] = uz;
|
|
392
|
+
frame[3] = vx;
|
|
393
|
+
frame[4] = vy;
|
|
394
|
+
frame[5] = vz;
|
|
336
395
|
for (let i = 0; i < 6; i += 1) {
|
|
337
396
|
const at = i < 3 ? S + i * 3 : T + (i - 3) * 3;
|
|
338
397
|
const px = corners[at];
|
|
@@ -367,28 +426,146 @@ const findZFighting = (root, options = {}) => {
|
|
|
367
426
|
}
|
|
368
427
|
return true;
|
|
369
428
|
};
|
|
429
|
+
/**
|
|
430
|
+
* The overlap `overlaps` just found, as area and centroid in s's plane: t's triangle clipped to
|
|
431
|
+
* s's (Sutherland–Hodgman), then the shoelace formula.
|
|
432
|
+
*/
|
|
433
|
+
const clipped = () => {
|
|
434
|
+
const order = (flat[2] - flat[0]) * (flat[5] - flat[1]) - (flat[4] - flat[0]) * (flat[3] - flat[1]) >= 0 ? [
|
|
435
|
+
0,
|
|
436
|
+
1,
|
|
437
|
+
2
|
|
438
|
+
] : [
|
|
439
|
+
0,
|
|
440
|
+
2,
|
|
441
|
+
1
|
|
442
|
+
];
|
|
443
|
+
let polygon = flat.slice(6, 12);
|
|
444
|
+
for (let edge = 0; edge < 3 && polygon.length >= 6; edge += 1) {
|
|
445
|
+
const ax = flat[order[edge] * 2];
|
|
446
|
+
const ay = flat[order[edge] * 2 + 1];
|
|
447
|
+
const ex = flat[order[(edge + 1) % 3] * 2] - ax;
|
|
448
|
+
const ey = flat[order[(edge + 1) % 3] * 2 + 1] - ay;
|
|
449
|
+
const next = [];
|
|
450
|
+
const n = polygon.length / 2;
|
|
451
|
+
for (let i = 0; i < n; i += 1) {
|
|
452
|
+
const px = polygon[i * 2];
|
|
453
|
+
const py = polygon[i * 2 + 1];
|
|
454
|
+
const qx = polygon[(i + 1) % n * 2];
|
|
455
|
+
const qy = polygon[(i + 1) % n * 2 + 1];
|
|
456
|
+
const dp = ex * (py - ay) - ey * (px - ax);
|
|
457
|
+
const dq = ex * (qy - ay) - ey * (qx - ax);
|
|
458
|
+
if (dp >= 0) next.push(px, py);
|
|
459
|
+
if (dp >= 0 !== dq >= 0) {
|
|
460
|
+
const k = dp / (dp - dq);
|
|
461
|
+
next.push(px + (qx - px) * k, py + (qy - py) * k);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
polygon = next;
|
|
465
|
+
}
|
|
466
|
+
let twice = 0;
|
|
467
|
+
let cx = 0;
|
|
468
|
+
let cy = 0;
|
|
469
|
+
const n = polygon.length / 2;
|
|
470
|
+
for (let i = 0; i < n; i += 1) {
|
|
471
|
+
const x0 = polygon[i * 2];
|
|
472
|
+
const y0 = polygon[i * 2 + 1];
|
|
473
|
+
const x1 = polygon[(i + 1) % n * 2];
|
|
474
|
+
const y1 = polygon[(i + 1) % n * 2 + 1];
|
|
475
|
+
const cross = x0 * y1 - x1 * y0;
|
|
476
|
+
twice += cross;
|
|
477
|
+
cx += (x0 + x1) * cross;
|
|
478
|
+
cy += (y0 + y1) * cross;
|
|
479
|
+
}
|
|
480
|
+
return Math.abs(twice) < 1e-12 ? {
|
|
481
|
+
area: 0,
|
|
482
|
+
x: 0,
|
|
483
|
+
y: 0
|
|
484
|
+
} : {
|
|
485
|
+
area: Math.abs(twice) / 2,
|
|
486
|
+
x: cx / (3 * twice),
|
|
487
|
+
y: cy / (3 * twice)
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
const onPlane = (s, t) => {
|
|
491
|
+
const nx = planes[s * 4];
|
|
492
|
+
const ny = planes[s * 4 + 1];
|
|
493
|
+
const nz = planes[s * 4 + 2];
|
|
494
|
+
const d = planes[s * 4 + 3];
|
|
495
|
+
for (let c = 0; c < 3; c += 1) {
|
|
496
|
+
const at = t * 9 + c * 3;
|
|
497
|
+
if (Math.abs(nx * corners[at] + ny * corners[at + 1] + nz * corners[at + 2] - d) > gap) return false;
|
|
498
|
+
}
|
|
499
|
+
return true;
|
|
500
|
+
};
|
|
370
501
|
const pairs = /* @__PURE__ */ new Map();
|
|
371
502
|
for (let s = 0; s < owner.length; s += 1) {
|
|
372
503
|
const [kx, ky, kz, kd] = keys[s];
|
|
373
504
|
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)) ?? []) {
|
|
374
505
|
const a = owner[s];
|
|
375
506
|
const b = owner[t];
|
|
376
|
-
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 ||
|
|
507
|
+
if (t <= s || a === b && !self || !canFight(parts[a], parts[b]) || 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;
|
|
377
508
|
const key = a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
378
509
|
const pair = pairs.get(key) ?? {
|
|
379
510
|
a: Math.min(a, b),
|
|
380
511
|
b: Math.max(a, b),
|
|
381
|
-
triangles: 0
|
|
512
|
+
triangles: 0,
|
|
513
|
+
area: 0,
|
|
514
|
+
centre: [
|
|
515
|
+
0,
|
|
516
|
+
0,
|
|
517
|
+
0
|
|
518
|
+
],
|
|
519
|
+
planes: /* @__PURE__ */ new Map()
|
|
382
520
|
};
|
|
521
|
+
const nx = planes[s * 4];
|
|
522
|
+
const ny = planes[s * 4 + 1];
|
|
523
|
+
const nz = planes[s * 4 + 2];
|
|
524
|
+
const d = planes[s * 4 + 3];
|
|
525
|
+
const { area, x, y } = clipped();
|
|
526
|
+
const plane = planeLabel(nx, ny, nz, d);
|
|
383
527
|
pair.triangles += 1;
|
|
528
|
+
pair.area += area;
|
|
529
|
+
pair.centre[0] += area * (x * frame[0] + y * frame[3] + d * nx);
|
|
530
|
+
pair.centre[1] += area * (x * frame[1] + y * frame[4] + d * ny);
|
|
531
|
+
pair.centre[2] += area * (x * frame[2] + y * frame[5] + d * nz);
|
|
532
|
+
pair.planes.set(plane, (pair.planes.get(plane) ?? 0) + area);
|
|
384
533
|
pairs.set(key, pair);
|
|
385
534
|
}
|
|
386
535
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
536
|
+
const rows = /* @__PURE__ */ new Map();
|
|
537
|
+
for (const pair of [...pairs.values()].sort((x, y) => y.triangles - x.triangles)) {
|
|
538
|
+
const meshes = [parts[pair.a].mesh, parts[pair.b].mesh];
|
|
539
|
+
const a = labelOf(parts[pair.a]);
|
|
540
|
+
const b = labelOf(parts[pair.b]);
|
|
541
|
+
const planeLabels = [...pair.planes.entries()].sort((x, y) => y[1] - x[1]).map(([plane]) => plane);
|
|
542
|
+
const key = JSON.stringify([
|
|
543
|
+
a,
|
|
544
|
+
b,
|
|
545
|
+
pair.triangles,
|
|
546
|
+
planeLabels
|
|
547
|
+
]);
|
|
548
|
+
const folded = rows.get(key);
|
|
549
|
+
if (folded) {
|
|
550
|
+
folded.count += 1;
|
|
551
|
+
folded.pairs.push(meshes);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const weight = pair.area > 0 ? pair.area : 1;
|
|
555
|
+
const row = withHidden(withHidden({
|
|
556
|
+
a,
|
|
557
|
+
b,
|
|
558
|
+
triangles: pair.triangles,
|
|
559
|
+
planes: planeLabels,
|
|
560
|
+
overlap: {
|
|
561
|
+
area: round(pair.area, 4),
|
|
562
|
+
centre: pair.centre.map((sum) => round(sum / weight))
|
|
563
|
+
},
|
|
564
|
+
count: 1
|
|
565
|
+
}, "meshes", meshes), "pairs", [meshes]);
|
|
566
|
+
rows.set(key, row);
|
|
567
|
+
}
|
|
568
|
+
return [...rows.values()];
|
|
392
569
|
};
|
|
393
570
|
//#endregion
|
|
394
571
|
//#region src/audit-scene.ts
|
|
@@ -409,6 +586,6 @@ const auditScene = (root, options = {}) => {
|
|
|
409
586
|
};
|
|
410
587
|
};
|
|
411
588
|
//#endregion
|
|
412
|
-
export { listMeshes as a, geometryCensus as i, findZFighting as n, findBadGeometry as o, countTriangles as r,
|
|
589
|
+
export { listMeshes as a, triangleCount as c, geometryCensus as i, findZFighting as n, findBadGeometry as o, countTriangles as r, pathOf as s, auditScene as t };
|
|
413
590
|
|
|
414
|
-
//# sourceMappingURL=audit-scene-
|
|
591
|
+
//# sourceMappingURL=audit-scene-BYsJcco9.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-scene-BYsJcco9.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/**\n * Any three.js object, from whichever copy of three's types the app has. The checks take this, so\n * an app on a newer `@types/three` than this package was built against passes its scene as is.\n */\nexport type AnyObject3D = { readonly isObject3D: true; readonly children: readonly object[] };\n\n/** The object as three's own type, for the walk. */\nexport const asObject3D = (object: AnyObject3D) => object as unknown as Object3D;\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\n/**\n * The named ancestors above an object, outermost first, from `root` (included when named) down:\n * at most the nearest `depth`, with `…` for the rest. An unnamed ancestor carrying `tagKey` goes\n * by its tag.\n */\nexport const pathOf = (object: Object3D, root: Object3D | null, tagKey?: string, depth = 3) => {\n const names: string[] = [];\n\n for (let at = object.parent; at; at = at === root ? null : at.parent) {\n const tag: unknown = tagKey === undefined ? undefined : at.userData[tagKey];\n const name = at.name || (tag === undefined || tag === null ? \"\" : String(tag));\n\n if (name) {\n names.unshift(name);\n }\n }\n\n return names.length > depth ? [\"…\", ...names.slice(-depth)] : names;\n};\n\n/** `path/to/mesh [material]`: enough to find a mesh in the scene without walking it by hand. */\nexport const meshLabel = (mesh: Mesh, root: Object3D | null, tagKey?: string) =>\n `${[...pathOf(mesh, root, tagKey), meshName(mesh)].join(\"/\")} [${materialName(mesh)}]`;\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 } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n isMesh,\n meshName,\n type Skip,\n tagOf,\n walk,\n withHidden,\n} 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: AnyObject3D,\n options: BadGeometryOptions = {},\n): BadGeometryRow[] => {\n const tagKey = options.tagKey ?? \"studioObject\";\n const rows: BadGeometryRow[] = [];\n\n walk(asObject3D(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 } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n batchedTriangles,\n isBatched,\n isInstanced,\n isMesh,\n materialName,\n meshName,\n pathOf,\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 /** `path/to/mesh`: its named ancestors, then its name (or its geometry's name or type). */\n name: string;\n material: 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: AnyObject3D, options: TriangleOptions = {}) => {\n let total = 0;\n\n walk(asObject3D(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 = (scene: AnyObject3D, options: TriangleOptions = {}): MeshRow[] => {\n const root = asObject3D(scene);\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: [...pathOf(object, root), meshName(object)].join(\"/\"),\n material: materialName(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: AnyObject3D, options: TriangleOptions = {}): GeometryRow[] => {\n const rows = new Map<string, GeometryRow>();\n\n walk(asObject3D(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, then clipped for its area.\n *\n * Left out, as they can't fight: hidden objects, instanced and batched meshes (and geometry a\n * shader positions, `InstancedBufferGeometry`), pairs where neither side writes depth or one\n * doesn't test it, and pairs where either material has a polygon offset (the usual decal fix).\n */\nimport type { Material, Mesh } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n hasTag,\n isBatched,\n isInstanced,\n isMesh,\n meshLabel,\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 /** `path/to/mesh [material] @ (x, y, z)`: named ancestors, and the centre in world space. */\n a: string;\n b: string;\n /** Overlapping triangle pairs between the two. */\n triangles: number;\n /**\n * The planes they share, most overlap first: `z = -3.000, facing +z` when axis-aligned, else\n * `(nx, ny, nz) · p = d`. Both faces point along the normal: opposite-facing faces never pair.\n */\n planes: string[];\n /** Where they overlap: the area in world units², and its centre in world space. */\n overlap: { area: number; centre: [number, number, number] };\n /** Identical pairs (same labels, triangles and planes) folded into this row. */\n count: number;\n /** Not enumerable: kept out of JSON and `console.table`. The first pair folded in. */\n readonly meshes: readonly [Mesh, Mesh];\n /** Not enumerable: every pair folded in. */\n readonly pairs: readonly (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 = {\n mesh: Mesh;\n label: string;\n min: number[];\n max: number[];\n /** Some material writes depth / tests it / offsets it. */\n writes: boolean;\n tests: boolean;\n offset: boolean;\n};\n\nconst round = (value: number, places = 2) => {\n const scale = 10 ** places;\n const rounded = Math.round(value * scale) / scale;\n\n return rounded === 0 ? 0 : rounded;\n};\n\nconst labelOf = (part: Part) => {\n const centre = part.min.map((low, axis) => round((low + part.max[axis]!) / 2));\n\n return `${part.label} @ (${centre.join(\", \")})`;\n};\n\nconst fixed = (value: number) => round(value, 3).toFixed(3);\n\n/** `z = -3.000, facing +z` for an axis-aligned plane, `(nx, ny, nz) · p = d` for any other. */\nconst planeLabel = (nx: number, ny: number, nz: number, d: number) => {\n for (const [axis, value] of [\n [\"x\", nx],\n [\"y\", ny],\n [\"z\", nz],\n ] as const) {\n if (Math.abs(value) > 0.9999) {\n return `${axis} = ${fixed(d / value)}, facing ${value > 0 ? \"+\" : \"-\"}${axis}`;\n }\n }\n\n return `(${fixed(nx)}, ${fixed(ny)}, ${fixed(nz)}) · p = ${fixed(d)}`;\n};\n\nconst materialsOf = (mesh: Mesh) =>\n (Array.isArray(mesh.material) ? mesh.material : [mesh.material]).filter(\n (material): material is Material => material !== undefined && material !== null,\n );\n\n/** Whether two parts can fight: one writes depth the other tests, and neither is offset. */\nconst canFight = (a: Part, b: Part) =>\n !a.offset && !b.offset && ((a.writes && b.tests) || (b.writes && a.tests));\n\n/** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */\nexport const findZFighting = (\n scene: AnyObject3D,\n options: ZFightingOptions = {},\n): ZFightingRow[] => {\n const root = asObject3D(scene);\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, visibleOnly: true }, (object) => {\n if (\n !isMesh(object) ||\n isInstanced(object) ||\n isBatched(object) ||\n (object.geometry as { isInstancedBufferGeometry?: boolean }).isInstancedBufferGeometry ===\n true ||\n !object.geometry.attributes.position\n ) {\n return;\n }\n\n if (tagged && tagOf(object, tagKey) === null) {\n return;\n }\n\n const materials = materialsOf(object).filter((material) => material.visible !== false);\n\n if (materials.length === 0) {\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({\n mesh: object,\n label: meshLabel(object, root, tagKey),\n min,\n max,\n writes: materials.some((material) => material.depthWrite !== false),\n tests: materials.some((material) => material.depthTest !== false),\n offset: materials.some(\n (material) =>\n material.polygonOffset === true &&\n (material.polygonOffsetFactor !== 0 || material.polygonOffsetUnits !== 0),\n ),\n });\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 // s's plane axes from the last `overlaps`: u, then v.\n const frame = [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 frame[0] = ux;\n frame[1] = uy;\n frame[2] = uz;\n frame[3] = vx;\n frame[4] = vy;\n frame[5] = vz;\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 /**\n * The overlap `overlaps` just found, as area and centroid in s's plane: t's triangle clipped to\n * s's (Sutherland–Hodgman), then the shoelace formula.\n */\n const clipped = () => {\n const turn =\n (flat[2]! - flat[0]!) * (flat[5]! - flat[1]!) - (flat[4]! - flat[0]!) * (flat[3]! - flat[1]!);\n // s's corners counter-clockwise.\n const order = turn >= 0 ? [0, 1, 2] : [0, 2, 1];\n let polygon = flat.slice(6, 12);\n\n for (let edge = 0; edge < 3 && polygon.length >= 6; edge += 1) {\n const ax = flat[order[edge]! * 2]!;\n const ay = flat[order[edge]! * 2 + 1]!;\n const ex = flat[order[(edge + 1) % 3]! * 2]! - ax;\n const ey = flat[order[(edge + 1) % 3]! * 2 + 1]! - ay;\n const next: number[] = [];\n const n = polygon.length / 2;\n\n for (let i = 0; i < n; i += 1) {\n const px = polygon[i * 2]!;\n const py = polygon[i * 2 + 1]!;\n const qx = polygon[((i + 1) % n) * 2]!;\n const qy = polygon[((i + 1) % n) * 2 + 1]!;\n const dp = ex * (py - ay) - ey * (px - ax);\n const dq = ex * (qy - ay) - ey * (qx - ax);\n\n if (dp >= 0) {\n next.push(px, py);\n }\n\n if (dp >= 0 !== dq >= 0) {\n const k = dp / (dp - dq);\n\n next.push(px + (qx - px) * k, py + (qy - py) * k);\n }\n }\n\n polygon = next;\n }\n\n let twice = 0;\n let cx = 0;\n let cy = 0;\n const n = polygon.length / 2;\n\n for (let i = 0; i < n; i += 1) {\n const x0 = polygon[i * 2]!;\n const y0 = polygon[i * 2 + 1]!;\n const x1 = polygon[((i + 1) % n) * 2]!;\n const y1 = polygon[((i + 1) % n) * 2 + 1]!;\n const cross = x0 * y1 - x1 * y0;\n\n twice += cross;\n cx += (x0 + x1) * cross;\n cy += (y0 + y1) * cross;\n }\n\n return Math.abs(twice) < 1e-12\n ? { area: 0, x: 0, y: 0 }\n : { area: Math.abs(twice) / 2, x: cx / (3 * twice), y: cy / (3 * twice) };\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 type Pair = {\n a: number;\n b: number;\n triangles: number;\n area: number;\n /** Area-weighted sums of the overlap's centre. */\n centre: [number, number, number];\n planes: Map<string, number>;\n };\n\n const pairs = new Map<string, Pair>();\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 !canFight(parts[a]!, parts[b]!) ||\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) ?? {\n a: Math.min(a, b),\n b: Math.max(a, b),\n triangles: 0,\n area: 0,\n centre: [0, 0, 0],\n planes: new Map<string, number>(),\n };\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 const { area, x, y } = clipped();\n const plane = planeLabel(nx, ny, nz, d);\n\n pair.triangles += 1;\n pair.area += area;\n pair.centre[0] += area * (x * frame[0]! + y * frame[3]! + d * nx);\n pair.centre[1] += area * (x * frame[1]! + y * frame[4]! + d * ny);\n pair.centre[2] += area * (x * frame[2]! + y * frame[5]! + d * nz);\n pair.planes.set(plane, (pair.planes.get(plane) ?? 0) + area);\n pairs.set(key, pair);\n }\n }\n }\n }\n }\n }\n\n // Fold identical pairs (pooled copies of one prop, all at one spot) into one row each.\n const rows = new Map<string, ZFightingRow & { pairs: (readonly [Mesh, Mesh])[] }>();\n\n for (const pair of [...pairs.values()].sort((x, y) => y.triangles - x.triangles)) {\n const meshes = [parts[pair.a]!.mesh, parts[pair.b]!.mesh] as const;\n const a = labelOf(parts[pair.a]!);\n const b = labelOf(parts[pair.b]!);\n const planeLabels = [...pair.planes.entries()]\n .sort((x, y) => y[1] - x[1])\n .map(([plane]) => plane);\n const key = JSON.stringify([a, b, pair.triangles, planeLabels]);\n const folded = rows.get(key);\n\n if (folded) {\n folded.count += 1;\n folded.pairs.push(meshes);\n continue;\n }\n\n const weight = pair.area > 0 ? pair.area : 1;\n const row = withHidden(\n withHidden(\n {\n a,\n b,\n triangles: pair.triangles,\n planes: planeLabels,\n overlap: {\n area: round(pair.area, 4),\n centre: pair.centre.map((sum) => round(sum / weight)) as [number, number, number],\n },\n count: 1,\n },\n \"meshes\",\n meshes,\n ),\n \"pairs\",\n [meshes],\n );\n\n rows.set(key, row);\n }\n\n return [...rows.values()];\n};\n","import { type BadGeometryRow, findBadGeometry } from \"./bad-geometry.ts\";\nimport type { AnyObject3D, 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: AnyObject3D, 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":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as auditScene } from "./audit-scene-
|
|
2
|
+
import { t as auditScene } from "./audit-scene-BYsJcco9.js";
|
|
3
3
|
import { t as loadModel } from "./load-nqfCU3-k.js";
|
|
4
4
|
import { basename } from "node:path";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
@@ -40,7 +40,11 @@ const printReport = (file, report, top) => {
|
|
|
40
40
|
if (report.zFighting.length === 0) lines.push(" ✓ no z-fighting");
|
|
41
41
|
else {
|
|
42
42
|
lines.push(` ✗ ${report.zFighting.length} z-fighting pair${report.zFighting.length === 1 ? "" : "s"}`);
|
|
43
|
-
for (const row of report.zFighting)
|
|
43
|
+
for (const row of report.zFighting) {
|
|
44
|
+
const more = row.planes.length > 1 ? ` (+${row.planes.length - 1} more)` : "";
|
|
45
|
+
const copies = row.count > 1 ? `, ×${row.count} identical pairs` : "";
|
|
46
|
+
lines.push(` ${row.a}`, ` ↔ ${row.b}`, ` ${row.triangles} triangles in ${row.planes[0]}${more}; overlap ${row.overlap.area} m² at (${row.overlap.centre.join(", ")})${copies}`);
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
49
|
if (report.badGeometry.length === 0) lines.push(" ✓ no NaN geometry");
|
|
46
50
|
else {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/main.ts","../src/cli.ts"],"sourcesContent":["/**\n * `three-audit model.glb [more.gltf …]`: load each model in Node and check it for z-fighting and\n * NaN geometry. Exits 1 when a check fails, so it can gate CI; `--json` for machines.\n */\n/* oxlint-disable no-console -- a CLI's output is the console */\nimport { readFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport { type AuditReport, auditScene } from \"../audit-scene.ts\";\nimport { loadModel } from \"../node/load.ts\";\n\nconst HELP = `Usage: three-audit <model.glb|model.gltf>… [options]\n\nChecks glTF models for z-fighting (faces in one plane, facing the same way, overlapping) and\ngeometry that renders NaN (non-finite positions or normals, zero-length normals). Exits 1 when\nany model fails a check, 2 when a model can't be read.\n\nOptions:\n --gap <m> How close two planes count as one, in metres (default 0.004)\n --self Also check for z-fighting within each mesh (merged geometry)\n --skip <regex> Leave out objects whose name matches, and everything under them (decals)\n --top <n> List the n costliest meshes (default 5, 0 for none)\n --json Print the reports as JSON\n --no-fail Exit 0 even when a check fails\n -h, --help Show this help\n -v, --version Show the version\n\nDraco and Meshopt models need draco3dgltf / meshoptimizer installed alongside.`;\n\n/** This package's version: its package.json is the nearest one up from here (`src/cli` or `dist`). */\nconst version = () => {\n for (let up = \"../\"; up.length <= 12; up += \"../\") {\n try {\n const pkg = JSON.parse(\n readFileSync(new URL(`${up}package.json`, import.meta.url), \"utf8\"),\n ) as {\n name?: string;\n version: string;\n };\n\n if (pkg.name === \"@zkmake/three-audit\") {\n return pkg.version;\n }\n } catch {\n // Not here; one more up.\n }\n }\n\n return \"unknown\";\n};\n\nconst count = (value: number) => value.toLocaleString(\"en-US\");\n\nconst printReport = (file: string, report: AuditReport, top: number) => {\n const lines = [`${basename(file)} ${count(report.triangles)} triangles`];\n\n if (report.zFighting.length === 0) {\n lines.push(\" ✓ no z-fighting\");\n } else {\n lines.push(\n ` ✗ ${report.zFighting.length} z-fighting pair${report.zFighting.length === 1 ? \"\" : \"s\"}`,\n );\n\n for (const row of report.zFighting) {\n lines.push(` ${row.a}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli/main.ts","../src/cli.ts"],"sourcesContent":["/**\n * `three-audit model.glb [more.gltf …]`: load each model in Node and check it for z-fighting and\n * NaN geometry. Exits 1 when a check fails, so it can gate CI; `--json` for machines.\n */\n/* oxlint-disable no-console -- a CLI's output is the console */\nimport { readFileSync } from \"node:fs\";\nimport { basename } from \"node:path\";\nimport { parseArgs } from \"node:util\";\n\nimport { type AuditReport, auditScene } from \"../audit-scene.ts\";\nimport { loadModel } from \"../node/load.ts\";\n\nconst HELP = `Usage: three-audit <model.glb|model.gltf>… [options]\n\nChecks glTF models for z-fighting (faces in one plane, facing the same way, overlapping) and\ngeometry that renders NaN (non-finite positions or normals, zero-length normals). Exits 1 when\nany model fails a check, 2 when a model can't be read.\n\nOptions:\n --gap <m> How close two planes count as one, in metres (default 0.004)\n --self Also check for z-fighting within each mesh (merged geometry)\n --skip <regex> Leave out objects whose name matches, and everything under them (decals)\n --top <n> List the n costliest meshes (default 5, 0 for none)\n --json Print the reports as JSON\n --no-fail Exit 0 even when a check fails\n -h, --help Show this help\n -v, --version Show the version\n\nDraco and Meshopt models need draco3dgltf / meshoptimizer installed alongside.`;\n\n/** This package's version: its package.json is the nearest one up from here (`src/cli` or `dist`). */\nconst version = () => {\n for (let up = \"../\"; up.length <= 12; up += \"../\") {\n try {\n const pkg = JSON.parse(\n readFileSync(new URL(`${up}package.json`, import.meta.url), \"utf8\"),\n ) as {\n name?: string;\n version: string;\n };\n\n if (pkg.name === \"@zkmake/three-audit\") {\n return pkg.version;\n }\n } catch {\n // Not here; one more up.\n }\n }\n\n return \"unknown\";\n};\n\nconst count = (value: number) => value.toLocaleString(\"en-US\");\n\nconst printReport = (file: string, report: AuditReport, top: number) => {\n const lines = [`${basename(file)} ${count(report.triangles)} triangles`];\n\n if (report.zFighting.length === 0) {\n lines.push(\" ✓ no z-fighting\");\n } else {\n lines.push(\n ` ✗ ${report.zFighting.length} z-fighting pair${report.zFighting.length === 1 ? \"\" : \"s\"}`,\n );\n\n for (const row of report.zFighting) {\n const more = row.planes.length > 1 ? ` (+${row.planes.length - 1} more)` : \"\";\n const copies = row.count > 1 ? `, ×${row.count} identical pairs` : \"\";\n\n lines.push(\n ` ${row.a}`,\n ` ↔ ${row.b}`,\n ` ${row.triangles} triangles in ${row.planes[0]}${more}; overlap ${row.overlap.area} m² at (${row.overlap.centre.join(\", \")})${copies}`,\n );\n }\n }\n\n if (report.badGeometry.length === 0) {\n lines.push(\" ✓ no NaN geometry\");\n } else {\n lines.push(\n ` ✗ ${report.badGeometry.length} mesh${report.badGeometry.length === 1 ? \"\" : \"es\"} with NaN geometry`,\n );\n\n for (const row of report.badGeometry) {\n lines.push(\n ` ${row.mesh}${row.under ? ` (${row.under})` : \"\"}: ${row.nonFinite} non-finite, ${row.zeroNormals} zero normals`,\n );\n }\n }\n\n if (top > 0 && report.meshes.length > 0) {\n lines.push(\" most triangles:\");\n\n for (const row of report.meshes) {\n lines.push(\n ` ${count(row.total).padStart(9)} ${row.name}${row.instances > 1 ? ` ×${row.instances}` : \"\"}`,\n );\n }\n }\n\n return lines.join(\"\\n\");\n};\n\nexport const main = async (argv: string[]): Promise<number> => {\n let parsed;\n\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n gap: { type: \"string\" },\n self: { type: \"boolean\", default: false },\n skip: { type: \"string\" },\n top: { type: \"string\" },\n json: { type: \"boolean\", default: false },\n \"no-fail\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n console.error(`three-audit: ${(error as Error).message}\\n\\n${HELP}`);\n\n return 2;\n }\n\n const { values, positionals } = parsed;\n\n if (values.help) {\n console.log(HELP);\n\n return 0;\n }\n\n if (values.version) {\n console.log(version());\n\n return 0;\n }\n\n if (positionals.length === 0) {\n console.error(HELP);\n\n return 2;\n }\n\n const gap = values.gap === undefined ? undefined : Number(values.gap);\n const top = values.top === undefined ? 5 : Number(values.top);\n\n if ((gap !== undefined && !(gap > 0)) || !(top >= 0)) {\n console.error(\"three-audit: --gap must be a positive number and --top zero or more\");\n\n return 2;\n }\n\n const skipPattern = values.skip === undefined ? undefined : new RegExp(values.skip);\n const results: { file: string; report?: AuditReport; error?: string }[] = [];\n\n for (const file of positionals) {\n try {\n const scene = await loadModel(file);\n const report = auditScene(scene, {\n top,\n ...(gap === undefined ? {} : { gap }),\n ...(values.self ? { self: true } : {}),\n ...(skipPattern ? { skip: (object) => skipPattern.test(object.name) } : {}),\n });\n\n results.push({ file, report });\n } catch (error) {\n results.push({ file, error: (error as Error).message });\n }\n }\n\n if (values.json) {\n console.log(JSON.stringify(results, null, 2));\n } else {\n console.log(\n results\n .map(({ file, report, error }) =>\n report\n ? printReport(file, report, top)\n : `${basename(file)}\\n ✗ couldn't read: ${error}`,\n )\n .join(\"\\n\\n\"),\n );\n }\n\n if (results.some((result) => result.error !== undefined)) {\n return 2;\n }\n\n const failed = results.some(\n ({ report }) => report!.zFighting.length > 0 || report!.badGeometry.length > 0,\n );\n\n return failed && !values[\"no-fail\"] ? 1 : 0;\n};\n","#!/usr/bin/env node\n/** The `three-audit` bin. The work is in `cli/main.ts`, which the tests import. */\nimport { main } from \"./cli/main.ts\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { BufferGeometry,
|
|
1
|
+
import { BufferGeometry, Mesh, Object3D } from "three";
|
|
2
2
|
//#region src/scene.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Any three.js object, from whichever copy of three's types the app has. The checks take this, so
|
|
5
|
+
* an app on a newer `@types/three` than this package was built against passes its scene as is.
|
|
6
|
+
*/
|
|
7
|
+
type AnyObject3D = {
|
|
8
|
+
readonly isObject3D: true;
|
|
9
|
+
readonly children: readonly object[];
|
|
10
|
+
};
|
|
3
11
|
/** Leave an object and everything under it out of a check. */
|
|
4
12
|
type Skip = (object: Object3D) => boolean;
|
|
5
13
|
/** Triangles one draw of this geometry makes: indexed or not, within its draw range. */
|
|
@@ -23,14 +31,16 @@ type BadGeometryRow = {
|
|
|
23
31
|
readonly object: Mesh;
|
|
24
32
|
};
|
|
25
33
|
/** Meshes with geometry that renders NaN, hidden ones included. `[]` is a pass. */
|
|
26
|
-
export declare const findBadGeometry: (root:
|
|
34
|
+
export declare const findBadGeometry: (root: AnyObject3D, options?: BadGeometryOptions) => BadGeometryRow[];
|
|
27
35
|
//#endregion
|
|
28
36
|
//#region src/triangles.d.ts
|
|
29
37
|
type TriangleOptions = {
|
|
30
38
|
skip?: Skip | undefined;
|
|
31
39
|
};
|
|
32
40
|
type MeshRow = {
|
|
41
|
+
/** `path/to/mesh`: its named ancestors, then its name (or its geometry's name or type). */
|
|
33
42
|
name: string;
|
|
43
|
+
material: string;
|
|
34
44
|
/** Triangles in one copy (a batch's average). */
|
|
35
45
|
triangles: number;
|
|
36
46
|
instances: number;
|
|
@@ -50,15 +60,15 @@ type GeometryRow = {
|
|
|
50
60
|
* Triangles one pass draws: visible meshes, instances counted, no frustum culling. A shadow pass
|
|
51
61
|
* adds roughly as much again for each shadow-casting light.
|
|
52
62
|
*/
|
|
53
|
-
export declare const countTriangles: (root:
|
|
63
|
+
export declare const countTriangles: (root: AnyObject3D, options?: TriangleOptions) => number;
|
|
54
64
|
/** Every visible mesh, most triangles drawn first. */
|
|
55
|
-
export declare const listMeshes: (
|
|
65
|
+
export declare const listMeshes: (scene: AnyObject3D, options?: TriangleOptions) => MeshRow[];
|
|
56
66
|
/**
|
|
57
67
|
* Geometries by triangles drawn, hidden meshes included, most first. Name geometries
|
|
58
68
|
* (`geometry.name`) to make this readable. Batches are left out: their shared buffer is not one
|
|
59
69
|
* geometry; `listMeshes` counts them.
|
|
60
70
|
*/
|
|
61
|
-
export declare const geometryCensus: (root:
|
|
71
|
+
export declare const geometryCensus: (root: AnyObject3D, options?: TriangleOptions) => GeometryRow[];
|
|
62
72
|
//#endregion
|
|
63
73
|
//#region src/z-fighting.d.ts
|
|
64
74
|
type ZFightingOptions = {
|
|
@@ -78,16 +88,30 @@ type ZFightingOptions = {
|
|
|
78
88
|
skip?: Skip | undefined;
|
|
79
89
|
};
|
|
80
90
|
type ZFightingRow = {
|
|
81
|
-
/** `
|
|
91
|
+
/** `path/to/mesh [material] @ (x, y, z)`: named ancestors, and the centre in world space. */
|
|
82
92
|
a: string;
|
|
83
93
|
b: string;
|
|
84
94
|
/** Overlapping triangle pairs between the two. */
|
|
85
95
|
triangles: number;
|
|
86
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* The planes they share, most overlap first: `z = -3.000, facing +z` when axis-aligned, else
|
|
98
|
+
* `(nx, ny, nz) · p = d`. Both faces point along the normal: opposite-facing faces never pair.
|
|
99
|
+
*/
|
|
100
|
+
planes: string[];
|
|
101
|
+
/** Where they overlap: the area in world units², and its centre in world space. */
|
|
102
|
+
overlap: {
|
|
103
|
+
area: number;
|
|
104
|
+
centre: [number, number, number];
|
|
105
|
+
};
|
|
106
|
+
/** Identical pairs (same labels, triangles and planes) folded into this row. */
|
|
107
|
+
count: number;
|
|
108
|
+
/** Not enumerable: kept out of JSON and `console.table`. The first pair folded in. */
|
|
87
109
|
readonly meshes: readonly [Mesh, Mesh];
|
|
110
|
+
/** Not enumerable: every pair folded in. */
|
|
111
|
+
readonly pairs: readonly (readonly [Mesh, Mesh])[];
|
|
88
112
|
};
|
|
89
113
|
/** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */
|
|
90
|
-
export declare const findZFighting: (
|
|
114
|
+
export declare const findZFighting: (scene: AnyObject3D, options?: ZFightingOptions) => ZFightingRow[];
|
|
91
115
|
//#endregion
|
|
92
116
|
//#region src/audit-scene.d.ts
|
|
93
117
|
type AuditSceneOptions = Pick<ZFightingOptions, "gap" | "self" | "tagged"> & {
|
|
@@ -107,12 +131,12 @@ type AuditReport = {
|
|
|
107
131
|
* Every check at once, JSON-safe: what a test asserts on or a headless browser prints. A clean
|
|
108
132
|
* scene has `zFighting` and `badGeometry` both `[]`.
|
|
109
133
|
*/
|
|
110
|
-
export declare const auditScene: (root:
|
|
134
|
+
export declare const auditScene: (root: AnyObject3D, options?: AuditSceneOptions) => AuditReport;
|
|
111
135
|
//#endregion
|
|
112
136
|
//#region src/black-frames.d.ts
|
|
113
|
-
/** The `WebGLRenderer` methods the probe uses. */
|
|
137
|
+
/** The `WebGLRenderer` methods the probe uses, typed loosely to fit any copy of three's types. */
|
|
114
138
|
type ProbeRenderer = {
|
|
115
|
-
render(scene:
|
|
139
|
+
render(scene: AnyObject3D, camera: object): void;
|
|
116
140
|
getContext(): unknown;
|
|
117
141
|
getRenderTarget(): unknown;
|
|
118
142
|
};
|
|
@@ -139,10 +163,13 @@ type BlackFrameWatch = {
|
|
|
139
163
|
export declare const watchBlackFrames: (renderer: ProbeRenderer, options?: BlackFrameOptions) => BlackFrameWatch;
|
|
140
164
|
//#endregion
|
|
141
165
|
//#region src/draw-ledger.d.ts
|
|
142
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* The two `WebGLRenderer` methods the ledger wraps. Typed loosely, so a renderer from any copy of
|
|
168
|
+
* three's types fits.
|
|
169
|
+
*/
|
|
143
170
|
type LedgerRenderer = {
|
|
144
|
-
render(scene:
|
|
145
|
-
renderBufferDirect(camera:
|
|
171
|
+
render(scene: AnyObject3D, camera: object): void;
|
|
172
|
+
renderBufferDirect(camera: object, scene: object | null, geometry: object, material: object, object: AnyObject3D, group: unknown): void;
|
|
146
173
|
};
|
|
147
174
|
type DrawLedgerOptions = {
|
|
148
175
|
/**
|
|
@@ -151,6 +178,15 @@ type DrawLedgerOptions = {
|
|
|
151
178
|
*/
|
|
152
179
|
groupOf?: (name: string) => string;
|
|
153
180
|
};
|
|
181
|
+
type RecordDrawLedgerOptions = DrawLedgerOptions & {
|
|
182
|
+
/**
|
|
183
|
+
* Record one call of this (the app's own render, sync or async) instead of one animation frame.
|
|
184
|
+
* For a tab in the background, where no frames come.
|
|
185
|
+
*/
|
|
186
|
+
render?: () => unknown;
|
|
187
|
+
/** How long to wait for each animation frame before failing, in ms. Default 2000. */
|
|
188
|
+
timeout?: number;
|
|
189
|
+
};
|
|
154
190
|
type LedgerRow = {
|
|
155
191
|
name: string;
|
|
156
192
|
total: number;
|
|
@@ -179,8 +215,11 @@ export declare const beginDrawLedger: (renderer: LedgerRenderer, options?: DrawL
|
|
|
179
215
|
* Record exactly one frame of an app that renders from `requestAnimationFrame` (three's
|
|
180
216
|
* `setAnimationLoop`, React Three Fiber): from one animation frame to the next. A demand-driven
|
|
181
217
|
* loop that renders nothing that frame gives an empty ledger. Browser only.
|
|
218
|
+
*
|
|
219
|
+
* With `render`, records one call of it instead, frames or not. Without, fails after `timeout`
|
|
220
|
+
* when no frame comes (a background tab), rather than waiting for ever.
|
|
182
221
|
*/
|
|
183
|
-
export declare const recordDrawLedger: (renderer: LedgerRenderer, options?:
|
|
222
|
+
export declare const recordDrawLedger: (renderer: LedgerRenderer, options?: RecordDrawLedgerOptions) => Promise<DrawLedger>;
|
|
184
223
|
/** Rows shaped for `console.table`: one column per pass, then the total. */
|
|
185
224
|
export declare const ledgerTable: (rows: LedgerRow[], passes: string[]) => {
|
|
186
225
|
name: string;
|
|
@@ -191,19 +230,25 @@ export declare const printDrawLedger: (ledger: DrawLedger) => void;
|
|
|
191
230
|
//#endregion
|
|
192
231
|
//#region src/console-helpers.d.ts
|
|
193
232
|
type AuditHelpersOptions = {
|
|
194
|
-
/** A `WebGLRenderer`, for `ledger()` and `blackFrames()`. */
|
|
233
|
+
/** A `WebGLRenderer`, for `ledger()`, `beginLedger()` and `blackFrames()`. */
|
|
195
234
|
renderer?: (LedgerRenderer & ProbeRenderer) | undefined;
|
|
196
235
|
skip?: Skip | undefined;
|
|
197
236
|
tagKey?: string;
|
|
237
|
+
/**
|
|
238
|
+
* Also put each helper on `globalThis` under its short name (`tris`, `report`…). Default true;
|
|
239
|
+
* false leaves only `threeAudit`, for pages where short names collide with other tooling.
|
|
240
|
+
*/
|
|
241
|
+
globals?: boolean;
|
|
198
242
|
};
|
|
199
243
|
/**
|
|
200
|
-
* Put the helpers on `globalThis`. Returns a function that
|
|
244
|
+
* Put the helpers on `globalThis`, and on `globalThis.threeAudit`. Returns a function that
|
|
245
|
+
* removes them.
|
|
201
246
|
*
|
|
202
247
|
* vanilla: installAuditHelpers(scene, { renderer })
|
|
203
248
|
* R3F: const { scene, gl } = useThree();
|
|
204
249
|
* useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);
|
|
205
250
|
*/
|
|
206
|
-
export declare const installAuditHelpers: (root:
|
|
251
|
+
export declare const installAuditHelpers: (root: AnyObject3D, options?: AuditHelpersOptions) => () => void;
|
|
207
252
|
//#endregion
|
|
208
253
|
export type { AuditHelpersOptions, AuditReport, AuditSceneOptions, BadGeometryOptions, BadGeometryRow, BlackFrameOptions, BlackFrameWatch, DrawLedger, DrawLedgerOptions, DrawLedgerRecording, GeometryRow, LedgerRenderer, LedgerRow, MeshRow, ProbeRenderer, Skip, TriangleOptions, ZFightingOptions, ZFightingRow };
|
|
209
254
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as listMeshes, i as geometryCensus, n as findZFighting, o as findBadGeometry, r as countTriangles, s as
|
|
1
|
+
import { a as listMeshes, c as triangleCount, i as geometryCensus, n as findZFighting, o as findBadGeometry, r as countTriangles, s as pathOf, t as auditScene } from "./audit-scene-BYsJcco9.js";
|
|
2
2
|
//#region src/patch.ts
|
|
3
3
|
/** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */
|
|
4
4
|
const patch = (target, key, value) => {
|
|
@@ -13,6 +13,13 @@ const patch = (target, key, value) => {
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/black-frames.ts
|
|
15
15
|
/**
|
|
16
|
+
* Black-frame probe: after each frame, samples a grid of the drawing buffer and counts frames
|
|
17
|
+
* where several samples are pure black. Catches the flash a NaN pixel makes once bloom spreads it,
|
|
18
|
+
* which lasts one frame in twenty and never shows in a screenshot.
|
|
19
|
+
*
|
|
20
|
+
* WebGL only (`readPixels`). Each sample stalls the GPU pipeline, so watch, read, stop.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
16
23
|
* Sample every frame the app draws to the screen. Several screen renders in one frame (an overlay
|
|
17
24
|
* redrawn after the composer) count once, as the last of them: the image the display shows.
|
|
18
25
|
* Browser only: frames are told apart by `requestAnimationFrame`.
|
|
@@ -72,9 +79,38 @@ const watchBlackFrames = (renderer, options = {}) => {
|
|
|
72
79
|
};
|
|
73
80
|
};
|
|
74
81
|
//#endregion
|
|
82
|
+
//#region src/frames.ts
|
|
83
|
+
/**
|
|
84
|
+
* Waiting on animation frames without hanging: a tab in the background (an agent's browser, an
|
|
85
|
+
* occluded window) gets none, so every wait gives up after a timeout and says why.
|
|
86
|
+
*/
|
|
87
|
+
/** Frames an agent-driven tab should see within this long, in ms. */
|
|
88
|
+
const FRAME_TIMEOUT = 2e3;
|
|
89
|
+
const noFrame = (caller, timeout, hint) => {
|
|
90
|
+
const hidden = typeof document !== "undefined" && document.hidden;
|
|
91
|
+
return /* @__PURE__ */ new Error(`three-audit: ${caller} got no animation frame in ${timeout / 1e3} s${hidden ? " (document.hidden is true: browsers pause requestAnimationFrame in background tabs)" : ""}. ${hint}`);
|
|
92
|
+
};
|
|
93
|
+
/** The next animation frame, or an error naming the cause when none comes within `timeout`. */
|
|
94
|
+
const nextFrame = (caller, timeout, hint) => new Promise((resolve, reject) => {
|
|
95
|
+
if (typeof requestAnimationFrame !== "function") {
|
|
96
|
+
reject(/* @__PURE__ */ new Error(`three-audit: ${caller} needs requestAnimationFrame`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let frame = 0;
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
cancelAnimationFrame(frame);
|
|
102
|
+
reject(noFrame(caller, timeout, hint));
|
|
103
|
+
}, timeout);
|
|
104
|
+
frame = requestAnimationFrame(() => {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
resolve();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
//#endregion
|
|
75
110
|
//#region src/draw-ledger.ts
|
|
76
111
|
const defaultGroupOf = (name) => name.split(/[:#]/)[0] || name;
|
|
77
|
-
|
|
112
|
+
/** The object's name, or for an unnamed one its named ancestors, type and material. */
|
|
113
|
+
const nameOf = (object, material) => object.name || `${[...pathOf(object, null, void 0, 2), object.type].join("/")} [${material.name || material.type}]`;
|
|
78
114
|
const tally = (rows, name, pass) => {
|
|
79
115
|
const row = rows.get(name) ?? {
|
|
80
116
|
name,
|
|
@@ -132,21 +168,33 @@ const beginDrawLedger = (renderer, options = {}) => {
|
|
|
132
168
|
* Record exactly one frame of an app that renders from `requestAnimationFrame` (three's
|
|
133
169
|
* `setAnimationLoop`, React Three Fiber): from one animation frame to the next. A demand-driven
|
|
134
170
|
* loop that renders nothing that frame gives an empty ledger. Browser only.
|
|
171
|
+
*
|
|
172
|
+
* With `render`, records one call of it instead, frames or not. Without, fails after `timeout`
|
|
173
|
+
* when no frame comes (a background tab), rather than waiting for ever.
|
|
135
174
|
*/
|
|
136
|
-
const recordDrawLedger = (renderer, options = {}) =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
requestAnimationFrame(() => {
|
|
175
|
+
const recordDrawLedger = async (renderer, options = {}) => {
|
|
176
|
+
const { render, timeout = FRAME_TIMEOUT, ...ledgerOptions } = options;
|
|
177
|
+
if (render) {
|
|
178
|
+
const recording = beginDrawLedger(renderer, ledgerOptions);
|
|
142
179
|
try {
|
|
143
|
-
|
|
144
|
-
requestAnimationFrame(() => resolve(recording.end()));
|
|
180
|
+
await render();
|
|
145
181
|
} catch (error) {
|
|
146
|
-
|
|
182
|
+
recording.end();
|
|
183
|
+
throw error;
|
|
147
184
|
}
|
|
148
|
-
|
|
149
|
-
}
|
|
185
|
+
return recording.end();
|
|
186
|
+
}
|
|
187
|
+
const hint = "Bring the tab to the front, or pass { render } to record one call of your own.";
|
|
188
|
+
await nextFrame("recordDrawLedger", timeout, hint);
|
|
189
|
+
const recording = beginDrawLedger(renderer, ledgerOptions);
|
|
190
|
+
try {
|
|
191
|
+
await nextFrame("recordDrawLedger", timeout, hint);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
recording.end();
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
return recording.end();
|
|
197
|
+
};
|
|
150
198
|
/** Rows shaped for `console.table`: one column per pass, then the total. */
|
|
151
199
|
const ledgerTable = (rows, passes) => rows.map((row) => ({
|
|
152
200
|
name: row.name,
|
|
@@ -164,6 +212,13 @@ const printDrawLedger = (ledger) => {
|
|
|
164
212
|
};
|
|
165
213
|
//#endregion
|
|
166
214
|
//#region src/console-helpers.ts
|
|
215
|
+
/**
|
|
216
|
+
* The checks as console globals, for a dev page or a headless browser's `eval`: `tris()`,
|
|
217
|
+
* `meshes()`, `census()`, `audit()`, `zfight()`, `report()`, and with a renderer `ledger()`,
|
|
218
|
+
* `beginLedger()` and `blackFrames()`. All of them also sit on `threeAudit`, which is the only
|
|
219
|
+
* global with `globals: false`.
|
|
220
|
+
*/
|
|
221
|
+
const NAMESPACE = "threeAudit";
|
|
167
222
|
const NAMES = [
|
|
168
223
|
"auditRoot",
|
|
169
224
|
"tris",
|
|
@@ -173,13 +228,15 @@ const NAMES = [
|
|
|
173
228
|
"zfight",
|
|
174
229
|
"report",
|
|
175
230
|
"ledger",
|
|
231
|
+
"beginLedger",
|
|
176
232
|
"blackFrames"
|
|
177
233
|
];
|
|
178
234
|
const needsRenderer = (name) => () => {
|
|
179
235
|
throw new Error(`three-audit: ${name}() needs installAuditHelpers(scene, { renderer })`);
|
|
180
236
|
};
|
|
181
237
|
/**
|
|
182
|
-
* Put the helpers on `globalThis`. Returns a function that
|
|
238
|
+
* Put the helpers on `globalThis`, and on `globalThis.threeAudit`. Returns a function that
|
|
239
|
+
* removes them.
|
|
183
240
|
*
|
|
184
241
|
* vanilla: installAuditHelpers(scene, { renderer })
|
|
185
242
|
* R3F: const { scene, gl } = useThree();
|
|
@@ -207,26 +264,40 @@ const installAuditHelpers = (root, options = {}) => {
|
|
|
207
264
|
skip,
|
|
208
265
|
tagKey
|
|
209
266
|
}),
|
|
210
|
-
ledger: renderer ? async () => {
|
|
211
|
-
const ledger = await recordDrawLedger(renderer);
|
|
267
|
+
ledger: renderer ? async (ledgerOptions) => {
|
|
268
|
+
const ledger = await recordDrawLedger(renderer, ledgerOptions);
|
|
212
269
|
printDrawLedger(ledger);
|
|
213
270
|
return ledger;
|
|
214
271
|
} : needsRenderer("ledger"),
|
|
215
|
-
|
|
272
|
+
beginLedger: renderer ? (ledgerOptions) => {
|
|
273
|
+
const recording = beginDrawLedger(renderer, ledgerOptions);
|
|
274
|
+
return { end: () => {
|
|
275
|
+
const ledger = recording.end();
|
|
276
|
+
printDrawLedger(ledger);
|
|
277
|
+
return ledger;
|
|
278
|
+
} };
|
|
279
|
+
} : needsRenderer("beginLedger"),
|
|
280
|
+
blackFrames: renderer ? async (seconds = 10) => {
|
|
281
|
+
await nextFrame("blackFrames()", FRAME_TIMEOUT, "Black frames only show in a visible tab: bring it to the front.");
|
|
216
282
|
const watch = watchBlackFrames(renderer);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
watch.stop();
|
|
286
|
+
resolve({
|
|
287
|
+
frames: watch.frames,
|
|
288
|
+
blackFrames: watch.blackFrames
|
|
289
|
+
});
|
|
290
|
+
}, seconds * 1e3);
|
|
291
|
+
});
|
|
292
|
+
} : needsRenderer("blackFrames")
|
|
225
293
|
};
|
|
226
|
-
|
|
294
|
+
const namespace = { ...helpers };
|
|
295
|
+
const scope = globalThis;
|
|
296
|
+
if (options.globals !== false) Object.assign(scope, helpers);
|
|
297
|
+
scope[NAMESPACE] = namespace;
|
|
227
298
|
return () => {
|
|
228
|
-
const scope = globalThis;
|
|
229
299
|
for (const name of NAMES) if (scope[name] === helpers[name]) delete scope[name];
|
|
300
|
+
if (scope[NAMESPACE] === namespace) delete scope[NAMESPACE];
|
|
230
301
|
};
|
|
231
302
|
};
|
|
232
303
|
//#endregion
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/patch.ts","../src/black-frames.ts","../src/draw-ledger.ts","../src/console-helpers.ts"],"sourcesContent":["/** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */\nexport const patch = <Target extends object, Key extends keyof Target>(\n target: Target,\n key: Key,\n value: Target[Key],\n) => {\n const own = Object.hasOwn(target, key);\n const original = target[key];\n\n target[key] = value;\n\n return () => {\n if (own) {\n target[key] = original;\n } else {\n delete target[key];\n }\n };\n};\n","/**\n * Black-frame probe: after each frame, samples a grid of the drawing buffer and counts frames\n * where several samples are pure black. Catches the flash a NaN pixel makes once bloom spreads it,\n * which lasts one frame in twenty and never shows in a screenshot.\n *\n * WebGL only (`readPixels`). Each sample stalls the GPU pipeline, so watch, read, stop.\n */\nimport type { Camera, Object3D } from \"three\";\n\nimport { patch } from \"./patch.ts\";\n\n/** The `WebGLRenderer` methods the probe uses. */\nexport type ProbeRenderer = {\n render(scene: Object3D, camera: Camera): void;\n getContext(): unknown;\n getRenderTarget(): unknown;\n};\n\nexport type BlackFrameOptions = {\n /** Samples per side: 3 reads a 3 × 3 grid at the quarter lines. */\n grid?: number;\n /** Black samples that make a frame black. */\n threshold?: number;\n};\n\nexport type BlackFrameWatch = {\n /** Frames sampled since the start or the last `reset()`. */\n readonly frames: number;\n /** Which of those frames (1-based) were black. */\n readonly blackFrames: readonly number[];\n reset: () => void;\n /** Restore the renderer and stop sampling. */\n stop: () => void;\n};\n\ntype ReadPixels = Pick<\n WebGLRenderingContext,\n \"RGBA\" | \"UNSIGNED_BYTE\" | \"drawingBufferHeight\" | \"drawingBufferWidth\" | \"readPixels\"\n>;\n\n/**\n * Sample every frame the app draws to the screen. Several screen renders in one frame (an overlay\n * redrawn after the composer) count once, as the last of them: the image the display shows.\n * Browser only: frames are told apart by `requestAnimationFrame`.\n */\nexport const watchBlackFrames = (\n renderer: ProbeRenderer,\n options: BlackFrameOptions = {},\n): BlackFrameWatch => {\n const context = renderer.getContext() as Partial<ReadPixels> | null;\n\n if (typeof context?.readPixels !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs a WebGLRenderer\");\n }\n\n if (typeof requestAnimationFrame !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs requestAnimationFrame\");\n }\n\n const gl = context as ReadPixels;\n const grid = options.grid ?? 3;\n const threshold = options.threshold ?? 3;\n const pixel = new Uint8Array(4);\n const render = renderer.render;\n let frames = 0;\n let black: number[] = [];\n let pending: boolean | null = null;\n\n const isBlack = () => {\n let count = 0;\n\n for (let i = 1; i <= grid; i += 1) {\n for (let j = 1; j <= grid; j += 1) {\n const x = Math.floor((gl.drawingBufferWidth * i) / (grid + 1));\n const y = Math.floor((gl.drawingBufferHeight * j) / (grid + 1));\n\n gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);\n\n if (pixel[0]! < 4 && pixel[1]! < 4 && pixel[2]! < 4 && pixel[3]! > 250) {\n count += 1;\n }\n }\n }\n\n return count >= threshold;\n };\n\n // Commit the frame's last screen sample once per animation frame.\n let raf = 0;\n const commit = () => {\n if (pending !== null) {\n frames += 1;\n\n if (pending) {\n black.push(frames);\n }\n\n pending = null;\n }\n\n raf = requestAnimationFrame(commit);\n };\n\n raf = requestAnimationFrame(commit);\n\n const restore = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n render.call(this, scene, camera);\n\n // Read before the browser presents (and, without preserveDrawingBuffer, clears) the buffer.\n if (renderer.getRenderTarget() === null) {\n pending = isBlack();\n }\n });\n\n return {\n get frames() {\n return frames;\n },\n get blackFrames() {\n return black;\n },\n reset: () => {\n frames = 0;\n black = [];\n pending = null;\n },\n stop: () => {\n cancelAnimationFrame(raf);\n restore();\n },\n };\n};\n","/**\n * A ledger of every draw call over one frame: which object drew, in which pass. Exact counts from\n * the renderer itself, where `renderer.info` gives one total (and resets it every `render()`).\n *\n * WebGL only: it wraps `WebGLRenderer.renderBufferDirect`, which every draw goes through, and\n * `render()`, to tell passes apart. Draws with a depth or distance material are the shadow maps.\n */\nimport type { BufferGeometry, Camera, Material, Object3D, Scene } from \"three\";\n\nimport { patch } from \"./patch.ts\";\n\n/** The two `WebGLRenderer` methods the ledger wraps. */\nexport type LedgerRenderer = {\n render(scene: Object3D, camera: Camera): void;\n renderBufferDirect(\n camera: Camera,\n scene: Scene | null,\n geometry: BufferGeometry,\n material: Material,\n object: Object3D,\n group: unknown,\n ): void;\n};\n\nexport type DrawLedgerOptions = {\n /**\n * The group an object name files under. Default: up to the first `:` or `#`, so a bake's\n * `wagon:paint` and a strip's `grass#3` group as `wagon` and `grass`.\n */\n groupOf?: (name: string) => string;\n};\n\nexport type LedgerRow = {\n name: string;\n total: number;\n /** Draws per pass name. */\n passes: Record<string, number>;\n};\n\nexport type DrawLedger = {\n /**\n * Pass names in the order they first drew: `shadow`, then each `render()` call as its scene's\n * name, or `render 1`, `render 2`… when unnamed (a post-processing chain is several).\n */\n passes: string[];\n /** By group, most draws first. */\n groups: LedgerRow[];\n /** By object name, most draws first. */\n objects: LedgerRow[];\n draws: number;\n};\n\nexport type DrawLedgerRecording = {\n /** Restore the renderer and return what was drawn since `beginDrawLedger`. */\n end: () => DrawLedger;\n};\n\nconst defaultGroupOf = (name: string) => name.split(/[:#]/)[0] || name;\n\nconst nameOf = (object: Object3D, material: Material) =>\n object.name || `${object.type}:${material.name || material.type}`;\n\nconst tally = (rows: Map<string, LedgerRow>, name: string, pass: string) => {\n const row = rows.get(name) ?? { name, total: 0, passes: {} };\n\n row.total += 1;\n row.passes[pass] = (row.passes[pass] ?? 0) + 1;\n rows.set(name, row);\n};\n\nconst sorted = (rows: Map<string, LedgerRow>) =>\n [...rows.values()].sort((a, b) => b.total - a.total);\n\n/** Start counting every draw. Call `end()` after the frame to restore the renderer and read it. */\nexport const beginDrawLedger = (\n renderer: LedgerRenderer,\n options: DrawLedgerOptions = {},\n): DrawLedgerRecording => {\n if (typeof renderer.renderBufferDirect !== \"function\") {\n throw new TypeError(\"three-audit: the draw ledger needs a WebGLRenderer\");\n }\n\n const groupOf = options.groupOf ?? defaultGroupOf;\n const render = renderer.render;\n const renderBufferDirect = renderer.renderBufferDirect;\n const passes: string[] = [];\n const objects = new Map<string, LedgerRow>();\n const groups = new Map<string, LedgerRow>();\n let renders = 0;\n let current = \"direct\";\n let draws = 0;\n\n const restoreRender = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n const outer = current;\n\n renders += 1;\n current = scene.name || `render ${renders}`;\n\n try {\n render.call(this, scene, camera);\n } finally {\n current = outer;\n }\n });\n\n const restoreDraw = patch(\n renderer,\n \"renderBufferDirect\",\n function (this: unknown, camera, scene, geometry, material, object, group) {\n const shadow =\n (material as { isMeshDepthMaterial?: boolean }).isMeshDepthMaterial === true ||\n (material as { isMeshDistanceMaterial?: boolean }).isMeshDistanceMaterial === true;\n const pass = shadow ? \"shadow\" : current;\n const name = nameOf(object, material);\n\n if (!passes.includes(pass)) {\n passes.push(pass);\n }\n\n draws += 1;\n tally(objects, name, pass);\n tally(groups, groupOf(name), pass);\n renderBufferDirect.call(this, camera, scene, geometry, material, object, group);\n },\n );\n\n return {\n end: () => {\n restoreDraw();\n restoreRender();\n\n return { passes, groups: sorted(groups), objects: sorted(objects), draws };\n },\n };\n};\n\n/**\n * Record exactly one frame of an app that renders from `requestAnimationFrame` (three's\n * `setAnimationLoop`, React Three Fiber): from one animation frame to the next. A demand-driven\n * loop that renders nothing that frame gives an empty ledger. Browser only.\n */\nexport const recordDrawLedger = (renderer: LedgerRenderer, options: DrawLedgerOptions = {}) =>\n new Promise<DrawLedger>((resolve, reject) => {\n if (typeof requestAnimationFrame !== \"function\") {\n reject(new Error(\"three-audit: recordDrawLedger needs requestAnimationFrame\"));\n\n return;\n }\n\n requestAnimationFrame(() => {\n try {\n const recording = beginDrawLedger(renderer, options);\n\n requestAnimationFrame(() => resolve(recording.end()));\n } catch (error) {\n reject(error);\n }\n });\n });\n\n/** Rows shaped for `console.table`: one column per pass, then the total. */\nexport const ledgerTable = (rows: LedgerRow[], passes: string[]) =>\n rows.map((row) => ({\n name: row.name,\n ...Object.fromEntries(passes.map((pass) => [pass, row.passes[pass] ?? 0])),\n total: row.total,\n }));\n\n/** The ledger as two console tables: by group, and every object collapsed beneath. */\nexport const printDrawLedger = (ledger: DrawLedger) => {\n /* oxlint-disable no-console -- printing is this function's job */\n console.group(`Draw ledger: ${ledger.draws} draws in one frame`);\n console.table(ledgerTable(ledger.groups, ledger.passes));\n console.groupCollapsed(\"Every object\");\n console.table(ledgerTable(ledger.objects, ledger.passes));\n console.groupEnd();\n console.groupEnd();\n /* oxlint-enable no-console */\n};\n","/**\n * The checks as short console globals, for a dev page or a headless browser's `eval`:\n * `tris()`, `meshes()`, `census()`, `audit()`, `zfight()`, `report()`, and with a renderer\n * `ledger()` and `blackFrames()`.\n */\nimport type { Object3D } from \"three\";\n\nimport { auditScene } from \"./audit-scene.ts\";\nimport { findBadGeometry } from \"./bad-geometry.ts\";\nimport { type ProbeRenderer, watchBlackFrames } from \"./black-frames.ts\";\nimport { type LedgerRenderer, printDrawLedger, recordDrawLedger } from \"./draw-ledger.ts\";\nimport type { Skip } from \"./scene.ts\";\nimport { countTriangles, geometryCensus, listMeshes } from \"./triangles.ts\";\nimport { findZFighting } from \"./z-fighting.ts\";\n\nexport type AuditHelpersOptions = {\n /** A `WebGLRenderer`, for `ledger()` and `blackFrames()`. */\n renderer?: (LedgerRenderer & ProbeRenderer) | undefined;\n skip?: Skip | undefined;\n tagKey?: string;\n};\n\nconst NAMES = [\n \"auditRoot\",\n \"tris\",\n \"meshes\",\n \"census\",\n \"audit\",\n \"zfight\",\n \"report\",\n \"ledger\",\n \"blackFrames\",\n] as const;\n\nconst needsRenderer = (name: string) => () => {\n throw new Error(`three-audit: ${name}() needs installAuditHelpers(scene, { renderer })`);\n};\n\n/**\n * Put the helpers on `globalThis`. Returns a function that removes them.\n *\n * vanilla: installAuditHelpers(scene, { renderer })\n * R3F: const { scene, gl } = useThree();\n * useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);\n */\nexport const installAuditHelpers = (root: Object3D, options: AuditHelpersOptions = {}) => {\n const { renderer, skip } = options;\n const tagKey = options.tagKey ?? \"studioObject\";\n\n const helpers: Record<(typeof NAMES)[number], unknown> = {\n auditRoot: root,\n tris: () => countTriangles(root, { skip }),\n meshes: () => listMeshes(root, { skip }),\n census: () => geometryCensus(root, { skip }),\n audit: () => findBadGeometry(root, { skip, tagKey }),\n zfight: (gap?: number, self?: boolean) =>\n findZFighting(root, {\n skip,\n tagKey,\n ...(gap === undefined ? {} : { gap }),\n ...(self === undefined ? {} : { self }),\n }),\n report: () => auditScene(root, { skip, tagKey }),\n ledger: renderer\n ? async () => {\n const ledger = await recordDrawLedger(renderer);\n\n printDrawLedger(ledger);\n\n return ledger;\n }\n : needsRenderer(\"ledger\"),\n blackFrames: renderer\n ? (seconds = 10) =>\n new Promise<{ frames: number; blackFrames: readonly number[] }>((resolve) => {\n const watch = watchBlackFrames(renderer);\n\n setTimeout(() => {\n watch.stop();\n resolve({ frames: watch.frames, blackFrames: watch.blackFrames });\n }, seconds * 1000);\n })\n : needsRenderer(\"blackFrames\"),\n };\n\n Object.assign(globalThis, helpers);\n\n return () => {\n const scope = globalThis as Record<string, unknown>;\n\n for (const name of NAMES) {\n if (scope[name] === helpers[name]) {\n delete scope[name];\n }\n }\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/patch.ts","../src/black-frames.ts","../src/frames.ts","../src/draw-ledger.ts","../src/console-helpers.ts"],"sourcesContent":["/** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */\nexport const patch = <Target extends object, Key extends keyof Target>(\n target: Target,\n key: Key,\n value: Target[Key],\n) => {\n const own = Object.hasOwn(target, key);\n const original = target[key];\n\n target[key] = value;\n\n return () => {\n if (own) {\n target[key] = original;\n } else {\n delete target[key];\n }\n };\n};\n","/**\n * Black-frame probe: after each frame, samples a grid of the drawing buffer and counts frames\n * where several samples are pure black. Catches the flash a NaN pixel makes once bloom spreads it,\n * which lasts one frame in twenty and never shows in a screenshot.\n *\n * WebGL only (`readPixels`). Each sample stalls the GPU pipeline, so watch, read, stop.\n */\nimport { patch } from \"./patch.ts\";\nimport type { AnyObject3D } from \"./scene.ts\";\n\n/** The `WebGLRenderer` methods the probe uses, typed loosely to fit any copy of three's types. */\nexport type ProbeRenderer = {\n render(scene: AnyObject3D, camera: object): void;\n getContext(): unknown;\n getRenderTarget(): unknown;\n};\n\nexport type BlackFrameOptions = {\n /** Samples per side: 3 reads a 3 × 3 grid at the quarter lines. */\n grid?: number;\n /** Black samples that make a frame black. */\n threshold?: number;\n};\n\nexport type BlackFrameWatch = {\n /** Frames sampled since the start or the last `reset()`. */\n readonly frames: number;\n /** Which of those frames (1-based) were black. */\n readonly blackFrames: readonly number[];\n reset: () => void;\n /** Restore the renderer and stop sampling. */\n stop: () => void;\n};\n\ntype ReadPixels = Pick<\n WebGLRenderingContext,\n \"RGBA\" | \"UNSIGNED_BYTE\" | \"drawingBufferHeight\" | \"drawingBufferWidth\" | \"readPixels\"\n>;\n\n/**\n * Sample every frame the app draws to the screen. Several screen renders in one frame (an overlay\n * redrawn after the composer) count once, as the last of them: the image the display shows.\n * Browser only: frames are told apart by `requestAnimationFrame`.\n */\nexport const watchBlackFrames = (\n renderer: ProbeRenderer,\n options: BlackFrameOptions = {},\n): BlackFrameWatch => {\n const context = renderer.getContext() as Partial<ReadPixels> | null;\n\n if (typeof context?.readPixels !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs a WebGLRenderer\");\n }\n\n if (typeof requestAnimationFrame !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs requestAnimationFrame\");\n }\n\n const gl = context as ReadPixels;\n const grid = options.grid ?? 3;\n const threshold = options.threshold ?? 3;\n const pixel = new Uint8Array(4);\n const render = renderer.render;\n let frames = 0;\n let black: number[] = [];\n let pending: boolean | null = null;\n\n const isBlack = () => {\n let count = 0;\n\n for (let i = 1; i <= grid; i += 1) {\n for (let j = 1; j <= grid; j += 1) {\n const x = Math.floor((gl.drawingBufferWidth * i) / (grid + 1));\n const y = Math.floor((gl.drawingBufferHeight * j) / (grid + 1));\n\n gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);\n\n if (pixel[0]! < 4 && pixel[1]! < 4 && pixel[2]! < 4 && pixel[3]! > 250) {\n count += 1;\n }\n }\n }\n\n return count >= threshold;\n };\n\n // Commit the frame's last screen sample once per animation frame.\n let raf = 0;\n const commit = () => {\n if (pending !== null) {\n frames += 1;\n\n if (pending) {\n black.push(frames);\n }\n\n pending = null;\n }\n\n raf = requestAnimationFrame(commit);\n };\n\n raf = requestAnimationFrame(commit);\n\n const restore = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n render.call(this, scene, camera);\n\n // Read before the browser presents (and, without preserveDrawingBuffer, clears) the buffer.\n if (renderer.getRenderTarget() === null) {\n pending = isBlack();\n }\n });\n\n return {\n get frames() {\n return frames;\n },\n get blackFrames() {\n return black;\n },\n reset: () => {\n frames = 0;\n black = [];\n pending = null;\n },\n stop: () => {\n cancelAnimationFrame(raf);\n restore();\n },\n };\n};\n","/**\n * Waiting on animation frames without hanging: a tab in the background (an agent's browser, an\n * occluded window) gets none, so every wait gives up after a timeout and says why.\n */\n\n/** Frames an agent-driven tab should see within this long, in ms. */\nexport const FRAME_TIMEOUT = 2000;\n\nconst noFrame = (caller: string, timeout: number, hint: string) => {\n const hidden = typeof document !== \"undefined\" && document.hidden;\n\n return new Error(\n `three-audit: ${caller} got no animation frame in ${timeout / 1000} s${\n hidden\n ? \" (document.hidden is true: browsers pause requestAnimationFrame in background tabs)\"\n : \"\"\n }. ${hint}`,\n );\n};\n\n/** The next animation frame, or an error naming the cause when none comes within `timeout`. */\nexport const nextFrame = (caller: string, timeout: number, hint: string) =>\n new Promise<void>((resolve, reject) => {\n if (typeof requestAnimationFrame !== \"function\") {\n reject(new Error(`three-audit: ${caller} needs requestAnimationFrame`));\n\n return;\n }\n\n let frame = 0;\n const timer = setTimeout(() => {\n cancelAnimationFrame(frame);\n reject(noFrame(caller, timeout, hint));\n }, timeout);\n\n frame = requestAnimationFrame(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n","/**\n * A ledger of every draw call over one frame: which object drew, in which pass. Exact counts from\n * the renderer itself, where `renderer.info` gives one total (and resets it every `render()`).\n *\n * WebGL only: it wraps `WebGLRenderer.renderBufferDirect`, which every draw goes through, and\n * `render()`, to tell passes apart. Draws with a depth or distance material are the shadow maps.\n */\nimport type { Material, Object3D } from \"three\";\n\nimport { FRAME_TIMEOUT, nextFrame } from \"./frames.ts\";\nimport { patch } from \"./patch.ts\";\nimport { type AnyObject3D, pathOf } from \"./scene.ts\";\n\n/**\n * The two `WebGLRenderer` methods the ledger wraps. Typed loosely, so a renderer from any copy of\n * three's types fits.\n */\nexport type LedgerRenderer = {\n render(scene: AnyObject3D, camera: object): void;\n renderBufferDirect(\n camera: object,\n scene: object | null,\n geometry: object,\n material: object,\n object: AnyObject3D,\n group: unknown,\n ): void;\n};\n\nexport type DrawLedgerOptions = {\n /**\n * The group an object name files under. Default: up to the first `:` or `#`, so a bake's\n * `wagon:paint` and a strip's `grass#3` group as `wagon` and `grass`.\n */\n groupOf?: (name: string) => string;\n};\n\nexport type RecordDrawLedgerOptions = DrawLedgerOptions & {\n /**\n * Record one call of this (the app's own render, sync or async) instead of one animation frame.\n * For a tab in the background, where no frames come.\n */\n render?: () => unknown;\n /** How long to wait for each animation frame before failing, in ms. Default 2000. */\n timeout?: number;\n};\n\nexport type LedgerRow = {\n name: string;\n total: number;\n /** Draws per pass name. */\n passes: Record<string, number>;\n};\n\nexport type DrawLedger = {\n /**\n * Pass names in the order they first drew: `shadow`, then each `render()` call as its scene's\n * name, or `render 1`, `render 2`… when unnamed (a post-processing chain is several).\n */\n passes: string[];\n /** By group, most draws first. */\n groups: LedgerRow[];\n /** By object name, most draws first. */\n objects: LedgerRow[];\n draws: number;\n};\n\nexport type DrawLedgerRecording = {\n /** Restore the renderer and return what was drawn since `beginDrawLedger`. */\n end: () => DrawLedger;\n};\n\nconst defaultGroupOf = (name: string) => name.split(/[:#]/)[0] || name;\n\n/** The object's name, or for an unnamed one its named ancestors, type and material. */\nconst nameOf = (object: Object3D, material: Material) =>\n object.name ||\n `${[...pathOf(object, null, undefined, 2), object.type].join(\"/\")} [${material.name || material.type}]`;\n\nconst tally = (rows: Map<string, LedgerRow>, name: string, pass: string) => {\n const row = rows.get(name) ?? { name, total: 0, passes: {} };\n\n row.total += 1;\n row.passes[pass] = (row.passes[pass] ?? 0) + 1;\n rows.set(name, row);\n};\n\nconst sorted = (rows: Map<string, LedgerRow>) =>\n [...rows.values()].sort((a, b) => b.total - a.total);\n\n/** Start counting every draw. Call `end()` after the frame to restore the renderer and read it. */\nexport const beginDrawLedger = (\n renderer: LedgerRenderer,\n options: DrawLedgerOptions = {},\n): DrawLedgerRecording => {\n if (typeof renderer.renderBufferDirect !== \"function\") {\n throw new TypeError(\"three-audit: the draw ledger needs a WebGLRenderer\");\n }\n\n const groupOf = options.groupOf ?? defaultGroupOf;\n const render = renderer.render;\n const renderBufferDirect = renderer.renderBufferDirect;\n const passes: string[] = [];\n const objects = new Map<string, LedgerRow>();\n const groups = new Map<string, LedgerRow>();\n let renders = 0;\n let current = \"direct\";\n let draws = 0;\n\n const restoreRender = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n const outer = current;\n\n renders += 1;\n current = (scene as Partial<Object3D>).name || `render ${renders}`;\n\n try {\n render.call(this, scene, camera);\n } finally {\n current = outer;\n }\n });\n\n const restoreDraw = patch(\n renderer,\n \"renderBufferDirect\",\n function (this: unknown, camera, scene, geometry, material, object, group) {\n const shadow =\n (material as { isMeshDepthMaterial?: boolean }).isMeshDepthMaterial === true ||\n (material as { isMeshDistanceMaterial?: boolean }).isMeshDistanceMaterial === true;\n const pass = shadow ? \"shadow\" : current;\n const name = nameOf(object as unknown as Object3D, material as Material);\n\n if (!passes.includes(pass)) {\n passes.push(pass);\n }\n\n draws += 1;\n tally(objects, name, pass);\n tally(groups, groupOf(name), pass);\n renderBufferDirect.call(this, camera, scene, geometry, material, object, group);\n },\n );\n\n return {\n end: () => {\n restoreDraw();\n restoreRender();\n\n return { passes, groups: sorted(groups), objects: sorted(objects), draws };\n },\n };\n};\n\n/**\n * Record exactly one frame of an app that renders from `requestAnimationFrame` (three's\n * `setAnimationLoop`, React Three Fiber): from one animation frame to the next. A demand-driven\n * loop that renders nothing that frame gives an empty ledger. Browser only.\n *\n * With `render`, records one call of it instead, frames or not. Without, fails after `timeout`\n * when no frame comes (a background tab), rather than waiting for ever.\n */\nexport const recordDrawLedger = async (\n renderer: LedgerRenderer,\n options: RecordDrawLedgerOptions = {},\n): Promise<DrawLedger> => {\n const { render, timeout = FRAME_TIMEOUT, ...ledgerOptions } = options;\n\n if (render) {\n const recording = beginDrawLedger(renderer, ledgerOptions);\n\n try {\n await render();\n } catch (error) {\n recording.end();\n throw error;\n }\n\n return recording.end();\n }\n\n const hint = \"Bring the tab to the front, or pass { render } to record one call of your own.\";\n\n await nextFrame(\"recordDrawLedger\", timeout, hint);\n\n const recording = beginDrawLedger(renderer, ledgerOptions);\n\n try {\n await nextFrame(\"recordDrawLedger\", timeout, hint);\n } catch (error) {\n recording.end();\n throw error;\n }\n\n return recording.end();\n};\n\n/** Rows shaped for `console.table`: one column per pass, then the total. */\nexport const ledgerTable = (rows: LedgerRow[], passes: string[]) =>\n rows.map((row) => ({\n name: row.name,\n ...Object.fromEntries(passes.map((pass) => [pass, row.passes[pass] ?? 0])),\n total: row.total,\n }));\n\n/** The ledger as two console tables: by group, and every object collapsed beneath. */\nexport const printDrawLedger = (ledger: DrawLedger) => {\n /* oxlint-disable no-console -- printing is this function's job */\n console.group(`Draw ledger: ${ledger.draws} draws in one frame`);\n console.table(ledgerTable(ledger.groups, ledger.passes));\n console.groupCollapsed(\"Every object\");\n console.table(ledgerTable(ledger.objects, ledger.passes));\n console.groupEnd();\n console.groupEnd();\n /* oxlint-enable no-console */\n};\n","/**\n * The checks as console globals, for a dev page or a headless browser's `eval`: `tris()`,\n * `meshes()`, `census()`, `audit()`, `zfight()`, `report()`, and with a renderer `ledger()`,\n * `beginLedger()` and `blackFrames()`. All of them also sit on `threeAudit`, which is the only\n * global with `globals: false`.\n */\nimport { auditScene } from \"./audit-scene.ts\";\nimport { findBadGeometry } from \"./bad-geometry.ts\";\nimport { type ProbeRenderer, watchBlackFrames } from \"./black-frames.ts\";\nimport {\n beginDrawLedger,\n type DrawLedgerOptions,\n type LedgerRenderer,\n printDrawLedger,\n recordDrawLedger,\n type RecordDrawLedgerOptions,\n} from \"./draw-ledger.ts\";\nimport { FRAME_TIMEOUT, nextFrame } from \"./frames.ts\";\nimport type { AnyObject3D, Skip } from \"./scene.ts\";\nimport { countTriangles, geometryCensus, listMeshes } from \"./triangles.ts\";\nimport { findZFighting } from \"./z-fighting.ts\";\n\nexport type AuditHelpersOptions = {\n /** A `WebGLRenderer`, for `ledger()`, `beginLedger()` and `blackFrames()`. */\n renderer?: (LedgerRenderer & ProbeRenderer) | undefined;\n skip?: Skip | undefined;\n tagKey?: string;\n /**\n * Also put each helper on `globalThis` under its short name (`tris`, `report`…). Default true;\n * false leaves only `threeAudit`, for pages where short names collide with other tooling.\n */\n globals?: boolean;\n};\n\nconst NAMESPACE = \"threeAudit\";\n\nconst NAMES = [\n \"auditRoot\",\n \"tris\",\n \"meshes\",\n \"census\",\n \"audit\",\n \"zfight\",\n \"report\",\n \"ledger\",\n \"beginLedger\",\n \"blackFrames\",\n] as const;\n\nconst needsRenderer = (name: string) => () => {\n throw new Error(`three-audit: ${name}() needs installAuditHelpers(scene, { renderer })`);\n};\n\n/**\n * Put the helpers on `globalThis`, and on `globalThis.threeAudit`. Returns a function that\n * removes them.\n *\n * vanilla: installAuditHelpers(scene, { renderer })\n * R3F: const { scene, gl } = useThree();\n * useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);\n */\nexport const installAuditHelpers = (root: AnyObject3D, options: AuditHelpersOptions = {}) => {\n const { renderer, skip } = options;\n const tagKey = options.tagKey ?? \"studioObject\";\n\n const helpers: Record<(typeof NAMES)[number], unknown> = {\n auditRoot: root,\n tris: () => countTriangles(root, { skip }),\n meshes: () => listMeshes(root, { skip }),\n census: () => geometryCensus(root, { skip }),\n audit: () => findBadGeometry(root, { skip, tagKey }),\n zfight: (gap?: number, self?: boolean) =>\n findZFighting(root, {\n skip,\n tagKey,\n ...(gap === undefined ? {} : { gap }),\n ...(self === undefined ? {} : { self }),\n }),\n report: () => auditScene(root, { skip, tagKey }),\n ledger: renderer\n ? async (ledgerOptions?: RecordDrawLedgerOptions) => {\n const ledger = await recordDrawLedger(renderer, ledgerOptions);\n\n printDrawLedger(ledger);\n\n return ledger;\n }\n : needsRenderer(\"ledger\"),\n beginLedger: renderer\n ? (ledgerOptions?: DrawLedgerOptions) => {\n const recording = beginDrawLedger(renderer, ledgerOptions);\n\n return {\n end: () => {\n const ledger = recording.end();\n\n printDrawLedger(ledger);\n\n return ledger;\n },\n };\n }\n : needsRenderer(\"beginLedger\"),\n blackFrames: renderer\n ? async (seconds = 10) => {\n await nextFrame(\n \"blackFrames()\",\n FRAME_TIMEOUT,\n \"Black frames only show in a visible tab: bring it to the front.\",\n );\n\n const watch = watchBlackFrames(renderer);\n\n return new Promise<{ frames: number; blackFrames: readonly number[] }>((resolve) => {\n setTimeout(() => {\n watch.stop();\n resolve({ frames: watch.frames, blackFrames: watch.blackFrames });\n }, seconds * 1000);\n });\n }\n : needsRenderer(\"blackFrames\"),\n };\n const namespace = { ...helpers };\n const scope = globalThis as Record<string, unknown>;\n\n if (options.globals !== false) {\n Object.assign(scope, helpers);\n }\n\n scope[NAMESPACE] = namespace;\n\n return () => {\n for (const name of NAMES) {\n if (scope[name] === helpers[name]) {\n delete scope[name];\n }\n }\n\n if (scope[NAMESPACE] === namespace) {\n delete scope[NAMESPACE];\n }\n };\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zkmake/three-audit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Scene checks for three.js: triangle counts, cost census, NaN normals, z-fighting, black frames and a one-frame draw-call ledger. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"audit",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"audit-scene-Cz1FZxhd.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; 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 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 Math.abs(planes[s * 4 + 3]! - planes[t * 4 + 3]!) > gap ||\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":"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"}
|