@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/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 triangleCount, t as auditScene } from "./audit-scene-D5ZaENGZ.js";
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
- const nameOf = (object, material) => object.name || `${object.type}:${material.name || material.type}`;
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 pass = material.isMeshDepthMaterial === true || material.isMeshDistanceMaterial === true ? "shadow" : current;
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 = {}) => new Promise((resolve, reject) => {
137
- if (typeof requestAnimationFrame !== "function") {
138
- reject(/* @__PURE__ */ new Error("three-audit: recordDrawLedger needs requestAnimationFrame"));
139
- return;
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
- const recording = beginDrawLedger(renderer, options);
144
- requestAnimationFrame(() => resolve(recording.end()));
268
+ await render();
145
269
  } catch (error) {
146
- reject(error);
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 removes them.
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, { skip }),
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
- ledger: renderer ? async () => {
211
- const ledger = await recordDrawLedger(renderer);
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
- blackFrames: renderer ? (seconds = 10) => new Promise((resolve) => {
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
- setTimeout(() => {
218
- watch.stop();
219
- resolve({
220
- frames: watch.frames,
221
- blackFrames: watch.blackFrames
222
- });
223
- }, seconds * 1e3);
224
- }) : needsRenderer("blackFrames")
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
- Object.assign(globalThis, helpers);
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