@zkmake/three-audit 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -14
- package/dist/audit-scene-CJG1DgRF.js +725 -0
- package/dist/audit-scene-CJG1DgRF.js.map +1 -0
- package/dist/cli.js +11 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +184 -24
- package/dist/index.js +459 -34
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/audit-scene-D5ZaENGZ.js +0 -425
- package/dist/audit-scene-D5ZaENGZ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { _ as triangleCount, a as countTriangles, c as findBadGeometry, d as batchedTriangles, f as isBatched, g as pathOf, h as meshLabel, i as countDraws, l as findEmptyMeshes, m as isMesh, n as summarizeScene, o as geometryCensus, p as isInstanced, r as findZFighting, s as listMeshes, t as auditScene, u as asObject3D, v as walk } from "./audit-scene-CJG1DgRF.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,
|
|
@@ -86,6 +122,15 @@ const tally = (rows, name, pass) => {
|
|
|
86
122
|
rows.set(name, row);
|
|
87
123
|
};
|
|
88
124
|
const sorted = (rows) => [...rows.values()].sort((a, b) => b.total - a.total);
|
|
125
|
+
/** Triangles one draw makes: its material group's range, times its instances. */
|
|
126
|
+
const drawTriangles = (geometry, object, group) => {
|
|
127
|
+
const mesh = object;
|
|
128
|
+
if (mesh.isMesh !== true) return 0;
|
|
129
|
+
if (isBatched(mesh)) return batchedTriangles(mesh).triangles;
|
|
130
|
+
const range = group;
|
|
131
|
+
const elements = geometry.index?.count ?? geometry.attributes.position?.count ?? 0;
|
|
132
|
+
return (range && typeof range.count === "number" ? Math.floor(Math.max(0, Math.min(range.count, elements - (range.start ?? 0))) / 3) : triangleCount(geometry)) * (isInstanced(mesh) ? mesh.count : 1);
|
|
133
|
+
};
|
|
89
134
|
/** Start counting every draw. Call `end()` after the frame to restore the renderer and read it. */
|
|
90
135
|
const beginDrawLedger = (renderer, options = {}) => {
|
|
91
136
|
if (typeof renderer.renderBufferDirect !== "function") throw new TypeError("three-audit: the draw ledger needs a WebGLRenderer");
|
|
@@ -95,23 +140,89 @@ const beginDrawLedger = (renderer, options = {}) => {
|
|
|
95
140
|
const passes = [];
|
|
96
141
|
const objects = /* @__PURE__ */ new Map();
|
|
97
142
|
const groups = /* @__PURE__ */ new Map();
|
|
143
|
+
const stats = /* @__PURE__ */ new Map();
|
|
144
|
+
const calls = [];
|
|
98
145
|
let renders = 0;
|
|
99
146
|
let current = "direct";
|
|
100
147
|
let draws = 0;
|
|
148
|
+
let fullscreenPasses = 0;
|
|
149
|
+
let fullscreenPixels = 0;
|
|
150
|
+
const statOf = (pass) => {
|
|
151
|
+
let stat = stats.get(pass);
|
|
152
|
+
if (!stat) {
|
|
153
|
+
stat = {
|
|
154
|
+
name: pass,
|
|
155
|
+
draws: 0,
|
|
156
|
+
triangles: 0,
|
|
157
|
+
objects: /* @__PURE__ */ new Set(),
|
|
158
|
+
targets: /* @__PURE__ */ new Set(),
|
|
159
|
+
calls: 0,
|
|
160
|
+
fullscreenCalls: 0
|
|
161
|
+
};
|
|
162
|
+
stats.set(pass, stat);
|
|
163
|
+
}
|
|
164
|
+
return stat;
|
|
165
|
+
};
|
|
166
|
+
const sizeNow = () => {
|
|
167
|
+
const target = renderer.getRenderTarget?.();
|
|
168
|
+
if (target && typeof target.width === "number" && typeof target.height === "number") return {
|
|
169
|
+
width: target.width,
|
|
170
|
+
height: target.height,
|
|
171
|
+
screen: false
|
|
172
|
+
};
|
|
173
|
+
const gl = renderer.getContext?.();
|
|
174
|
+
return target === null && typeof gl?.drawingBufferWidth === "number" ? {
|
|
175
|
+
width: gl.drawingBufferWidth,
|
|
176
|
+
height: gl.drawingBufferHeight ?? 0,
|
|
177
|
+
screen: true
|
|
178
|
+
} : null;
|
|
179
|
+
};
|
|
101
180
|
const restoreRender = patch(renderer, "render", function(scene, camera) {
|
|
102
181
|
const outer = current;
|
|
103
182
|
renders += 1;
|
|
104
183
|
current = scene.name || `render ${renders}`;
|
|
184
|
+
const call = {
|
|
185
|
+
pass: current,
|
|
186
|
+
draws: 0,
|
|
187
|
+
orthographic: camera.isOrthographicCamera === true,
|
|
188
|
+
vertices: 0,
|
|
189
|
+
size: null
|
|
190
|
+
};
|
|
191
|
+
calls.push(call);
|
|
105
192
|
try {
|
|
106
193
|
render.call(this, scene, camera);
|
|
107
194
|
} finally {
|
|
195
|
+
calls.pop();
|
|
108
196
|
current = outer;
|
|
197
|
+
const stat = stats.get(call.pass);
|
|
198
|
+
if (stat) {
|
|
199
|
+
stat.calls += 1;
|
|
200
|
+
if (call.draws === 1 && call.orthographic && call.vertices <= 6) {
|
|
201
|
+
stat.fullscreenCalls += 1;
|
|
202
|
+
fullscreenPasses += 1;
|
|
203
|
+
fullscreenPixels += call.size ? call.size.width * call.size.height : 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
109
206
|
}
|
|
110
207
|
});
|
|
111
208
|
const restoreDraw = patch(renderer, "renderBufferDirect", function(camera, scene, geometry, material, object, group) {
|
|
112
|
-
const
|
|
209
|
+
const shadow = material.isMeshDepthMaterial === true || material.isMeshDistanceMaterial === true;
|
|
210
|
+
const pass = shadow ? "shadow" : current;
|
|
113
211
|
const name = nameOf(object, material);
|
|
114
212
|
if (!passes.includes(pass)) passes.push(pass);
|
|
213
|
+
const stat = statOf(pass);
|
|
214
|
+
const size = sizeNow();
|
|
215
|
+
const drawn = geometry;
|
|
216
|
+
stat.draws += 1;
|
|
217
|
+
stat.triangles += drawTriangles(drawn, object, group);
|
|
218
|
+
stat.objects.add(object);
|
|
219
|
+
if (size) stat.targets.add(`${size.screen ? "screen " : ""}${size.width}×${size.height}`);
|
|
220
|
+
const call = calls.at(-1);
|
|
221
|
+
if (call && !shadow) {
|
|
222
|
+
call.draws += 1;
|
|
223
|
+
call.vertices = drawn.attributes?.position?.count ?? Infinity;
|
|
224
|
+
call.size = size;
|
|
225
|
+
}
|
|
115
226
|
draws += 1;
|
|
116
227
|
tally(objects, name, pass);
|
|
117
228
|
tally(groups, groupOf(name), pass);
|
|
@@ -124,7 +235,20 @@ const beginDrawLedger = (renderer, options = {}) => {
|
|
|
124
235
|
passes,
|
|
125
236
|
groups: sorted(groups),
|
|
126
237
|
objects: sorted(objects),
|
|
127
|
-
draws
|
|
238
|
+
draws,
|
|
239
|
+
passStats: passes.map((pass) => {
|
|
240
|
+
const stat = stats.get(pass);
|
|
241
|
+
return {
|
|
242
|
+
name: pass,
|
|
243
|
+
draws: stat.draws,
|
|
244
|
+
objects: stat.objects.size,
|
|
245
|
+
triangles: stat.triangles,
|
|
246
|
+
targets: [...stat.targets],
|
|
247
|
+
fullscreen: stat.calls > 0 && stat.fullscreenCalls === stat.calls
|
|
248
|
+
};
|
|
249
|
+
}),
|
|
250
|
+
fullscreenPasses,
|
|
251
|
+
fullscreenPixels
|
|
128
252
|
};
|
|
129
253
|
} };
|
|
130
254
|
};
|
|
@@ -132,21 +256,33 @@ const beginDrawLedger = (renderer, options = {}) => {
|
|
|
132
256
|
* Record exactly one frame of an app that renders from `requestAnimationFrame` (three's
|
|
133
257
|
* `setAnimationLoop`, React Three Fiber): from one animation frame to the next. A demand-driven
|
|
134
258
|
* loop that renders nothing that frame gives an empty ledger. Browser only.
|
|
259
|
+
*
|
|
260
|
+
* With `render`, records one call of it instead, frames or not. Without, fails after `timeout`
|
|
261
|
+
* when no frame comes (a background tab), rather than waiting for ever.
|
|
135
262
|
*/
|
|
136
|
-
const recordDrawLedger = (renderer, options = {}) =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
requestAnimationFrame(() => {
|
|
263
|
+
const recordDrawLedger = async (renderer, options = {}) => {
|
|
264
|
+
const { render, timeout = FRAME_TIMEOUT, ...ledgerOptions } = options;
|
|
265
|
+
if (render) {
|
|
266
|
+
const recording = beginDrawLedger(renderer, ledgerOptions);
|
|
142
267
|
try {
|
|
143
|
-
|
|
144
|
-
requestAnimationFrame(() => resolve(recording.end()));
|
|
268
|
+
await render();
|
|
145
269
|
} catch (error) {
|
|
146
|
-
|
|
270
|
+
recording.end();
|
|
271
|
+
throw error;
|
|
147
272
|
}
|
|
148
|
-
|
|
149
|
-
}
|
|
273
|
+
return recording.end();
|
|
274
|
+
}
|
|
275
|
+
const hint = "Bring the tab to the front, or pass { render } to record one call of your own.";
|
|
276
|
+
await nextFrame("recordDrawLedger", timeout, hint);
|
|
277
|
+
const recording = beginDrawLedger(renderer, ledgerOptions);
|
|
278
|
+
try {
|
|
279
|
+
await nextFrame("recordDrawLedger", timeout, hint);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
recording.end();
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
return recording.end();
|
|
285
|
+
};
|
|
150
286
|
/** Rows shaped for `console.table`: one column per pass, then the total. */
|
|
151
287
|
const ledgerTable = (rows, passes) => rows.map((row) => ({
|
|
152
288
|
name: row.name,
|
|
@@ -157,42 +293,314 @@ const ledgerTable = (rows, passes) => rows.map((row) => ({
|
|
|
157
293
|
const printDrawLedger = (ledger) => {
|
|
158
294
|
console.group(`Draw ledger: ${ledger.draws} draws in one frame`);
|
|
159
295
|
console.table(ledgerTable(ledger.groups, ledger.passes));
|
|
296
|
+
console.table(ledger.passStats.map((pass) => ({
|
|
297
|
+
...pass,
|
|
298
|
+
targets: pass.targets.join(", ")
|
|
299
|
+
})));
|
|
300
|
+
if (ledger.fullscreenPasses > 0) console.log(`${ledger.fullscreenPasses} fullscreen passes: ${(ledger.fullscreenPixels / 1e6).toFixed(1)} Mpx shaded`);
|
|
160
301
|
console.groupCollapsed("Every object");
|
|
161
302
|
console.table(ledgerTable(ledger.objects, ledger.passes));
|
|
162
303
|
console.groupEnd();
|
|
163
304
|
console.groupEnd();
|
|
164
305
|
};
|
|
165
306
|
//#endregion
|
|
307
|
+
//#region src/measure.ts
|
|
308
|
+
const multiply = (a, b, out) => {
|
|
309
|
+
for (let row = 0; row < 4; row += 1) for (let column = 0; column < 4; column += 1) {
|
|
310
|
+
let sum = 0;
|
|
311
|
+
for (let k = 0; k < 4; k += 1) sum += a[k * 4 + row] * b[column * 4 + k];
|
|
312
|
+
out[column * 4 + row] = sum;
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
};
|
|
316
|
+
/** Every triangle a mesh draws, in world space: each instance of an instanced or batched mesh. */
|
|
317
|
+
const eachTriangle = (mesh, visit) => {
|
|
318
|
+
const { index, attributes } = mesh.geometry;
|
|
319
|
+
const position = attributes.position;
|
|
320
|
+
if (!position) return;
|
|
321
|
+
const world = mesh.matrixWorld.elements;
|
|
322
|
+
const corner = [
|
|
323
|
+
0,
|
|
324
|
+
0,
|
|
325
|
+
0,
|
|
326
|
+
0,
|
|
327
|
+
0,
|
|
328
|
+
0,
|
|
329
|
+
0,
|
|
330
|
+
0,
|
|
331
|
+
0
|
|
332
|
+
];
|
|
333
|
+
const emit = (matrix, first, count, indexed) => {
|
|
334
|
+
for (let k = first; k + 2 < first + count; k += 3) {
|
|
335
|
+
for (let c = 0; c < 3; c += 1) {
|
|
336
|
+
const v = indexed ? index.getX(k + c) : k + c;
|
|
337
|
+
const x = position.getX(v);
|
|
338
|
+
const y = position.getY(v);
|
|
339
|
+
const z = position.getZ(v);
|
|
340
|
+
for (let axis = 0; axis < 3; axis += 1) corner[c * 3 + axis] = matrix[axis] * x + matrix[axis + 4] * y + matrix[axis + 8] * z + matrix[axis + 12];
|
|
341
|
+
}
|
|
342
|
+
visit(...corner);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
if (isBatched(mesh)) {
|
|
346
|
+
const batch = mesh;
|
|
347
|
+
const instance = Array.from({ length: 16 }, () => 0);
|
|
348
|
+
const matrix = Array.from({ length: 16 }, () => 0);
|
|
349
|
+
const target = { fromArray(array, offset = 0) {
|
|
350
|
+
for (let i = 0; i < 16; i += 1) instance[i] = array[offset + i];
|
|
351
|
+
return target;
|
|
352
|
+
} };
|
|
353
|
+
for (let id = 0; id < batch.maxInstanceCount; id += 1) {
|
|
354
|
+
let range;
|
|
355
|
+
try {
|
|
356
|
+
if (!batch.getVisibleAt(id)) continue;
|
|
357
|
+
range = batch.getGeometryRangeAt(batch.getGeometryIdAt(id));
|
|
358
|
+
batch.getMatrixAt(id, target);
|
|
359
|
+
} catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
multiply(world, instance, matrix);
|
|
363
|
+
if (index) emit(matrix, range.indexStart, range.indexCount, true);
|
|
364
|
+
else emit(matrix, range.vertexStart, range.vertexCount, false);
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const { start, count } = mesh.geometry.drawRange;
|
|
369
|
+
const elements = index?.count ?? position.count;
|
|
370
|
+
const first = Math.min(start, elements);
|
|
371
|
+
const drawn = Math.min(elements, start + count) - first;
|
|
372
|
+
if (isInstanced(mesh)) {
|
|
373
|
+
const matrix = Array.from({ length: 16 }, () => 0);
|
|
374
|
+
const instances = mesh.instanceMatrix.array;
|
|
375
|
+
for (let i = 0; i < mesh.count; i += 1) {
|
|
376
|
+
multiply(world, Array.prototype.slice.call(instances, i * 16, i * 16 + 16), matrix);
|
|
377
|
+
emit(matrix, first, drawn, index !== null);
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
emit(world, first, drawn, index !== null);
|
|
382
|
+
};
|
|
383
|
+
const emptyBox = () => [
|
|
384
|
+
Infinity,
|
|
385
|
+
Infinity,
|
|
386
|
+
Infinity,
|
|
387
|
+
-Infinity,
|
|
388
|
+
-Infinity,
|
|
389
|
+
-Infinity
|
|
390
|
+
];
|
|
391
|
+
const grow = (box, x, y, z) => {
|
|
392
|
+
box[0] = Math.min(box[0], x);
|
|
393
|
+
box[1] = Math.min(box[1], y);
|
|
394
|
+
box[2] = Math.min(box[2], z);
|
|
395
|
+
box[3] = Math.max(box[3], x);
|
|
396
|
+
box[4] = Math.max(box[4], y);
|
|
397
|
+
box[5] = Math.max(box[5], z);
|
|
398
|
+
};
|
|
399
|
+
/** Per axis, how far apart two boxes are (0 where they overlap). */
|
|
400
|
+
const apart = (a, ai, b, bi, out) => {
|
|
401
|
+
for (let axis = 0; axis < 3; axis += 1) out[axis] = Math.max(0, a[ai + axis] - b[bi + axis + 3], b[bi + axis] - a[ai + axis + 3]);
|
|
402
|
+
return Math.hypot(out[0], out[1], out[2]);
|
|
403
|
+
};
|
|
404
|
+
const round = (value) => {
|
|
405
|
+
const rounded = Math.round(value * 1e3) / 1e3;
|
|
406
|
+
return rounded === 0 ? 0 : rounded;
|
|
407
|
+
};
|
|
408
|
+
/** The visible meshes a target stands for. */
|
|
409
|
+
const meshesOf = (root, target, tagKey, role) => {
|
|
410
|
+
let objects;
|
|
411
|
+
if (typeof target === "string") {
|
|
412
|
+
const tagged = [];
|
|
413
|
+
const named = [];
|
|
414
|
+
walk(root, {}, (object) => {
|
|
415
|
+
const tag = object.userData[tagKey];
|
|
416
|
+
if (tag !== void 0 && tag !== null && String(tag) === target) tagged.push(object);
|
|
417
|
+
else if (object.name === target) named.push(object);
|
|
418
|
+
});
|
|
419
|
+
objects = tagged.length > 0 ? tagged : named;
|
|
420
|
+
if (objects.length === 0) throw new Error(`three-audit: ${role}: nothing is tagged userData.${tagKey} = "${target}" or named "${target}"`);
|
|
421
|
+
} else objects = [asObject3D(target)];
|
|
422
|
+
const meshes = /* @__PURE__ */ new Set();
|
|
423
|
+
for (const object of objects) walk(object, { visibleOnly: true }, (child) => {
|
|
424
|
+
if (isMesh(child)) meshes.add(child);
|
|
425
|
+
});
|
|
426
|
+
return meshes;
|
|
427
|
+
};
|
|
428
|
+
/** A target's world envelope, from its visible meshes' vertices; `null` when it draws nothing. */
|
|
429
|
+
const measureBounds = (root, target, options = {}) => {
|
|
430
|
+
const scene = asObject3D(root);
|
|
431
|
+
scene.updateWorldMatrix(true, true);
|
|
432
|
+
const box = emptyBox();
|
|
433
|
+
for (const mesh of meshesOf(scene, target, options.tagKey ?? "studioObject", "bounds")) eachTriangle(mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {
|
|
434
|
+
grow(box, ax, ay, az);
|
|
435
|
+
grow(box, bx, by, bz);
|
|
436
|
+
grow(box, cx, cy, cz);
|
|
437
|
+
});
|
|
438
|
+
if (box[0] > box[3]) return null;
|
|
439
|
+
return {
|
|
440
|
+
min: [
|
|
441
|
+
box[0],
|
|
442
|
+
box[1],
|
|
443
|
+
box[2]
|
|
444
|
+
].map(round),
|
|
445
|
+
max: [
|
|
446
|
+
box[3],
|
|
447
|
+
box[4],
|
|
448
|
+
box[5]
|
|
449
|
+
].map(round),
|
|
450
|
+
size: [
|
|
451
|
+
0,
|
|
452
|
+
1,
|
|
453
|
+
2
|
|
454
|
+
].map((axis) => round(box[axis + 3] - box[axis])),
|
|
455
|
+
centre: [
|
|
456
|
+
0,
|
|
457
|
+
1,
|
|
458
|
+
2
|
|
459
|
+
].map((axis) => round((box[axis] + box[axis + 3]) / 2))
|
|
460
|
+
};
|
|
461
|
+
};
|
|
462
|
+
/** Triangle boxes past this many pairs, a mesh pair is measured by its whole boxes instead. */
|
|
463
|
+
const PAIR_LIMIT = 4e6;
|
|
464
|
+
/**
|
|
465
|
+
* The gap between two targets, and which of their meshes come closest. Throws when a target
|
|
466
|
+
* matches nothing; `null` when either draws nothing. Meshes in both targets are left out of `a`.
|
|
467
|
+
*/
|
|
468
|
+
const measureClearance = (root, a, b, options = {}) => {
|
|
469
|
+
const scene = asObject3D(root);
|
|
470
|
+
const tagKey = options.tagKey ?? "studioObject";
|
|
471
|
+
scene.updateWorldMatrix(true, true);
|
|
472
|
+
const inB = meshesOf(scene, b, tagKey, "clearance b");
|
|
473
|
+
const inA = [...meshesOf(scene, a, tagKey, "clearance a")].filter((mesh) => !inB.has(mesh));
|
|
474
|
+
const measure = (mesh) => {
|
|
475
|
+
const box = emptyBox();
|
|
476
|
+
eachTriangle(mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {
|
|
477
|
+
grow(box, ax, ay, az);
|
|
478
|
+
grow(box, bx, by, bz);
|
|
479
|
+
grow(box, cx, cy, cz);
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
mesh,
|
|
483
|
+
box,
|
|
484
|
+
triangles: null
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
const sideA = inA.map(measure).filter(({ box }) => box[0] <= box[3]);
|
|
488
|
+
const sideB = [...inB].map(measure).filter(({ box }) => box[0] <= box[3]);
|
|
489
|
+
if (sideA.length === 0 || sideB.length === 0) return null;
|
|
490
|
+
const trianglesOf = (side) => {
|
|
491
|
+
if (!side.triangles) {
|
|
492
|
+
const boxes = [];
|
|
493
|
+
eachTriangle(side.mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {
|
|
494
|
+
boxes.push(Math.min(ax, bx, cx), Math.min(ay, by, cy), Math.min(az, bz, cz), Math.max(ax, bx, cx), Math.max(ay, by, cy), Math.max(az, bz, cz));
|
|
495
|
+
});
|
|
496
|
+
side.triangles = Float64Array.from(boxes);
|
|
497
|
+
}
|
|
498
|
+
return side.triangles;
|
|
499
|
+
};
|
|
500
|
+
const axes = [
|
|
501
|
+
0,
|
|
502
|
+
0,
|
|
503
|
+
0
|
|
504
|
+
];
|
|
505
|
+
const pairs = sideA.flatMap((left) => sideB.map((right) => ({
|
|
506
|
+
left,
|
|
507
|
+
right,
|
|
508
|
+
distance: apart(left.box, 0, right.box, 0, axes)
|
|
509
|
+
}))).sort((x, y) => x.distance - y.distance);
|
|
510
|
+
let best = Infinity;
|
|
511
|
+
let bestAxes = [
|
|
512
|
+
0,
|
|
513
|
+
0,
|
|
514
|
+
0
|
|
515
|
+
];
|
|
516
|
+
let between = [pairs[0].left.mesh, pairs[0].right.mesh];
|
|
517
|
+
for (const { left, right, distance } of pairs) {
|
|
518
|
+
if (distance >= best) break;
|
|
519
|
+
const leftTriangles = trianglesOf(left);
|
|
520
|
+
const rightTriangles = trianglesOf(right);
|
|
521
|
+
const near = (triangles, other) => {
|
|
522
|
+
const kept = [];
|
|
523
|
+
for (let t = 0; t < triangles.length; t += 6) if (apart(triangles, t, other, 0, axes) < best) kept.push(t);
|
|
524
|
+
return kept;
|
|
525
|
+
};
|
|
526
|
+
const nearLeft = near(leftTriangles, right.box);
|
|
527
|
+
const nearRight = near(rightTriangles, left.box);
|
|
528
|
+
if (nearLeft.length * nearRight.length > PAIR_LIMIT) {
|
|
529
|
+
if (distance < best) {
|
|
530
|
+
best = distance;
|
|
531
|
+
apart(left.box, 0, right.box, 0, axes);
|
|
532
|
+
bestAxes = [...axes];
|
|
533
|
+
between = [left.mesh, right.mesh];
|
|
534
|
+
}
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
for (const i of nearLeft) for (const j of nearRight) {
|
|
538
|
+
const gap = apart(leftTriangles, i, rightTriangles, j, axes);
|
|
539
|
+
if (gap < best) {
|
|
540
|
+
best = gap;
|
|
541
|
+
bestAxes = [...axes];
|
|
542
|
+
between = [left.mesh, right.mesh];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const separated = bestAxes.flatMap((value, axis) => value > 0 ? [axis] : []);
|
|
547
|
+
return {
|
|
548
|
+
gap: round(best),
|
|
549
|
+
axis: separated.length === 1 ? [
|
|
550
|
+
"x",
|
|
551
|
+
"y",
|
|
552
|
+
"z"
|
|
553
|
+
][separated[0]] : null,
|
|
554
|
+
between: [meshLabel(between[0], scene, tagKey), meshLabel(between[1], scene, tagKey)]
|
|
555
|
+
};
|
|
556
|
+
};
|
|
557
|
+
//#endregion
|
|
166
558
|
//#region src/console-helpers.ts
|
|
559
|
+
const NAMESPACE = "threeAudit";
|
|
167
560
|
const NAMES = [
|
|
168
561
|
"auditRoot",
|
|
562
|
+
"summary",
|
|
169
563
|
"tris",
|
|
170
564
|
"meshes",
|
|
171
565
|
"census",
|
|
172
566
|
"audit",
|
|
173
567
|
"zfight",
|
|
174
568
|
"report",
|
|
569
|
+
"bbox",
|
|
570
|
+
"clearance",
|
|
175
571
|
"ledger",
|
|
572
|
+
"beginLedger",
|
|
176
573
|
"blackFrames"
|
|
177
574
|
];
|
|
178
575
|
const needsRenderer = (name) => () => {
|
|
179
576
|
throw new Error(`three-audit: ${name}() needs installAuditHelpers(scene, { renderer })`);
|
|
180
577
|
};
|
|
181
578
|
/**
|
|
182
|
-
* Put the helpers on `globalThis`. Returns a function that
|
|
579
|
+
* Put the helpers on `globalThis`, and on `globalThis.threeAudit`. Returns a function that
|
|
580
|
+
* removes them.
|
|
183
581
|
*
|
|
184
582
|
* vanilla: installAuditHelpers(scene, { renderer })
|
|
185
583
|
* R3F: const { scene, gl } = useThree();
|
|
186
584
|
* useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);
|
|
187
585
|
*/
|
|
188
586
|
const installAuditHelpers = (root, options = {}) => {
|
|
189
|
-
const { renderer, skip } = options;
|
|
587
|
+
const { renderer, skip, batchLabel } = options;
|
|
190
588
|
const tagKey = options.tagKey ?? "studioObject";
|
|
191
589
|
const helpers = {
|
|
192
590
|
auditRoot: root,
|
|
591
|
+
summary: (tag, summaryOptions = {}) => summarizeScene(root, {
|
|
592
|
+
skip,
|
|
593
|
+
tagKey,
|
|
594
|
+
...summaryOptions,
|
|
595
|
+
...tag === void 0 ? {} : { tag }
|
|
596
|
+
}),
|
|
193
597
|
tris: () => countTriangles(root, { skip }),
|
|
194
598
|
meshes: () => listMeshes(root, { skip }),
|
|
195
|
-
census: () => geometryCensus(root, {
|
|
599
|
+
census: (censusOptions = {}) => geometryCensus(root, {
|
|
600
|
+
skip,
|
|
601
|
+
label: batchLabel,
|
|
602
|
+
...censusOptions
|
|
603
|
+
}),
|
|
196
604
|
audit: () => findBadGeometry(root, {
|
|
197
605
|
skip,
|
|
198
606
|
tagKey
|
|
@@ -203,33 +611,50 @@ const installAuditHelpers = (root, options = {}) => {
|
|
|
203
611
|
...gap === void 0 ? {} : { gap },
|
|
204
612
|
...self === void 0 ? {} : { self }
|
|
205
613
|
}),
|
|
206
|
-
report: () => auditScene(root, {
|
|
614
|
+
report: (tag) => auditScene(root, {
|
|
207
615
|
skip,
|
|
208
|
-
tagKey
|
|
616
|
+
tagKey,
|
|
617
|
+
...tag === void 0 ? {} : { tag }
|
|
209
618
|
}),
|
|
210
|
-
|
|
211
|
-
|
|
619
|
+
bbox: (target) => measureBounds(root, target, { tagKey }),
|
|
620
|
+
clearance: (a, b) => measureClearance(root, a, b, { tagKey }),
|
|
621
|
+
ledger: renderer ? async (ledgerOptions) => {
|
|
622
|
+
const ledger = await recordDrawLedger(renderer, ledgerOptions);
|
|
212
623
|
printDrawLedger(ledger);
|
|
213
624
|
return ledger;
|
|
214
625
|
} : needsRenderer("ledger"),
|
|
215
|
-
|
|
626
|
+
beginLedger: renderer ? (ledgerOptions) => {
|
|
627
|
+
const recording = beginDrawLedger(renderer, ledgerOptions);
|
|
628
|
+
return { end: () => {
|
|
629
|
+
const ledger = recording.end();
|
|
630
|
+
printDrawLedger(ledger);
|
|
631
|
+
return ledger;
|
|
632
|
+
} };
|
|
633
|
+
} : needsRenderer("beginLedger"),
|
|
634
|
+
blackFrames: renderer ? async (seconds = 10) => {
|
|
635
|
+
await nextFrame("blackFrames()", FRAME_TIMEOUT, "Black frames only show in a visible tab: bring it to the front.");
|
|
216
636
|
const watch = watchBlackFrames(renderer);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
637
|
+
return new Promise((resolve) => {
|
|
638
|
+
setTimeout(() => {
|
|
639
|
+
watch.stop();
|
|
640
|
+
resolve({
|
|
641
|
+
frames: watch.frames,
|
|
642
|
+
blackFrames: watch.blackFrames
|
|
643
|
+
});
|
|
644
|
+
}, seconds * 1e3);
|
|
645
|
+
});
|
|
646
|
+
} : needsRenderer("blackFrames")
|
|
225
647
|
};
|
|
226
|
-
|
|
648
|
+
const namespace = { ...helpers };
|
|
649
|
+
const scope = globalThis;
|
|
650
|
+
if (options.globals !== false) Object.assign(scope, helpers);
|
|
651
|
+
scope[NAMESPACE] = namespace;
|
|
227
652
|
return () => {
|
|
228
|
-
const scope = globalThis;
|
|
229
653
|
for (const name of NAMES) if (scope[name] === helpers[name]) delete scope[name];
|
|
654
|
+
if (scope[NAMESPACE] === namespace) delete scope[NAMESPACE];
|
|
230
655
|
};
|
|
231
656
|
};
|
|
232
657
|
//#endregion
|
|
233
|
-
export { auditScene, beginDrawLedger, countTriangles, findBadGeometry, findZFighting, geometryCensus, installAuditHelpers, ledgerTable, listMeshes, printDrawLedger, recordDrawLedger, triangleCount, watchBlackFrames };
|
|
658
|
+
export { auditScene, beginDrawLedger, countDraws, countTriangles, findBadGeometry, findEmptyMeshes, findZFighting, geometryCensus, installAuditHelpers, ledgerTable, listMeshes, measureBounds, measureClearance, printDrawLedger, recordDrawLedger, summarizeScene, triangleCount, watchBlackFrames };
|
|
234
659
|
|
|
235
660
|
//# sourceMappingURL=index.js.map
|