@zkmake/three-audit 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,7 +52,7 @@ npx @zkmake/three-audit assets/*.glb # or bunx
52
52
  ```
53
53
 
54
54
  ```
55
- windmill.glb 4,812 triangles
55
+ windmill.glb 4,812 triangles, 14 draws
56
56
  ✗ 1 z-fighting pair
57
57
  windmill/plinth [stone] @ (0, 0.1, 0)
58
58
  ↔ windmill/wall [plaster] @ (0, 0.5, 0)
@@ -104,16 +104,29 @@ useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl])
104
104
 
105
105
  | Global | Returns |
106
106
  | --------------------- | ----------------------------------------------------------------------------------- |
107
+ | `summary(tag?)` | One count per check: the call to run after every edit |
107
108
  | `tris()` | Triangles one pass draws: visible meshes, instances and batches counted, no culling |
108
109
  | `meshes()` | Every visible mesh: triangles, instances, total, most first |
109
- | `census()` | Geometries by triangles drawn, hidden meshes included |
110
+ | `census({ budget? })` | Geometries by triangles drawn, batches broken down, hidden meshes included |
110
111
  | `audit()` | Meshes with NaN positions or normals, or zero-length normals |
111
112
  | `zfight(gap?, self?)` | Pairs of meshes that z-fight |
112
- | `report()` | All of the above in one JSON-safe object |
113
- | `ledger(options?)` | One frame's draw calls by object and pass, printed as tables (WebGL) |
113
+ | `report(tag?)` | Every check in one JSON-safe object |
114
+ | `bbox(target)` | An object's world envelope: min, max, size, centre |
115
+ | `clearance(a, b)` | The gap between two objects, and which of their meshes come closest |
116
+ | `ledger(options?)` | One frame's draw calls by object and pass, and each pass's size (WebGL) |
114
117
  | `beginLedger()` | `{ end() }`: draw calls between the two, for any span you choose (WebGL) |
115
118
  | `blackFrames(10)` | Frames in the next 10 s that came out black (WebGL) |
116
119
 
120
+ A `tag` or `target` is a `userData.studioObject` value (or your `tagKey`), falling back to an
121
+ object's name; `bbox` and `clearance` also take the object itself.
122
+
123
+ ```js
124
+ threeAudit.summary("bridge");
125
+ // { triangles: 4812, draws: 14, meshes: 9, zFighting: 0, selfZFighting: 0, badGeometry: 0, emptyMeshes: 0 }
126
+ threeAudit.clearance("train", "tower");
127
+ // { gap: 0.15, axis: "y", between: ["train/car [paint]", "tower/crossbar [steel]"] }
128
+ ```
129
+
117
130
  Each one is also on `threeAudit` (`threeAudit.report()`). Where short names like `report` collide
118
131
  with other tooling, `installAuditHelpers(scene, { globals: false })` installs only `threeAudit`.
119
132
 
@@ -146,7 +159,7 @@ into one row with a `count`.
146
159
 
147
160
  Left out because they can't fight:
148
161
 
149
- - hidden objects
162
+ - hidden objects, except a bake's parts (see "Baked groups" below)
150
163
  - instanced and batched meshes, and `InstancedBufferGeometry`, which a shader positions. Check
151
164
  their source parts as plain meshes.
152
165
  - pairs where neither material writes depth, or one ignores depth entirely. A decal that doesn't
@@ -161,11 +174,45 @@ between each other instead of over each other's ends.
161
174
  Vertices with a non-finite position or normal, and zero-length normals on triangles with area.
162
175
  Either lights a pixel NaN, and bloom smears one NaN pixel into a black block.
163
176
 
164
- ### `countTriangles`, `listMeshes`, `geometryCensus`
177
+ ### `summarizeScene(root, options?)`
178
+
179
+ Every check as one count: `{ triangles, draws, meshes, zFighting, selfZFighting, badGeometry,
180
+ emptyMeshes }`. `tag` narrows it to one tagged object. `zFighting: false` skips the slowest check
181
+ (its counts come back `null`) for a quick look at a large scene.
182
+
183
+ ### `countTriangles`, `listMeshes`, `geometryCensus`, `countDraws`
165
184
 
166
185
  Where a triangle budget goes. Name meshes and geometries (`mesh.name`, `geometry.name`) so the rows
167
186
  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.
187
+ or type when it has none (`CharacterHands/left/BoxGeometry`).
188
+
189
+ `geometryCensus` lists a `BatchedMesh` one geometry at a time: visible instances × triangles each,
190
+ which is where a batch's budget actually goes. Rows are `batch#id` unless `label(batch, geometryId)`
191
+ names them from the app's own records. Each row has its `share` of the total; with `budget: 0.2`,
192
+ rows over 20% get `overBudget: true`.
193
+
194
+ ```
195
+ geometry uses triangles total share overBudget
196
+ treadmill_belt 570 513 292,410 0.306 true
197
+ treadmill_coupler 570 297 169,290 0.177 false
198
+ ```
199
+
200
+ `countDraws` counts the draw calls one main pass makes: one per visible mesh, line or point cloud,
201
+ one per material group when a mesh has several. It doesn't cull and doesn't count shadows; the draw
202
+ ledger counts the real thing.
203
+
204
+ ### `findEmptyMeshes(root)`
205
+
206
+ Meshes with no vertices, such as what merging nothing leaves. Each one still costs a draw call and
207
+ a geometry to dispose. A zero draw range doesn't count, since that's usually a pool waiting to fill.
208
+
209
+ ### `measureBounds(root, target)` / `measureClearance(root, a, b)`
210
+
211
+ An object's world envelope, and the smallest gap between two objects, from their vertices as drawn:
212
+ instances and batches included, rotations exact. The gap is measured between triangles' bounding
213
+ boxes, which is exact for faces square to the axes and slightly under for sloped ones. So a tower
214
+ merged into one mesh still measures to its crossbar, not to the box around the whole tower. `axis`
215
+ says which way the gap runs when it's straight along one axis. Draw ranges are respected; a shadow pass adds roughly as much again per light.
169
216
 
170
217
  ### `recordDrawLedger(renderer)` / `beginDrawLedger(renderer)`
171
218
 
@@ -178,6 +225,12 @@ to its first `:` or `#`), and by pass: `shadow`, then each `render()` call by sc
178
225
  frame comes. `{ render }` records one call of your own render instead, frames or not.
179
226
  `beginDrawLedger` returns `{ end() }` for any span. `printDrawLedger` prints the tables.
180
227
 
228
+ `passStats` sizes each pass: draws, distinct objects (for `shadow`, the casters), triangles, and
229
+ the targets drawn to in device pixels (`screen 2880×1800` for the canvas). A pass is `fullscreen`
230
+ when each call drew one small mesh through an orthographic camera, the shape of a post-processing
231
+ pass. `fullscreenPasses` and `fullscreenPixels` total them: 8 fullscreen passes at 2880×1800 shade
232
+ 41 Mpx a frame before the scene itself draws a pixel.
233
+
181
234
  ### `watchBlackFrames(renderer)`
182
235
 
183
236
  After each render to the screen, reads a 3 × 3 grid of pixels and counts frames where three or more
@@ -190,6 +243,18 @@ stalls the GPU, so watch, read `blackFrames`, `stop()`.
190
243
  non-enumerable property (`row.mesh`, `row.meshes`), so they print and serialise without dragging the
191
244
  scene along.
192
245
 
246
+ ## Baked groups
247
+
248
+ [`@zkmake/three-batch`](../three-batch)'s `bake` and `<Baked>` hide the parts and draw one merged
249
+ mesh per material. For z-fighting, NaN geometry, empty meshes, `bbox` and `clearance`,
250
+ three-audit checks the parts instead of the merge by default (`bakes: "sources"`). A row then
251
+ names the deck and the rail rather than `bridge:steel`, and a part is never compared with its own
252
+ merged copy. Parts are found by three-batch's `userData.bakeSource` flag. For bakes from before
253
+ that flag, they're the hidden objects inside anything holding a `userData.bakedResult` merge.
254
+ `bakes: "merged"` checks what's drawn instead.
255
+
256
+ Costs (`tris`, `meshes`, `census`, draws) always count what's drawn: the merge.
257
+
193
258
  ## Types
194
259
 
195
260
  The checks take any object with three's `isObject3D` flag, and the renderer as only the methods
@@ -1,17 +1,32 @@
1
1
  //#region src/scene.ts
2
2
  /** The object as three's own type, for the walk. */
3
3
  const asObject3D = (object) => object;
4
+ /**
5
+ * A part a bake hid in favour of its merge: flagged `userData.bakeSource` by three-batch, or (for
6
+ * bakes from before that flag) inside an object that holds a `userData.bakedResult` merge.
7
+ */
8
+ const isBakeSource = (object) => {
9
+ if (object.userData.bakeSource === true) return true;
10
+ for (let at = object.parent; at; at = at.parent) if (at.children.some((child) => child.userData.bakedResult === true)) return true;
11
+ return false;
12
+ };
4
13
  /** Depth-first, in child order. A skipped (or, with `visibleOnly`, hidden) object prunes its subtree. */
5
14
  const walk = (root, options, visit) => {
6
15
  const stack = [root];
16
+ const sources = options.bakes === "sources";
7
17
  while (stack.length > 0) {
8
18
  const object = stack.pop();
9
- if (options.visibleOnly === true && !object.visible || options.skip?.(object) === true) continue;
19
+ if (sources && object.userData.bakedResult === true || options.visibleOnly === true && !object.visible && !(sources && isBakeSource(object)) || options.skip?.(object) === true) continue;
10
20
  visit(object);
11
21
  for (let i = object.children.length - 1; i >= 0; i -= 1) stack.push(object.children[i]);
12
22
  }
13
23
  };
14
24
  const isMesh = (object) => object.isMesh === true;
25
+ /** Anything the renderer draws: a mesh, line, point cloud or sprite. */
26
+ const isDrawable = (object) => {
27
+ const flags = object;
28
+ return flags.isMesh === true || flags.isLine === true || flags.isPoints === true || flags.isSprite === true;
29
+ };
15
30
  const isInstanced = (mesh) => mesh.isInstancedMesh === true;
16
31
  const isBatched = (mesh) => mesh.isBatchedMesh === true;
17
32
  /** Triangles one draw of this geometry makes: indexed or not, within its draw range. */
@@ -21,10 +36,12 @@ const triangleCount = (geometry) => {
21
36
  const drawn = Math.min(elements, start + count) - start;
22
37
  return Math.max(0, Math.floor(drawn / 3));
23
38
  };
24
- /** Triangles a batch draws across its visible instances, and how many instances that is. */
25
- const batchedTriangles = (batch) => {
26
- let triangles = 0;
27
- let instances = 0;
39
+ /**
40
+ * A batch's visible instances by the geometry each draws: how many, and that geometry's triangles
41
+ * (its range in the shared buffer). In first-drawn order.
42
+ */
43
+ const batchedGeometries = (batch) => {
44
+ const geometries = /* @__PURE__ */ new Map();
28
45
  for (let id = 0; id < batch.maxInstanceCount; id += 1) {
29
46
  let geometryId;
30
47
  try {
@@ -33,10 +50,27 @@ const batchedTriangles = (batch) => {
33
50
  } catch {
34
51
  continue;
35
52
  }
53
+ const known = geometries.get(geometryId);
54
+ if (known) {
55
+ known.instances += 1;
56
+ continue;
57
+ }
36
58
  const range = batch.getGeometryRangeAt(geometryId);
37
59
  const elements = range.count ?? (batch.geometry.index ? range.indexCount : range.vertexCount);
38
- triangles += Math.floor(elements / 3);
39
- instances += 1;
60
+ geometries.set(geometryId, {
61
+ instances: 1,
62
+ triangles: Math.floor(elements / 3)
63
+ });
64
+ }
65
+ return geometries;
66
+ };
67
+ /** Triangles a batch draws across its visible instances, and how many instances that is. */
68
+ const batchedTriangles = (batch) => {
69
+ let triangles = 0;
70
+ let instances = 0;
71
+ for (const geometry of batchedGeometries(batch).values()) {
72
+ triangles += geometry.triangles * geometry.instances;
73
+ instances += geometry.instances;
40
74
  }
41
75
  return {
42
76
  instances,
@@ -51,6 +85,24 @@ const tagOf = (object, tagKey) => {
51
85
  }
52
86
  return null;
53
87
  };
88
+ /**
89
+ * A skip that keeps only what's tagged `tag`: prunes objects tagged otherwise, and meshes outside
90
+ * any object tagged `tag`. Throws when nothing under `root` carries the tag.
91
+ */
92
+ const onlyTag = (root, tag, tagKey, skip) => {
93
+ let found = false;
94
+ walk(root, { skip }, (object) => {
95
+ const own = object.userData[tagKey];
96
+ found ||= own !== void 0 && own !== null && String(own) === tag;
97
+ });
98
+ if (!found) throw new Error(`three-audit: nothing is tagged userData.${tagKey} = "${tag}"`);
99
+ return (object) => {
100
+ if (skip?.(object) === true) return true;
101
+ const own = object.userData[tagKey];
102
+ if (own !== void 0 && own !== null && String(own) !== tag) return true;
103
+ return isDrawable(object) && tagOf(object, tagKey) !== tag;
104
+ };
105
+ };
54
106
  const hasTag = (root, tagKey, skip) => {
55
107
  let found = false;
56
108
  walk(root, { skip }, (object) => {
@@ -131,7 +183,10 @@ const check = (geometry) => {
131
183
  const findBadGeometry = (root, options = {}) => {
132
184
  const tagKey = options.tagKey ?? "studioObject";
133
185
  const rows = [];
134
- walk(asObject3D(root), { skip: options.skip }, (object) => {
186
+ walk(asObject3D(root), {
187
+ skip: options.skip,
188
+ bakes: options.bakes ?? "sources"
189
+ }, (object) => {
135
190
  if (!isMesh(object) || !object.geometry.attributes.position) return;
136
191
  const { nonFinite, zeroNormals } = check(object.geometry);
137
192
  if (nonFinite > 0 || zeroNormals > 0) {
@@ -146,6 +201,22 @@ const findBadGeometry = (root, options = {}) => {
146
201
  });
147
202
  return rows;
148
203
  };
204
+ /**
205
+ * Meshes with no vertices, hidden ones included: what a merge of nothing leaves behind. They draw
206
+ * nothing, but each one is still a draw call and a leak to dispose. A draw range of zero doesn't
207
+ * count: that's usually a pool waiting to fill.
208
+ */
209
+ const findEmptyMeshes = (root, options = {}) => {
210
+ const scene = asObject3D(root);
211
+ const rows = [];
212
+ walk(scene, {
213
+ skip: options.skip,
214
+ bakes: options.bakes ?? "sources"
215
+ }, (object) => {
216
+ if (isMesh(object) && (object.geometry.attributes.position?.count ?? 0) === 0) rows.push(withHidden({ mesh: meshLabel(object, scene, options.tagKey ?? "studioObject") }, "object", object));
217
+ });
218
+ return rows;
219
+ };
149
220
  //#endregion
150
221
  //#region src/triangles.ts
151
222
  const cost = (mesh) => {
@@ -193,28 +264,62 @@ const listMeshes = (scene, options = {}) => {
193
264
  };
194
265
  /**
195
266
  * Geometries by triangles drawn, hidden meshes included, most first. Name geometries
196
- * (`geometry.name`) to make this readable. Batches are left out: their shared buffer is not one
197
- * geometry; `listMeshes` counts them.
267
+ * (`geometry.name`) to make this readable. A batch counts each geometry in it separately, by its
268
+ * visible instances: the row a budget is really spent on.
198
269
  */
199
270
  const geometryCensus = (root, options = {}) => {
271
+ const scene = asObject3D(root);
200
272
  const rows = /* @__PURE__ */ new Map();
201
- walk(asObject3D(root), { skip: options.skip }, (object) => {
202
- if (!isMesh(object) || isBatched(object) || !object.geometry.attributes.position) return;
203
- const geometry = object.geometry;
204
- const key = geometry.name || `${geometry.type}:${geometry.uuid.slice(0, 6)}`;
205
- const triangles = triangleCount(geometry);
206
- const uses = isInstanced(object) ? object.count : 1;
273
+ const add = (key, triangles, uses) => {
207
274
  const row = rows.get(key) ?? {
208
275
  geometry: key,
209
276
  uses: 0,
210
277
  triangles,
211
- total: 0
278
+ total: 0,
279
+ share: 0
212
280
  };
213
281
  row.uses += uses;
214
282
  row.total += triangles * uses;
215
283
  rows.set(key, row);
284
+ };
285
+ walk(scene, { skip: options.skip }, (object) => {
286
+ if (!isMesh(object) || !object.geometry.attributes.position) return;
287
+ if (isBatched(object)) {
288
+ const name = [...pathOf(object, scene), meshName(object)].join("/");
289
+ for (const [id, { instances, triangles }] of batchedGeometries(object)) add(options.label?.(object, id) ?? `${name}#${id}`, triangles, instances);
290
+ return;
291
+ }
292
+ const geometry = object.geometry;
293
+ add(geometry.name || `${geometry.type}:${geometry.uuid.slice(0, 6)}`, triangleCount(geometry), isInstanced(object) ? object.count : 1);
216
294
  });
217
- return [...rows.values()].sort((a, b) => b.total - a.total);
295
+ const all = [...rows.values()];
296
+ const sum = all.reduce((total, row) => total + row.total, 0);
297
+ for (const row of all) {
298
+ row.share = sum > 0 ? Math.round(row.total / sum * 1e3) / 1e3 : 0;
299
+ if (options.budget !== void 0) row.overBudget = row.total / Math.max(sum, 1) > options.budget;
300
+ }
301
+ return all.sort((a, b) => b.total - a.total);
302
+ };
303
+ /**
304
+ * Draw calls one main pass makes: one per visible mesh, line or point cloud, or one per material
305
+ * group for a mesh with several materials. Before frustum culling; each shadow-casting light adds
306
+ * one per caster. `recordDrawLedger` counts the real thing.
307
+ */
308
+ const countDraws = (root, options = {}) => {
309
+ let draws = 0;
310
+ walk(asObject3D(root), {
311
+ skip: options.skip,
312
+ visibleOnly: true
313
+ }, (object) => {
314
+ if (!isDrawable(object)) return;
315
+ const drawable = object;
316
+ const material = drawable.material;
317
+ if (Array.isArray(material)) {
318
+ const groups = drawable.geometry?.groups ?? [];
319
+ draws += groups.filter((group) => material[group.materialIndex ?? 0]?.visible !== false).length;
320
+ } else if (material?.visible !== false) draws += 1;
321
+ });
322
+ return draws;
218
323
  };
219
324
  //#endregion
220
325
  //#region src/z-fighting.ts
@@ -261,7 +366,8 @@ const findZFighting = (scene, options = {}) => {
261
366
  const keyOf = (x, y, z, d) => `${x},${y},${z},${d}`;
262
367
  walk(root, {
263
368
  skip: options.skip,
264
- visibleOnly: true
369
+ visibleOnly: true,
370
+ bakes: options.bakes ?? "sources"
265
371
  }, (object) => {
266
372
  if (!isMesh(object) || isInstanced(object) || isBatched(object) || object.geometry.isInstancedBufferGeometry === true || !object.geometry.attributes.position) return;
267
373
  if (tagged && tagOf(object, tagKey) === null) return;
@@ -569,23 +675,71 @@ const findZFighting = (scene, options = {}) => {
569
675
  };
570
676
  //#endregion
571
677
  //#region src/audit-scene.ts
678
+ /** The skip every check runs with: the caller's, narrowed to `tag` when given. */
679
+ const scopeOf = (root, options) => options.tag === void 0 ? options.skip : onlyTag(asObject3D(root), options.tag, options.tagKey ?? "studioObject", options.skip);
572
680
  /**
573
681
  * Every check at once, JSON-safe: what a test asserts on or a headless browser prints. A clean
574
- * scene has `zFighting` and `badGeometry` both `[]`.
682
+ * scene has `zFighting`, `badGeometry` and `emptyMeshes` all `[]`.
575
683
  */
576
684
  const auditScene = (root, options = {}) => {
577
- const { skip, tagKey } = options;
685
+ const { tagKey, bakes } = options;
686
+ const skip = scopeOf(root, options);
687
+ const keyed = {
688
+ skip,
689
+ bakes,
690
+ ...tagKey === void 0 ? {} : { tagKey }
691
+ };
578
692
  return {
579
693
  triangles: countTriangles(root, { skip }),
580
- zFighting: findZFighting(root, options),
581
- badGeometry: findBadGeometry(root, tagKey === void 0 ? { skip } : {
694
+ draws: countDraws(root, { skip }),
695
+ zFighting: findZFighting(root, {
696
+ ...options,
582
697
  skip,
583
- tagKey
698
+ ...options.tag === void 0 ? {} : { tagged: true }
584
699
  }),
700
+ badGeometry: findBadGeometry(root, keyed),
701
+ emptyMeshes: findEmptyMeshes(root, keyed),
585
702
  meshes: listMeshes(root, { skip }).slice(0, options.top ?? 10)
586
703
  };
587
704
  };
705
+ /**
706
+ * Every check as one count each: the single call to run after every edit. `0` everywhere (and
707
+ * `triangles`, `draws` where you expect them) is a pass. Use `auditScene` to see what failed.
708
+ */
709
+ const summarizeScene = (root, options = {}) => {
710
+ const { tagKey, bakes } = options;
711
+ const skip = scopeOf(root, options);
712
+ const keyed = {
713
+ skip,
714
+ bakes,
715
+ ...tagKey === void 0 ? {} : { tagKey }
716
+ };
717
+ let zFighting = null;
718
+ let selfZFighting = null;
719
+ if (options.zFighting !== false) {
720
+ zFighting = 0;
721
+ selfZFighting = 0;
722
+ for (const row of findZFighting(root, {
723
+ ...options.gap === void 0 ? {} : { gap: options.gap },
724
+ ...tagKey === void 0 ? {} : { tagKey },
725
+ ...options.tag === void 0 ? {} : { tagged: true },
726
+ skip,
727
+ bakes,
728
+ self: true
729
+ })) if (row.meshes[0] === row.meshes[1]) selfZFighting += row.count;
730
+ else zFighting += row.count;
731
+ }
732
+ return {
733
+ triangles: countTriangles(root, { skip }),
734
+ draws: countDraws(root, { skip }),
735
+ meshes: listMeshes(root, { skip }).length,
736
+ zFighting,
737
+ selfZFighting,
738
+ badGeometry: findBadGeometry(root, keyed).length,
739
+ emptyMeshes: findEmptyMeshes(root, keyed).length
740
+ };
741
+ };
588
742
  //#endregion
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 };
743
+ export { triangleCount as _, countTriangles as a, findBadGeometry as c, batchedTriangles as d, isBatched as f, pathOf as g, meshLabel as h, countDraws as i, findEmptyMeshes as l, isMesh as m, summarizeScene as n, geometryCensus as o, isInstanced as p, findZFighting as r, listMeshes as s, auditScene as t, asObject3D as u, walk as v };
590
744
 
591
- //# sourceMappingURL=audit-scene-BYsJcco9.js.map
745
+ //# sourceMappingURL=audit-scene-Da-8SA1x.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-scene-Da-8SA1x.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\n/**\n * What a check sees of a bake (`@zkmake/three-batch`'s `bake` and `<Baked>`), which hides its\n * parts and draws one merged mesh per material instead. `sources`: the parts, as if shown, and not\n * the merge, so rows name real parts. `merged`: what the renderer draws.\n */\nexport type Bakes = \"sources\" | \"merged\";\n\nexport type WalkOptions = {\n skip?: Skip | undefined;\n /** Prune hidden objects and their subtrees, as the renderer does. */\n visibleOnly?: boolean;\n /** With `sources`: prune bake results, and walk the parts a bake hid as though visible. */\n bakes?: Bakes | undefined;\n};\n\n/**\n * A part a bake hid in favour of its merge: flagged `userData.bakeSource` by three-batch, or (for\n * bakes from before that flag) inside an object that holds a `userData.bakedResult` merge.\n */\nexport const isBakeSource = (object: Object3D) => {\n if (object.userData.bakeSource === true) {\n return true;\n }\n\n for (let at = object.parent; at; at = at.parent) {\n if (at.children.some((child) => child.userData.bakedResult === true)) {\n return true;\n }\n }\n\n return false;\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 const sources = options.bakes === \"sources\";\n\n while (stack.length > 0) {\n const object = stack.pop()!;\n\n if (\n (sources && object.userData.bakedResult === true) ||\n (options.visibleOnly === true && !object.visible && !(sources && isBakeSource(object))) ||\n options.skip?.(object) === true\n ) {\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\n/** Anything the renderer draws: a mesh, line, point cloud or sprite. */\nexport const isDrawable = (object: Object3D) => {\n const flags = object as Partial<Record<\"isMesh\" | \"isLine\" | \"isPoints\" | \"isSprite\", boolean>>;\n\n return (\n flags.isMesh === true ||\n flags.isLine === true ||\n flags.isPoints === true ||\n flags.isSprite === true\n );\n};\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/**\n * A batch's visible instances by the geometry each draws: how many, and that geometry's triangles\n * (its range in the shared buffer). In first-drawn order.\n */\nexport const batchedGeometries = (batch: BatchedMesh) => {\n const geometries = new Map<number, { instances: number; triangles: number }>();\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 known = geometries.get(geometryId);\n\n if (known) {\n known.instances += 1;\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 geometries.set(geometryId, { instances: 1, triangles: Math.floor(elements / 3) });\n }\n\n return geometries;\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 (const geometry of batchedGeometries(batch).values()) {\n triangles += geometry.triangles * geometry.instances;\n instances += geometry.instances;\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\n/**\n * A skip that keeps only what's tagged `tag`: prunes objects tagged otherwise, and meshes outside\n * any object tagged `tag`. Throws when nothing under `root` carries the tag.\n */\nexport const onlyTag = (root: Object3D, tag: string, tagKey: string, skip?: Skip): Skip => {\n let found = false;\n\n walk(root, { skip }, (object) => {\n const own: unknown = object.userData[tagKey];\n\n found ||= own !== undefined && own !== null && String(own) === tag;\n });\n\n if (!found) {\n throw new Error(`three-audit: nothing is tagged userData.${tagKey} = \"${tag}\"`);\n }\n\n return (object) => {\n if (skip?.(object) === true) {\n return true;\n }\n\n const own: unknown = object.userData[tagKey];\n\n if (own !== undefined && own !== null && String(own) !== tag) {\n return true;\n }\n\n return isDrawable(object) && tagOf(object, tagKey) !== tag;\n };\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 type Bakes,\n isMesh,\n meshLabel,\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 /** A bake's parts (default), or its merged meshes: see `Bakes`. Not both, or each counts twice. */\n bakes?: Bakes | undefined;\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, bakes: options.bakes ?? \"sources\" }, (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\nexport type EmptyMeshRow = {\n /** `path/to/mesh [material]`. */\n mesh: string;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly object: Mesh;\n};\n\n/**\n * Meshes with no vertices, hidden ones included: what a merge of nothing leaves behind. They draw\n * nothing, but each one is still a draw call and a leak to dispose. A draw range of zero doesn't\n * count: that's usually a pool waiting to fill.\n */\nexport const findEmptyMeshes = (\n root: AnyObject3D,\n options: BadGeometryOptions = {},\n): EmptyMeshRow[] => {\n const scene = asObject3D(root);\n const rows: EmptyMeshRow[] = [];\n\n walk(scene, { skip: options.skip, bakes: options.bakes ?? \"sources\" }, (object) => {\n if (isMesh(object) && (object.geometry.attributes.position?.count ?? 0) === 0) {\n rows.push(\n withHidden(\n { mesh: meshLabel(object, scene, options.tagKey ?? \"studioObject\") },\n \"object\",\n object,\n ),\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 { BatchedMesh, Mesh } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n batchedGeometries,\n batchedTriangles,\n isBatched,\n isDrawable,\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 is an average: `geometryCensus` breaks it down. */\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 /**\n * `geometry.name`, or its type and the start of its uuid. For a geometry in a batch, the batch's\n * `label` for it, or `batch#id`.\n */\n geometry: string;\n /** Copies drawn: every mesh using it, instances counted. */\n uses: number;\n triangles: number;\n total: number;\n /** Its part of the census's total, 0–1. */\n share: number;\n /** With `budget`: whether `share` is over it. */\n overBudget?: boolean;\n};\n\nexport type CensusOptions = TriangleOptions & {\n /**\n * Names a geometry inside a batch, e.g. from the app's own table of what it added. Default\n * `batch#id`, the batch named by its path.\n */\n label?: ((batch: BatchedMesh, geometryId: number) => string | undefined) | undefined;\n /** Flag rows whose share of the total is over this, 0–1: `0.2` for anything over 20%. */\n budget?: number | undefined;\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. A batch counts each geometry in it separately, by its\n * visible instances: the row a budget is really spent on.\n */\nexport const geometryCensus = (root: AnyObject3D, options: CensusOptions = {}): GeometryRow[] => {\n const scene = asObject3D(root);\n const rows = new Map<string, GeometryRow>();\n const add = (key: string, triangles: number, uses: number) => {\n const row = rows.get(key) ?? { geometry: key, uses: 0, triangles, total: 0, share: 0 };\n\n row.uses += uses;\n row.total += triangles * uses;\n rows.set(key, row);\n };\n\n walk(scene, { skip: options.skip }, (object) => {\n if (!isMesh(object) || !object.geometry.attributes.position) {\n return;\n }\n\n if (isBatched(object)) {\n const name = [...pathOf(object, scene), meshName(object)].join(\"/\");\n\n for (const [id, { instances, triangles }] of batchedGeometries(object)) {\n add(options.label?.(object, id) ?? `${name}#${id}`, triangles, instances);\n }\n\n return;\n }\n\n const geometry = object.geometry;\n\n add(\n geometry.name || `${geometry.type}:${geometry.uuid.slice(0, 6)}`,\n triangleCount(geometry),\n isInstanced(object) ? object.count : 1,\n );\n });\n\n const all = [...rows.values()];\n const sum = all.reduce((total, row) => total + row.total, 0);\n\n for (const row of all) {\n row.share = sum > 0 ? Math.round((row.total / sum) * 1000) / 1000 : 0;\n\n if (options.budget !== undefined) {\n row.overBudget = row.total / Math.max(sum, 1) > options.budget;\n }\n }\n\n return all.sort((a, b) => b.total - a.total);\n};\n\n/**\n * Draw calls one main pass makes: one per visible mesh, line or point cloud, or one per material\n * group for a mesh with several materials. Before frustum culling; each shadow-casting light adds\n * one per caster. `recordDrawLedger` counts the real thing.\n */\nexport const countDraws = (root: AnyObject3D, options: TriangleOptions = {}) => {\n let draws = 0;\n\n walk(asObject3D(root), { skip: options.skip, visibleOnly: true }, (object) => {\n if (!isDrawable(object)) {\n return;\n }\n\n const drawable = object as Partial<Mesh>;\n\n const material = drawable.material;\n\n if (Array.isArray(material)) {\n const groups = drawable.geometry?.groups ?? [];\n\n draws += groups.filter(\n (group) => material[group.materialIndex ?? 0]?.visible !== false,\n ).length;\n } else if (material?.visible !== false) {\n draws += 1;\n }\n });\n\n return draws;\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 * A bake's hidden parts are checked in place of its merge (`bakes`). Left out, as they can't\n * fight: other 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 type Bakes,\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 * A bake's parts (default), or its merged meshes: see `Bakes`. The parts name what to fix; the\n * merge is the same faces under one name.\n */\n bakes?: Bakes | 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(\n root,\n { skip: options.skip, visibleOnly: true, bakes: options.bakes ?? \"sources\" },\n (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\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 {\n type BadGeometryRow,\n type EmptyMeshRow,\n findBadGeometry,\n findEmptyMeshes,\n} from \"./bad-geometry.ts\";\nimport { type AnyObject3D, asObject3D, onlyTag, type Skip } from \"./scene.ts\";\nimport { countDraws, countTriangles, listMeshes, type MeshRow } from \"./triangles.ts\";\nimport { findZFighting, type ZFightingOptions, type ZFightingRow } from \"./z-fighting.ts\";\n\nexport type AuditSceneOptions = Pick<ZFightingOptions, \"bakes\" | \"gap\" | \"self\" | \"tagged\"> & {\n skip?: Skip | undefined;\n tagKey?: string;\n /** Only the object(s) tagged this (`userData[tagKey]`): one studio object out of a scene. */\n tag?: string | undefined;\n /** How many of the costliest meshes to list. */\n top?: number;\n};\n\nexport type AuditReport = {\n triangles: number;\n /** Draw calls one main pass makes, before culling and shadows: see `countDraws`. */\n draws: number;\n zFighting: ZFightingRow[];\n badGeometry: BadGeometryRow[];\n emptyMeshes: EmptyMeshRow[];\n /** The costliest meshes, most triangles first. */\n meshes: MeshRow[];\n};\n\n/** The skip every check runs with: the caller's, narrowed to `tag` when given. */\nconst scopeOf = (\n root: AnyObject3D,\n options: { skip?: Skip | undefined; tag?: string | undefined; tagKey?: string },\n) =>\n options.tag === undefined\n ? options.skip\n : onlyTag(asObject3D(root), options.tag, options.tagKey ?? \"studioObject\", options.skip);\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`, `badGeometry` and `emptyMeshes` all `[]`.\n */\nexport const auditScene = (root: AnyObject3D, options: AuditSceneOptions = {}): AuditReport => {\n const { tagKey, bakes } = options;\n const skip = scopeOf(root, options);\n const keyed = { skip, bakes, ...(tagKey === undefined ? {} : { tagKey }) };\n\n return {\n triangles: countTriangles(root, { skip }),\n draws: countDraws(root, { skip }),\n zFighting: findZFighting(root, {\n ...options,\n skip,\n ...(options.tag === undefined ? {} : { tagged: true }),\n }),\n badGeometry: findBadGeometry(root, keyed),\n emptyMeshes: findEmptyMeshes(root, keyed),\n meshes: listMeshes(root, { skip }).slice(0, options.top ?? 10),\n };\n};\n\nexport type AuditSummaryOptions = Pick<\n AuditSceneOptions,\n \"bakes\" | \"gap\" | \"skip\" | \"tag\" | \"tagKey\"\n> & {\n /** Run the z-fighting check, the slow one. Default true; false gives `null` counts. */\n zFighting?: boolean;\n};\n\nexport type AuditSummary = {\n triangles: number;\n draws: number;\n meshes: number;\n /** Pairs of meshes that z-fight, identical pairs counted each. */\n zFighting: number | null;\n /** Meshes that z-fight with themselves: merged geometry whose parts overlap. */\n selfZFighting: number | null;\n badGeometry: number;\n emptyMeshes: number;\n};\n\n/**\n * Every check as one count each: the single call to run after every edit. `0` everywhere (and\n * `triangles`, `draws` where you expect them) is a pass. Use `auditScene` to see what failed.\n */\nexport const summarizeScene = (\n root: AnyObject3D,\n options: AuditSummaryOptions = {},\n): AuditSummary => {\n const { tagKey, bakes } = options;\n const skip = scopeOf(root, options);\n const keyed = { skip, bakes, ...(tagKey === undefined ? {} : { tagKey }) };\n let zFighting: number | null = null;\n let selfZFighting: number | null = null;\n\n if (options.zFighting !== false) {\n zFighting = 0;\n selfZFighting = 0;\n\n for (const row of findZFighting(root, {\n ...(options.gap === undefined ? {} : { gap: options.gap }),\n ...(tagKey === undefined ? {} : { tagKey }),\n ...(options.tag === undefined ? {} : { tagged: true }),\n skip,\n bakes,\n self: true,\n })) {\n if (row.meshes[0] === row.meshes[1]) {\n selfZFighting += row.count;\n } else {\n zFighting += row.count;\n }\n }\n }\n\n return {\n triangles: countTriangles(root, { skip }),\n draws: countDraws(root, { skip }),\n meshes: listMeshes(root, { skip }).length,\n zFighting,\n selfZFighting,\n badGeometry: findBadGeometry(root, keyed).length,\n emptyMeshes: findEmptyMeshes(root, keyed).length,\n };\n};\n"],"mappings":";;AAeA,MAAa,cAAc,WAAwB;;;;;AAwBnD,MAAa,gBAAgB,WAAqB;CAChD,IAAI,OAAO,SAAS,eAAe,MACjC,OAAO;CAGT,KAAK,IAAI,KAAK,OAAO,QAAQ,IAAI,KAAK,GAAG,QACvC,IAAI,GAAG,SAAS,MAAM,UAAU,MAAM,SAAS,gBAAgB,IAAI,GACjE,OAAO;CAIX,OAAO;AACT;;AAGA,MAAa,QAAQ,MAAgB,SAAsB,UAAsC;CAC/F,MAAM,QAAQ,CAAC,IAAI;CACnB,MAAM,UAAU,QAAQ,UAAU;CAElC,OAAO,MAAM,SAAS,GAAG;EACvB,MAAM,SAAS,MAAM,IAAI;EAEzB,IACG,WAAW,OAAO,SAAS,gBAAgB,QAC3C,QAAQ,gBAAgB,QAAQ,CAAC,OAAO,WAAW,EAAE,WAAW,aAAa,MAAM,MACpF,QAAQ,OAAO,MAAM,MAAM,MAE3B;EAGF,MAAM,MAAM;EAEZ,KAAK,IAAI,IAAI,OAAO,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK,GACpD,MAAM,KAAK,OAAO,SAAS,EAAG;CAElC;AACF;AAEA,MAAa,UAAU,WAAsC,OAAgB,WAAW;;AAGxF,MAAa,cAAc,WAAqB;CAC9C,MAAM,QAAQ;CAEd,OACE,MAAM,WAAW,QACjB,MAAM,WAAW,QACjB,MAAM,aAAa,QACnB,MAAM,aAAa;AAEvB;AAEA,MAAa,eAAe,SACzB,KAAuB,oBAAoB;AAE9C,MAAa,aAAa,SACvB,KAAgC,kBAAkB;;AAGrD,MAAa,iBAAiB,aAA6B;CACzD,MAAM,WAAW,SAAS,OAAO,SAAS,SAAS,WAAW,UAAU,SAAS;CACjF,MAAM,EAAE,OAAO,UAAU,SAAS;CAClC,MAAM,QAAQ,KAAK,IAAI,UAAU,QAAQ,KAAK,IAAI;CAElD,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,CAAC,CAAC;AAC1C;;;;;AAMA,MAAa,qBAAqB,UAAuB;CACvD,MAAM,6BAAa,IAAI,IAAsD;CAE7E,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,kBAAkB,MAAM,GAAG;EACrD,IAAI;EAGJ,IAAI;GACF,IAAI,CAAC,MAAM,aAAa,EAAE,GACxB;GAGF,aAAa,MAAM,gBAAgB,EAAE;EACvC,QAAQ;GACN;EACF;EAEA,MAAM,QAAQ,WAAW,IAAI,UAAU;EAEvC,IAAI,OAAO;GACT,MAAM,aAAa;GACnB;EACF;EAEA,MAAM,QAAQ,MAAM,mBAAmB,UAAU;EAKjD,MAAM,WAAW,MAAM,UAAU,MAAM,SAAS,QAAQ,MAAM,aAAa,MAAM;EAEjF,WAAW,IAAI,YAAY;GAAE,WAAW;GAAG,WAAW,KAAK,MAAM,WAAW,CAAC;EAAE,CAAC;CAClF;CAEA,OAAO;AACT;;AAGA,MAAa,oBAAoB,UAAuB;CACtD,IAAI,YAAY;CAChB,IAAI,YAAY;CAEhB,KAAK,MAAM,YAAY,kBAAkB,KAAK,CAAC,CAAC,OAAO,GAAG;EACxD,aAAa,SAAS,YAAY,SAAS;EAC3C,aAAa,SAAS;CACxB;CAEA,OAAO;EAAE;EAAW;CAAU;AAChC;;AAGA,MAAa,SAAS,QAAkB,WAAmB;CACzD,KAAK,IAAI,KAAsB,QAAQ,IAAI,KAAK,GAAG,QAAQ;EACzD,MAAM,MAAe,GAAG,SAAS;EAEjC,IAAI,QAAQ,KAAA,KAAa,QAAQ,MAC/B,OAAO,OAAO,GAAG;CAErB;CAEA,OAAO;AACT;;;;;AAMA,MAAa,WAAW,MAAgB,KAAa,QAAgB,SAAsB;CACzF,IAAI,QAAQ;CAEZ,KAAK,MAAM,EAAE,KAAK,IAAI,WAAW;EAC/B,MAAM,MAAe,OAAO,SAAS;EAErC,UAAU,QAAQ,KAAA,KAAa,QAAQ,QAAQ,OAAO,GAAG,MAAM;CACjE,CAAC;CAED,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,2CAA2C,OAAO,MAAM,IAAI,EAAE;CAGhF,QAAQ,WAAW;EACjB,IAAI,OAAO,MAAM,MAAM,MACrB,OAAO;EAGT,MAAM,MAAe,OAAO,SAAS;EAErC,IAAI,QAAQ,KAAA,KAAa,QAAQ,QAAQ,OAAO,GAAG,MAAM,KACvD,OAAO;EAGT,OAAO,WAAW,MAAM,KAAK,MAAM,QAAQ,MAAM,MAAM;CACzD;AACF;AAEA,MAAa,UAAU,MAAgB,QAAgB,SAAgB;CACrE,IAAI,QAAQ;CAEZ,KAAK,MAAM,EAAE,KAAK,IAAI,WAAW;EAC/B,UAAU,OAAO,SAAS,YAAY,KAAA,KAAa,OAAO,SAAS,YAAY;CACjF,CAAC;CAED,OAAO;AACT;AAEA,MAAa,YAAY,SAAe,KAAK,QAAQ,KAAK,SAAS,QAAQ,KAAK,SAAS;;;;;;AAOzF,MAAa,UAAU,QAAkB,MAAuB,QAAiB,QAAQ,MAAM;CAC7F,MAAM,QAAkB,CAAC;CAEzB,KAAK,IAAI,KAAK,OAAO,QAAQ,IAAI,KAAK,OAAO,OAAO,OAAO,GAAG,QAAQ;EACpE,MAAM,MAAe,WAAW,KAAA,IAAY,KAAA,IAAY,GAAG,SAAS;EACpE,MAAM,OAAO,GAAG,SAAS,QAAQ,KAAA,KAAa,QAAQ,OAAO,KAAK,OAAO,GAAG;EAE5E,IAAI,MACF,MAAM,QAAQ,IAAI;CAEtB;CAEA,OAAO,MAAM,SAAS,QAAQ,CAAC,KAAK,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,IAAI;AAChE;;AAGA,MAAa,aAAa,MAAY,MAAuB,WAC3D,GAAG,CAAC,GAAG,OAAO,MAAM,MAAM,MAAM,GAAG,SAAS,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,IAAI,aAAa,IAAI,EAAE;AAEtF,MAAa,gBAAgB,SAAe;CAC1C,MAAM,WAAY,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,SAAS,KAAK,KAAK;CAIzE,OAAO,WAAW,SAAS,QAAQ,SAAS,OAAO;AACrD;;AAGA,MAAa,cACX,KACA,KACA,UACG,OAAO,eAAe,KAAK,KAAK;CAAE;CAAO,YAAY;AAAM,CAAC;;;ACvNjE,MAAM,UAAU,UAA0B,MAAc,SAAS,OAAO,KAAK,CAAC,KAAK;AAEnF,MAAM,SAAS,aAA6B;CAC1C,MAAM,WAAW,SAAS,WAAW;CACrC,MAAM,SAAS,SAAS,WAAW;CACnC,MAAM,UAAU,IAAI,WAAW,SAAS,KAAK;CAC7C,IAAI,YAAY;CAChB,IAAI,cAAc;CAElB,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,OAAO,KAAK,GAUvC,IAAI,EARF,OAAO,SAAS,SAAS,KAAK,CAAC,CAAC,KAChC,OAAO,SAAS,SAAS,KAAK,CAAC,CAAC,KAChC,OAAO,SAAS,SAAS,KAAK,CAAC,CAAC,MAC/B,CAAC,UACC,OAAO,SAAS,OAAO,KAAK,CAAC,CAAC,KAC7B,OAAO,SAAS,OAAO,KAAK,CAAC,CAAC,KAC9B,OAAO,SAAS,OAAO,KAAK,CAAC,CAAC,KAEvB;EACX,QAAQ,KAAK;EACb,aAAa;CACf;CAGF,IAAI,CAAC,QACH,OAAO;EAAE;EAAW;CAAY;CAGlC,MAAM,UAAU,SAAS,OAAO,SAAS,SAAS;CAElD,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,SAAS,KAAK,GAAG;EACvC,MAAM,IAAI,OAAO,UAAU,CAAC;EAC5B,MAAM,IAAI,OAAO,UAAU,IAAI,CAAC;EAChC,MAAM,IAAI,OAAO,UAAU,IAAI,CAAC;EAChC,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAC7C,MAAM,KAAK,SAAS,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC;EAG7C,IAAI,EAFS,KAAK,MAAM,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,EAEpE,IAAI,OACX;EAGF,KAAK,MAAM,KAAK;GAAC;GAAG;GAAG;EAAC,GACtB,IAAI,QAAQ,OAAO,KAAK,KAAK,MAAM,OAAO,KAAK,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,IAAI,MAAM;GACzF,QAAQ,KAAK;GACb,eAAe;EACjB;CAEJ;CAEA,OAAO;EAAE;EAAW;CAAY;AAClC;;AAGA,MAAa,mBACX,MACA,UAA8B,CAAC,MACV;CACrB,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,OAAyB,CAAC;CAEhC,KAAK,WAAW,IAAI,GAAG;EAAE,MAAM,QAAQ;EAAM,OAAO,QAAQ,SAAS;CAAU,IAAI,WAAW;EAC5F,IAAI,CAAC,OAAO,MAAM,KAAK,CAAC,OAAO,SAAS,WAAW,UACjD;EAGF,MAAM,EAAE,WAAW,gBAAgB,MAAM,OAAO,QAAQ;EAExD,IAAI,YAAY,KAAK,cAAc,GAAG;GACpC,MAAM,QAAQ,MAAM,QAAQ,MAAM,KAAK,OAAO,QAAQ,QAAQ;GAE9D,KAAK,KACH,WAAW;IAAE,MAAM,SAAS,MAAM;IAAG;IAAO;IAAW;GAAY,GAAG,UAAU,MAAM,CACxF;EACF;CACF,CAAC;CAED,OAAO;AACT;;;;;;AAcA,MAAa,mBACX,MACA,UAA8B,CAAC,MACZ;CACnB,MAAM,QAAQ,WAAW,IAAI;CAC7B,MAAM,OAAuB,CAAC;CAE9B,KAAK,OAAO;EAAE,MAAM,QAAQ;EAAM,OAAO,QAAQ,SAAS;CAAU,IAAI,WAAW;EACjF,IAAI,OAAO,MAAM,MAAM,OAAO,SAAS,WAAW,UAAU,SAAS,OAAO,GAC1E,KAAK,KACH,WACE,EAAE,MAAM,UAAU,QAAQ,OAAO,QAAQ,UAAU,cAAc,EAAE,GACnE,UACA,MACF,CACF;CAEJ,CAAC;CAED,OAAO;AACT;;;AC5FA,MAAM,QAAQ,SAAe;CAC3B,IAAI,UAAU,IAAI,GAChB,OAAO,iBAAiB,IAAI;CAG9B,MAAM,YAAY,YAAY,IAAI,IAAI,KAAK,QAAQ;CAEnD,OAAO;EAAE;EAAW,WAAW,cAAc,KAAK,QAAQ,IAAI;CAAU;AAC1E;;;;;AAMA,MAAa,kBAAkB,MAAmB,UAA2B,CAAC,MAAM;CAClF,IAAI,QAAQ;CAEZ,KAAK,WAAW,IAAI,GAAG;EAAE,MAAM,QAAQ;EAAM,aAAa;CAAK,IAAI,WAAW;EAC5E,IAAI,OAAO,MAAM,GACf,SAAS,KAAK,MAAM,CAAC,CAAC;CAE1B,CAAC;CAED,OAAO;AACT;;AAGA,MAAa,cAAc,OAAoB,UAA2B,CAAC,MAAiB;CAC1F,MAAM,OAAO,WAAW,KAAK;CAC7B,MAAM,OAAkB,CAAC;CAEzB,KAAK,MAAM;EAAE,MAAM,QAAQ;EAAM,aAAa;CAAK,IAAI,WAAW;EAChE,IAAI,CAAC,OAAO,MAAM,GAChB;EAGF,MAAM,EAAE,WAAW,cAAc,KAAK,MAAM;EAC5C,MAAM,MAAM;GACV,MAAM,CAAC,GAAG,OAAO,QAAQ,IAAI,GAAG,SAAS,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;GAC1D,UAAU,aAAa,MAAM;GAC7B,WAAW,YAAY,IAAI,KAAK,MAAM,YAAY,SAAS,IAAI;GAC/D;GACA,OAAO;EACT;EAEA,KAAK,KAAK,WAAW,KAAK,QAAQ,MAAM,CAAC;CAC3C,CAAC;CAED,OAAO,KAAK,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC9C;;;;;;AAOA,MAAa,kBAAkB,MAAmB,UAAyB,CAAC,MAAqB;CAC/F,MAAM,QAAQ,WAAW,IAAI;CAC7B,MAAM,uBAAO,IAAI,IAAyB;CAC1C,MAAM,OAAO,KAAa,WAAmB,SAAiB;EAC5D,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK;GAAE,UAAU;GAAK,MAAM;GAAG;GAAW,OAAO;GAAG,OAAO;EAAE;EAErF,IAAI,QAAQ;EACZ,IAAI,SAAS,YAAY;EACzB,KAAK,IAAI,KAAK,GAAG;CACnB;CAEA,KAAK,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,WAAW;EAC9C,IAAI,CAAC,OAAO,MAAM,KAAK,CAAC,OAAO,SAAS,WAAW,UACjD;EAGF,IAAI,UAAU,MAAM,GAAG;GACrB,MAAM,OAAO,CAAC,GAAG,OAAO,QAAQ,KAAK,GAAG,SAAS,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;GAElE,KAAK,MAAM,CAAC,IAAI,EAAE,WAAW,gBAAgB,kBAAkB,MAAM,GACnE,IAAI,QAAQ,QAAQ,QAAQ,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,WAAW,SAAS;GAG1E;EACF;EAEA,MAAM,WAAW,OAAO;EAExB,IACE,SAAS,QAAQ,GAAG,SAAS,KAAK,GAAG,SAAS,KAAK,MAAM,GAAG,CAAC,KAC7D,cAAc,QAAQ,GACtB,YAAY,MAAM,IAAI,OAAO,QAAQ,CACvC;CACF,CAAC;CAED,MAAM,MAAM,CAAC,GAAG,KAAK,OAAO,CAAC;CAC7B,MAAM,MAAM,IAAI,QAAQ,OAAO,QAAQ,QAAQ,IAAI,OAAO,CAAC;CAE3D,KAAK,MAAM,OAAO,KAAK;EACrB,IAAI,QAAQ,MAAM,IAAI,KAAK,MAAO,IAAI,QAAQ,MAAO,GAAI,IAAI,MAAO;EAEpE,IAAI,QAAQ,WAAW,KAAA,GACrB,IAAI,aAAa,IAAI,QAAQ,KAAK,IAAI,KAAK,CAAC,IAAI,QAAQ;CAE5D;CAEA,OAAO,IAAI,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAC7C;;;;;;AAOA,MAAa,cAAc,MAAmB,UAA2B,CAAC,MAAM;CAC9E,IAAI,QAAQ;CAEZ,KAAK,WAAW,IAAI,GAAG;EAAE,MAAM,QAAQ;EAAM,aAAa;CAAK,IAAI,WAAW;EAC5E,IAAI,CAAC,WAAW,MAAM,GACpB;EAGF,MAAM,WAAW;EAEjB,MAAM,WAAW,SAAS;EAE1B,IAAI,MAAM,QAAQ,QAAQ,GAAG;GAC3B,MAAM,SAAS,SAAS,UAAU,UAAU,CAAC;GAE7C,SAAS,OAAO,QACb,UAAU,SAAS,MAAM,iBAAiB,EAAE,EAAE,YAAY,KAC7D,CAAC,CAAC;EACJ,OAAO,IAAI,UAAU,YAAY,OAC/B,SAAS;CAEb,CAAC;CAED,OAAO;AACT;;;;AC1HA,MAAM,QAAQ;;AAEd,MAAM,WAAW;AAajB,MAAM,SAAS,OAAe,SAAS,MAAM;CAC3C,MAAM,QAAQ,MAAM;CACpB,MAAM,UAAU,KAAK,MAAM,QAAQ,KAAK,IAAI;CAE5C,OAAO,YAAY,IAAI,IAAI;AAC7B;AAEA,MAAM,WAAW,SAAe;CAC9B,MAAM,SAAS,KAAK,IAAI,KAAK,KAAK,SAAS,OAAO,MAAM,KAAK,IAAI,SAAU,CAAC,CAAC;CAE7E,OAAO,GAAG,KAAK,MAAM,MAAM,OAAO,KAAK,IAAI,EAAE;AAC/C;AAEA,MAAM,SAAS,UAAkB,MAAM,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC;;AAG1D,MAAM,cAAc,IAAY,IAAY,IAAY,MAAc;CACpE,KAAK,MAAM,CAAC,MAAM,UAAU;EAC1B,CAAC,KAAK,EAAE;EACR,CAAC,KAAK,EAAE;EACR,CAAC,KAAK,EAAE;CACV,GACE,IAAI,KAAK,IAAI,KAAK,IAAI,OACpB,OAAO,GAAG,KAAK,KAAK,MAAM,IAAI,KAAK,EAAE,WAAW,QAAQ,IAAI,MAAM,MAAM;CAI5E,OAAO,IAAI,MAAM,EAAE,EAAE,IAAI,MAAM,EAAE,EAAE,IAAI,MAAM,EAAE,EAAE,UAAU,MAAM,CAAC;AACpE;AAEA,MAAM,eAAe,UAClB,MAAM,QAAQ,KAAK,QAAQ,IAAI,KAAK,WAAW,CAAC,KAAK,QAAQ,EAAA,CAAG,QAC9D,aAAmC,aAAa,KAAA,KAAa,aAAa,IAC7E;;AAGF,MAAM,YAAY,GAAS,MACzB,CAAC,EAAE,UAAU,CAAC,EAAE,WAAY,EAAE,UAAU,EAAE,SAAW,EAAE,UAAU,EAAE;;AAGrE,MAAa,iBACX,OACA,UAA4B,CAAC,MACV;CACnB,MAAM,OAAO,WAAW,KAAK;CAC7B,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,SAAS,QAAQ,UAAU,OAAO,MAAM,QAAQ,QAAQ,IAAI;CAElE,KAAK,kBAAkB,MAAM,IAAI;CAEjC,MAAM,QAAgB,CAAC;CAEvB,MAAM,QAAkB,CAAC;CACzB,MAAM,SAAmB,CAAC;CAC1B,MAAM,UAAoB,CAAC;CAC3B,MAAM,OAA2C,CAAC;CAClD,MAAM,0BAAU,IAAI,IAAsB;CAC1C,MAAM,SAAS,GAAW,GAAW,GAAW,MAAc,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;CAEhF,KACE,MACA;EAAE,MAAM,QAAQ;EAAM,aAAa;EAAM,OAAO,QAAQ,SAAS;CAAU,IAC1E,WAAW;EACV,IACE,CAAC,OAAO,MAAM,KACd,YAAY,MAAM,KAClB,UAAU,MAAM,KACf,OAAO,SAAqD,8BAC3D,QACF,CAAC,OAAO,SAAS,WAAW,UAE5B;EAGF,IAAI,UAAU,MAAM,QAAQ,MAAM,MAAM,MACtC;EAGF,MAAM,YAAY,YAAY,MAAM,CAAC,CAAC,QAAQ,aAAa,SAAS,YAAY,KAAK;EAErF,IAAI,UAAU,WAAW,GACvB;EAGF,MAAM,OAAO,MAAM;EACnB,MAAM,EAAE,OAAO,eAAe,OAAO;EACrC,MAAM,WAAW,WAAW;EAC5B,MAAM,IAAI,OAAO,YAAY;EAC7B,MAAM,MAAM;GAAC;GAAU;GAAU;EAAQ;EACzC,MAAM,MAAM;GAAC;GAAW;GAAW;EAAS;EAC5C,MAAM,QAAQ,OAAO,SAAS,SAAS;EACvC,MAAM,QAAQ;GAAC;GAAG;GAAG;GAAG;GAAG;GAAG;GAAG;GAAG;GAAG;EAAC;EAExC,MAAM,KAAK;GACT,MAAM;GACN,OAAO,UAAU,QAAQ,MAAM,MAAM;GACrC;GACA;GACA,QAAQ,UAAU,MAAM,aAAa,SAAS,eAAe,KAAK;GAClE,OAAO,UAAU,MAAM,aAAa,SAAS,cAAc,KAAK;GAChE,QAAQ,UAAU,MACf,aACC,SAAS,kBAAkB,SAC1B,SAAS,wBAAwB,KAAK,SAAS,uBAAuB,EAC3E;EACF,CAAC;EAED,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,OAAO,KAAK,GAAG;GACrC,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;IAC7B,MAAM,IAAI,OAAO,KAAK,IAAI,CAAC,KAAK,IAAI;IACpC,MAAM,IAAI,SAAS,KAAK,CAAC;IACzB,MAAM,IAAI,SAAS,KAAK,CAAC;IACzB,MAAM,IAAI,SAAS,KAAK,CAAC;IAEzB,KAAK,IAAI,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG;KACtC,MAAM,QAAQ,EAAE,QAAS,IAAI,EAAE,OAAO,KAAM,IAAI,EAAE,OAAO,KAAM,IAAI,EAAE,OAAO;KAE5E,MAAM,IAAI,IAAI,QAAQ;KACtB,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAQ,KAAK;KACtC,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAQ,KAAK;IACxC;GACF;GAEA,MAAM,CAAC,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,MAAM;GAW7C,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,MAAM,KAAK,KAAK;GAChB,IAAI,KAAK,KAAK,KAAK,KAAK;GACxB,IAAI,KAAK,KAAK,KAAK,KAAK;GACxB,IAAI,KAAK,KAAK,KAAK,KAAK;GACxB,MAAM,SAAS,KAAK,MAAM,IAAI,IAAI,EAAE;GAGpC,IAAI,EAAE,SAAS,KAAK,OAClB;GAGF,MAAM;GACN,MAAM;GACN,MAAM;GAEN,MAAM,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK;GACnC,MAAM,KAAK,MAAM;GACjB,MAAM,MAAwC;IAC5C,KAAK,MAAM,KAAK,KAAK;IACrB,KAAK,MAAM,KAAK,KAAK;IACrB,KAAK,MAAM,KAAK,KAAK;IACrB,KAAK,MAAM,IAAI,GAAG;GACpB;GACA,MAAM,SAAS,QAAQ,IAAI,MAAM,GAAG,GAAG,CAAC;GAExC,MAAM,KAAK,IAAI;GACf,OAAO,KAAK,IAAI,IAAI,IAAI,CAAC;GACzB,QAAQ,KAAK,GAAG,KAAK;GACrB,KAAK,KAAK,GAAG;GAEb,IAAI,QACF,OAAO,KAAK,EAAE;QAEd,QAAQ,IAAI,MAAM,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC;EAEnC;CACF,CACF;CAGA,MAAM,SAAS,MAAM;CACrB,MAAM,OAAO;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAC;CAEhD,MAAM,QAAQ;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;CAAC;CAE/B,MAAM,YAAY,GAAW,MAAc;EACzC,MAAM,IAAI,IAAI;EACd,MAAM,IAAI,IAAI;EACd,IAAI,KAAK,QAAQ,IAAI,KAAM,QAAQ;EACnC,IAAI,KAAK,QAAQ,IAAI,KAAM,QAAQ,IAAI;EACvC,IAAI,KAAK,QAAQ,IAAI,KAAM,QAAQ,IAAI;EACvC,MAAM,SAAS,KAAK,MAAM,IAAI,IAAI,EAAE,KAAK;EAEzC,MAAM;EACN,MAAM;EACN,MAAM;EAEN,MAAM,KAAK,OAAO,IAAI;EACtB,MAAM,KAAK,OAAO,IAAI,IAAI;EAC1B,MAAM,KAAK,OAAO,IAAI,IAAI;EAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;EAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;EAC1B,MAAM,KAAK,KAAK,KAAK,KAAK;EAE1B,MAAM,KAAK;EACX,MAAM,KAAK;EACX,MAAM,KAAK;EACX,MAAM,KAAK;EACX,MAAM,KAAK;EACX,MAAM,KAAK;EAGX,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;GAC7B,MAAM,KAAK,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,IAAI,KAAK;GAC7C,MAAM,KAAK,QAAQ;GACnB,MAAM,KAAK,QAAQ,KAAK;GACxB,MAAM,KAAK,QAAQ,KAAK;GAExB,KAAK,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK;GACvC,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK;EAC7C;EAEA,KAAK,IAAI,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG;GACtC,MAAM,OAAO;GACb,MAAM,KAAK,OAAO,KAAK,OAAO,KAAK,IAAI,KAAM,OAAO,KAAK;GACzD,MAAM,KAAK,KAAK,KAAK,KAAM,KAAK,OAAO;GACvC,MAAM,KAAK,KAAK,KAAK,IAAI,KAAM,KAAK,OAAO,IAAI;GAC/C,MAAM,aAAa,KAAK,MAAM,IAAI,EAAE,KAAK;GACzC,MAAM,QAAQ,CAAC,KAAK;GACpB,MAAM,QAAQ,KAAK;GACnB,IAAI,KAAK;GACT,IAAI,KAAK;GACT,IAAI,KAAK;GACT,IAAI,KAAK;GAET,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;IAC7B,MAAM,QAAQ,KAAK,IAAI,KAAM,QAAQ,KAAK,IAAI,IAAI,KAAM;IAExD,IAAI,IAAI,GAAG;KACT,KAAK,KAAK,IAAI,IAAI,KAAK;KACvB,KAAK,KAAK,IAAI,IAAI,KAAK;IACzB,OAAO;KACL,KAAK,KAAK,IAAI,IAAI,KAAK;KACvB,KAAK,KAAK,IAAI,IAAI,KAAK;IACzB;GACF;GAEA,IAAI,KAAK,IAAI,IAAI,EAAE,IAAI,KAAK,IAAI,IAAI,EAAE,IAAI,QACxC,OAAO;EAEX;EAEA,OAAO;CACT;;;;;CAMA,MAAM,gBAAgB;EAIpB,MAAM,SAFH,KAAK,KAAM,KAAK,OAAQ,KAAK,KAAM,KAAK,OAAQ,KAAK,KAAM,KAAK,OAAQ,KAAK,KAAM,KAAK,OAErE,IAAI;GAAC;GAAG;GAAG;EAAC,IAAI;GAAC;GAAG;GAAG;EAAC;EAC9C,IAAI,UAAU,KAAK,MAAM,GAAG,EAAE;EAE9B,KAAK,IAAI,OAAO,GAAG,OAAO,KAAK,QAAQ,UAAU,GAAG,QAAQ,GAAG;GAC7D,MAAM,KAAK,KAAK,MAAM,QAAS;GAC/B,MAAM,KAAK,KAAK,MAAM,QAAS,IAAI;GACnC,MAAM,KAAK,KAAK,OAAO,OAAO,KAAK,KAAM,KAAM;GAC/C,MAAM,KAAK,KAAK,OAAO,OAAO,KAAK,KAAM,IAAI,KAAM;GACnD,MAAM,OAAiB,CAAC;GACxB,MAAM,IAAI,QAAQ,SAAS;GAE3B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;IAC7B,MAAM,KAAK,QAAQ,IAAI;IACvB,MAAM,KAAK,QAAQ,IAAI,IAAI;IAC3B,MAAM,KAAK,SAAU,IAAI,KAAK,IAAK;IACnC,MAAM,KAAK,SAAU,IAAI,KAAK,IAAK,IAAI;IACvC,MAAM,KAAK,MAAM,KAAK,MAAM,MAAM,KAAK;IACvC,MAAM,KAAK,MAAM,KAAK,MAAM,MAAM,KAAK;IAEvC,IAAI,MAAM,GACR,KAAK,KAAK,IAAI,EAAE;IAGlB,IAAI,MAAM,MAAM,MAAM,GAAG;KACvB,MAAM,IAAI,MAAM,KAAK;KAErB,KAAK,KAAK,MAAM,KAAK,MAAM,GAAG,MAAM,KAAK,MAAM,CAAC;IAClD;GACF;GAEA,UAAU;EACZ;EAEA,IAAI,QAAQ;EACZ,IAAI,KAAK;EACT,IAAI,KAAK;EACT,MAAM,IAAI,QAAQ,SAAS;EAE3B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;GAC7B,MAAM,KAAK,QAAQ,IAAI;GACvB,MAAM,KAAK,QAAQ,IAAI,IAAI;GAC3B,MAAM,KAAK,SAAU,IAAI,KAAK,IAAK;GACnC,MAAM,KAAK,SAAU,IAAI,KAAK,IAAK,IAAI;GACvC,MAAM,QAAQ,KAAK,KAAK,KAAK;GAE7B,SAAS;GACT,OAAO,KAAK,MAAM;GAClB,OAAO,KAAK,MAAM;EACpB;EAEA,OAAO,KAAK,IAAI,KAAK,IAAI,QACrB;GAAE,MAAM;GAAG,GAAG;GAAG,GAAG;EAAE,IACtB;GAAE,MAAM,KAAK,IAAI,KAAK,IAAI;GAAG,GAAG,MAAM,IAAI;GAAQ,GAAG,MAAM,IAAI;EAAO;CAC5E;CAGA,MAAM,WAAW,GAAW,MAAc;EACxC,MAAM,KAAK,OAAO,IAAI;EACtB,MAAM,KAAK,OAAO,IAAI,IAAI;EAC1B,MAAM,KAAK,OAAO,IAAI,IAAI;EAC1B,MAAM,IAAI,OAAO,IAAI,IAAI;EAEzB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;GAC7B,MAAM,KAAK,IAAI,IAAI,IAAI;GAEvB,IAAI,KAAK,IAAI,KAAK,QAAQ,MAAO,KAAK,QAAQ,KAAK,KAAM,KAAK,QAAQ,KAAK,KAAM,CAAC,IAAI,KACpF,OAAO;EAEX;EAEA,OAAO;CACT;CAYA,MAAM,wBAAQ,IAAI,IAAkB;CAGpC,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxC,MAAM,CAAC,IAAI,IAAI,IAAI,MAAM,KAAK;EAE9B,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,KAAK,GAC5B,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,KAAK,GAC5B,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,KAAK,GAC5B,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,KAAK,GAC5B,KAAK,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG;GACxE,MAAM,IAAI,MAAM;GAChB,MAAM,IAAI,MAAM;GAEhB,IACE,KAAK,KACJ,MAAM,KAAK,CAAC,QACb,CAAC,SAAS,MAAM,IAAK,MAAM,EAAG,KAC9B,OAAO,IAAI,KAAM,OAAO,IAAI,KAC1B,OAAO,IAAI,IAAI,KAAM,OAAO,IAAI,IAAI,KACpC,OAAO,IAAI,IAAI,KAAM,OAAO,IAAI,IAAI,KACpC,YACF,CAAC,QAAQ,GAAG,CAAC,KACb,CAAC,QAAQ,GAAG,CAAC,KACb,CAAC,SAAS,GAAG,CAAC,GAEd;GAGF,MAAM,MAAM,IAAI,IAAI,GAAG,EAAE,GAAG,MAAM,GAAG,EAAE,GAAG;GAC1C,MAAM,OAAO,MAAM,IAAI,GAAG,KAAK;IAC7B,GAAG,KAAK,IAAI,GAAG,CAAC;IAChB,GAAG,KAAK,IAAI,GAAG,CAAC;IAChB,WAAW;IACX,MAAM;IACN,QAAQ;KAAC;KAAG;KAAG;IAAC;IAChB,wBAAQ,IAAI,IAAoB;GAClC;GACA,MAAM,KAAK,OAAO,IAAI;GACtB,MAAM,KAAK,OAAO,IAAI,IAAI;GAC1B,MAAM,KAAK,OAAO,IAAI,IAAI;GAC1B,MAAM,IAAI,OAAO,IAAI,IAAI;GACzB,MAAM,EAAE,MAAM,GAAG,MAAM,QAAQ;GAC/B,MAAM,QAAQ,WAAW,IAAI,IAAI,IAAI,CAAC;GAEtC,KAAK,aAAa;GAClB,KAAK,QAAQ;GACb,KAAK,OAAO,MAAM,QAAQ,IAAI,MAAM,KAAM,IAAI,MAAM,KAAM,IAAI;GAC9D,KAAK,OAAO,MAAM,QAAQ,IAAI,MAAM,KAAM,IAAI,MAAM,KAAM,IAAI;GAC9D,KAAK,OAAO,MAAM,QAAQ,IAAI,MAAM,KAAM,IAAI,MAAM,KAAM,IAAI;GAC9D,KAAK,OAAO,IAAI,QAAQ,KAAK,OAAO,IAAI,KAAK,KAAK,KAAK,IAAI;GAC3D,MAAM,IAAI,KAAK,IAAI;EACrB;CAKV;CAGA,MAAM,uBAAO,IAAI,IAAiE;CAElF,KAAK,MAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,YAAY,EAAE,SAAS,GAAG;EAChF,MAAM,SAAS,CAAC,MAAM,KAAK,EAAE,CAAE,MAAM,MAAM,KAAK,EAAE,CAAE,IAAI;EACxD,MAAM,IAAI,QAAQ,MAAM,KAAK,EAAG;EAChC,MAAM,IAAI,QAAQ,MAAM,KAAK,EAAG;EAChC,MAAM,cAAc,CAAC,GAAG,KAAK,OAAO,QAAQ,CAAC,CAAC,CAC3C,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAC3B,KAAK,CAAC,WAAW,KAAK;EACzB,MAAM,MAAM,KAAK,UAAU;GAAC;GAAG;GAAG,KAAK;GAAW;EAAW,CAAC;EAC9D,MAAM,SAAS,KAAK,IAAI,GAAG;EAE3B,IAAI,QAAQ;GACV,OAAO,SAAS;GAChB,OAAO,MAAM,KAAK,MAAM;GACxB;EACF;EAEA,MAAM,SAAS,KAAK,OAAO,IAAI,KAAK,OAAO;EAC3C,MAAM,MAAM,WACV,WACE;GACE;GACA;GACA,WAAW,KAAK;GAChB,QAAQ;GACR,SAAS;IACP,MAAM,MAAM,KAAK,MAAM,CAAC;IACxB,QAAQ,KAAK,OAAO,KAAK,QAAQ,MAAM,MAAM,MAAM,CAAC;GACtD;GACA,OAAO;EACT,GACA,UACA,MACF,GACA,SACA,CAAC,MAAM,CACT;EAEA,KAAK,IAAI,KAAK,GAAG;CACnB;CAEA,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B;;;;AC9fA,MAAM,WACJ,MACA,YAEA,QAAQ,QAAQ,KAAA,IACZ,QAAQ,OACR,QAAQ,WAAW,IAAI,GAAG,QAAQ,KAAK,QAAQ,UAAU,gBAAgB,QAAQ,IAAI;;;;;AAM3F,MAAa,cAAc,MAAmB,UAA6B,CAAC,MAAmB;CAC7F,MAAM,EAAE,QAAQ,UAAU;CAC1B,MAAM,OAAO,QAAQ,MAAM,OAAO;CAClC,MAAM,QAAQ;EAAE;EAAM;EAAO,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;CAAG;CAEzE,OAAO;EACL,WAAW,eAAe,MAAM,EAAE,KAAK,CAAC;EACxC,OAAO,WAAW,MAAM,EAAE,KAAK,CAAC;EAChC,WAAW,cAAc,MAAM;GAC7B,GAAG;GACH;GACA,GAAI,QAAQ,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,KAAK;EACtD,CAAC;EACD,aAAa,gBAAgB,MAAM,KAAK;EACxC,aAAa,gBAAgB,MAAM,KAAK;EACxC,QAAQ,WAAW,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,QAAQ,OAAO,EAAE;CAC/D;AACF;;;;;AA0BA,MAAa,kBACX,MACA,UAA+B,CAAC,MACf;CACjB,MAAM,EAAE,QAAQ,UAAU;CAC1B,MAAM,OAAO,QAAQ,MAAM,OAAO;CAClC,MAAM,QAAQ;EAAE;EAAM;EAAO,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;CAAG;CACzE,IAAI,YAA2B;CAC/B,IAAI,gBAA+B;CAEnC,IAAI,QAAQ,cAAc,OAAO;EAC/B,YAAY;EACZ,gBAAgB;EAEhB,KAAK,MAAM,OAAO,cAAc,MAAM;GACpC,GAAI,QAAQ,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK,QAAQ,IAAI;GACxD,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;GACzC,GAAI,QAAQ,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,KAAK;GACpD;GACA;GACA,MAAM;EACR,CAAC,GACC,IAAI,IAAI,OAAO,OAAO,IAAI,OAAO,IAC/B,iBAAiB,IAAI;OAErB,aAAa,IAAI;CAGvB;CAEA,OAAO;EACL,WAAW,eAAe,MAAM,EAAE,KAAK,CAAC;EACxC,OAAO,WAAW,MAAM,EAAE,KAAK,CAAC;EAChC,QAAQ,WAAW,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;EACnC;EACA;EACA,aAAa,gBAAgB,MAAM,KAAK,CAAC,CAAC;EAC1C,aAAa,gBAAgB,MAAM,KAAK,CAAC,CAAC;CAC5C;AACF"}
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as auditScene } from "./audit-scene-BYsJcco9.js";
2
+ import { t as auditScene } from "./audit-scene-Da-8SA1x.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";
@@ -36,7 +36,7 @@ const version = () => {
36
36
  };
37
37
  const count = (value) => value.toLocaleString("en-US");
38
38
  const printReport = (file, report, top) => {
39
- const lines = [`${basename(file)} ${count(report.triangles)} triangles`];
39
+ const lines = [`${basename(file)} ${count(report.triangles)} triangles, ${count(report.draws)} draws`];
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"}`);
@@ -51,6 +51,10 @@ const printReport = (file, report, top) => {
51
51
  lines.push(` ✗ ${report.badGeometry.length} mesh${report.badGeometry.length === 1 ? "" : "es"} with NaN geometry`);
52
52
  for (const row of report.badGeometry) lines.push(` ${row.mesh}${row.under ? ` (${row.under})` : ""}: ${row.nonFinite} non-finite, ${row.zeroNormals} zero normals`);
53
53
  }
54
+ if (report.emptyMeshes.length > 0) {
55
+ lines.push(` ! ${report.emptyMeshes.length} empty mesh${report.emptyMeshes.length === 1 ? "" : "es"} (no vertices)`);
56
+ for (const row of report.emptyMeshes) lines.push(` ${row.mesh}`);
57
+ }
54
58
  if (top > 0 && report.meshes.length > 0) {
55
59
  lines.push(" most triangles:");
56
60
  for (const row of report.meshes) lines.push(` ${count(row.total).padStart(9)} ${row.name}${row.instances > 1 ? ` ×${row.instances}` : ""}`);