@zkmake/three-audit 0.3.2 → 0.4.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.d.ts CHANGED
@@ -1,166 +1,5 @@
1
- import { BatchedMesh, BufferGeometry, Mesh, Object3D } from "three";
2
- //#region src/scene.d.ts
3
- /**
4
- * Any three.js object, from whichever copy of three's types the app has. The checks take this, so
5
- * an app on a newer `@types/three` than this package was built against passes its scene as is.
6
- */
7
- type AnyObject3D = {
8
- readonly isObject3D: true;
9
- readonly children: readonly object[];
10
- };
11
- /** Leave an object and everything under it out of a check. */
12
- type Skip = (object: Object3D) => boolean;
13
- /**
14
- * What a check sees of a bake (`@zkmake/three-batch`'s `bake` and `<Baked>`), which hides its
15
- * parts and draws one merged mesh per material instead. `sources`: the parts, as if shown, and not
16
- * the merge, so rows name real parts. `merged`: what the renderer draws.
17
- */
18
- type Bakes = "sources" | "merged";
19
- /** Triangles one draw of this geometry makes: indexed or not, within its draw range. */
20
- export declare const triangleCount: (geometry: BufferGeometry) => number;
21
- //#endregion
22
- //#region src/bad-geometry.d.ts
23
- type BadGeometryOptions = {
24
- skip?: Skip | undefined;
25
- /** `userData` key naming the part a mesh belongs to, for the `under` column. */
26
- tagKey?: string;
27
- /** A bake's parts (default), or its merged meshes: see `Bakes`. Not both, or each counts twice. */
28
- bakes?: Bakes | undefined;
29
- };
30
- type BadGeometryRow = {
31
- mesh: string;
32
- /** The tagged part it sits in, or its parent's name. */
33
- under: string;
34
- /** Vertices with a NaN or infinite position or normal component. */
35
- nonFinite: number;
36
- /** Vertices of triangles with area whose normal has no length. */
37
- zeroNormals: number;
38
- /** Not enumerable: kept out of JSON and `console.table`. */
39
- readonly object: Mesh;
40
- };
41
- /** Meshes with geometry that renders NaN, hidden ones included. `[]` is a pass. */
42
- export declare const findBadGeometry: (root: AnyObject3D, options?: BadGeometryOptions) => BadGeometryRow[];
43
- type EmptyMeshRow = {
44
- /** `path/to/mesh [material]`. */
45
- mesh: string;
46
- /** Not enumerable: kept out of JSON and `console.table`. */
47
- readonly object: Mesh;
48
- };
49
- /**
50
- * Meshes with no vertices, hidden ones included: what a merge of nothing leaves behind. They draw
51
- * nothing, but each one is still a draw call and a leak to dispose. A draw range of zero doesn't
52
- * count: that's usually a pool waiting to fill.
53
- */
54
- export declare const findEmptyMeshes: (root: AnyObject3D, options?: BadGeometryOptions) => EmptyMeshRow[];
55
- //#endregion
56
- //#region src/triangles.d.ts
57
- type TriangleOptions = {
58
- skip?: Skip | undefined;
59
- };
60
- type MeshRow = {
61
- /** `path/to/mesh`: its named ancestors, then its name (or its geometry's name or type). */
62
- name: string;
63
- material: string;
64
- /** Triangles in one copy. A batch's is an average: `geometryCensus` breaks it down. */
65
- triangles: number;
66
- instances: number;
67
- total: number;
68
- /** Not enumerable: kept out of JSON and `console.table`. */
69
- readonly mesh: Mesh;
70
- };
71
- type GeometryRow = {
72
- /**
73
- * `geometry.name`, or its type and the start of its uuid. For a geometry in a batch, the batch's
74
- * `label` for it, or `batch#id`.
75
- */
76
- geometry: string;
77
- /** Copies drawn: every mesh using it, instances counted. */
78
- uses: number;
79
- triangles: number;
80
- total: number;
81
- /** Its part of the census's total, 0–1. */
82
- share: number;
83
- /** With `budget`: whether `share` is over it. */
84
- overBudget?: boolean;
85
- };
86
- type CensusOptions = TriangleOptions & {
87
- /**
88
- * Names a geometry inside a batch, e.g. from the app's own table of what it added. Default
89
- * `batch#id`, the batch named by its path.
90
- */
91
- label?: ((batch: BatchedMesh, geometryId: number) => string | undefined) | undefined;
92
- /** Flag rows whose share of the total is over this, 0–1: `0.2` for anything over 20%. */
93
- budget?: number | undefined;
94
- };
95
- /**
96
- * Triangles one pass draws: visible meshes, instances counted, no frustum culling. A shadow pass
97
- * adds roughly as much again for each shadow-casting light.
98
- */
99
- export declare const countTriangles: (root: AnyObject3D, options?: TriangleOptions) => number;
100
- /** Every visible mesh, most triangles drawn first. */
101
- export declare const listMeshes: (scene: AnyObject3D, options?: TriangleOptions) => MeshRow[];
102
- /**
103
- * Geometries by triangles drawn, hidden meshes included, most first. Name geometries
104
- * (`geometry.name`) to make this readable. A batch counts each geometry in it separately, by its
105
- * visible instances: the row a budget is really spent on.
106
- */
107
- export declare const geometryCensus: (root: AnyObject3D, options?: CensusOptions) => GeometryRow[];
108
- /**
109
- * Draw calls one main pass makes: one per visible mesh, line or point cloud, or one per material
110
- * group for a mesh with several materials. Before frustum culling; each shadow-casting light adds
111
- * one per caster. `recordDrawLedger` counts the real thing.
112
- */
113
- export declare const countDraws: (root: AnyObject3D, options?: TriangleOptions) => number;
114
- //#endregion
115
- //#region src/z-fighting.d.ts
116
- type ZFightingOptions = {
117
- /**
118
- * How close two planes count as one, in world units. 0.004 (4 mm at 1 unit = 1 m) is about what
119
- * depth precision loses with a camera ~50 m out. Scale it with your units.
120
- */
121
- gap?: number;
122
- /** Also compare triangles within one mesh: for merged geometry whose parts share a buffer. */
123
- self?: boolean;
124
- /**
125
- * Only meshes inside a tagged object (`userData[tagKey]`), so a studio's ground and props stay
126
- * out. Defaults to true when anything under the root is tagged, false otherwise.
127
- */
128
- tagged?: boolean;
129
- tagKey?: string;
130
- skip?: Skip | undefined;
131
- /**
132
- * A bake's parts (default), or its merged meshes: see `Bakes`. The parts name what to fix; the
133
- * merge is the same faces under one name.
134
- */
135
- bakes?: Bakes | undefined;
136
- };
137
- type ZFightingRow = {
138
- /** `path/to/mesh [material] @ (x, y, z)`: named ancestors, and the centre in world space. */
139
- a: string;
140
- b: string;
141
- /** Overlapping triangle pairs between the two. */
142
- triangles: number;
143
- /**
144
- * The planes they share, most overlap first: `z = -3.000, facing +z` when axis-aligned, else
145
- * `(nx, ny, nz) · p = d`. Both faces point along the normal: opposite-facing faces never pair.
146
- */
147
- planes: string[];
148
- /** Where they overlap: the area in world units², and its centre in world space. */
149
- overlap: {
150
- area: number;
151
- centre: [number, number, number];
152
- };
153
- /** Identical pairs (same labels, triangles and planes) folded into this row. */
154
- count: number;
155
- /** Not enumerable: kept out of JSON and `console.table`. The first pair folded in. */
156
- readonly meshes: readonly [Mesh, Mesh];
157
- /** Not enumerable: every pair folded in. */
158
- readonly pairs: readonly (readonly [Mesh, Mesh])[];
159
- };
160
- /** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */
161
- export declare const findZFighting: (scene: AnyObject3D, options?: ZFightingOptions) => ZFightingRow[];
162
- //#endregion
163
- //#region src/audit-scene.d.ts
1
+ import { A as triangleCount, C as BadGeometryRow, D as AnyObject3D, E as findEmptyMeshes, O as Bakes, S as BadGeometryOptions, T as findBadGeometry, _ as countDraws, a as FrameBudget, b as listMeshes, c as defineBudgets, d as ZFightingRow, f as findZFighting, g as TriangleOptions, h as MeshRow, i as BudgetsInput, k as Skip, l as parseBudgets, m as GeometryRow, n as BudgetMetric, o as PartBudget, p as CensusOptions, r as Budgets, s as checkBudget, t as BudgetCheck, u as ZFightingOptions, v as countTriangles, w as EmptyMeshRow, x as triangles, y as geometryCensus } from "./budgets-Dl-AOd9T.js";
2
+ import { BatchedMesh } from "three";
164
3
  type AuditSceneOptions = Pick<ZFightingOptions, "bakes" | "gap" | "self" | "tagged"> & {
165
4
  skip?: Skip | undefined;
166
5
  tagKey?: string;
@@ -204,8 +43,6 @@ type AuditSummary = {
204
43
  * `triangles`, `draws` where you expect them) is a pass. Use `auditScene` to see what failed.
205
44
  */
206
45
  export declare const summarizeScene: (root: AnyObject3D, options?: AuditSummaryOptions) => AuditSummary;
207
- //#endregion
208
- //#region src/black-frames.d.ts
209
46
  /** The `WebGLRenderer` methods the probe uses, typed loosely to fit any copy of three's types. */
210
47
  type ProbeRenderer = {
211
48
  render(scene: AnyObject3D, camera: object): void;
@@ -233,8 +70,6 @@ type BlackFrameWatch = {
233
70
  * Browser only: frames are told apart by `requestAnimationFrame`.
234
71
  */
235
72
  export declare const watchBlackFrames: (renderer: ProbeRenderer, options?: BlackFrameOptions) => BlackFrameWatch;
236
- //#endregion
237
- //#region src/draw-ledger.d.ts
238
73
  /**
239
74
  * The two `WebGLRenderer` methods the ledger wraps. Typed loosely, so a renderer from any copy of
240
75
  * three's types fits.
@@ -320,8 +155,6 @@ export declare const ledgerTable: (rows: LedgerRow[], passes: string[]) => {
320
155
  }[];
321
156
  /** The ledger as two console tables: by group, and every object collapsed beneath. */
322
157
  export declare const printDrawLedger: (ledger: DrawLedger) => void;
323
- //#endregion
324
- //#region src/console-helpers.d.ts
325
158
  type AuditHelpersOptions = {
326
159
  /** A `WebGLRenderer`, for `ledger()`, `beginLedger()` and `blackFrames()`. */
327
160
  renderer?: (LedgerRenderer & ProbeRenderer) | undefined;
@@ -346,8 +179,6 @@ type AuditHelpersOptions = {
346
179
  * useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);
347
180
  */
348
181
  export declare const installAuditHelpers: (root: AnyObject3D, options?: AuditHelpersOptions) => () => void;
349
- //#endregion
350
- //#region src/measure.d.ts
351
182
  type MeasureOptions = {
352
183
  /** `userData` key a string target is looked up by. Default `studioObject`. */
353
184
  tagKey?: string;
@@ -381,6 +212,5 @@ export declare const measureBounds: (root: AnyObject3D, target: Target, options?
381
212
  * matches nothing; `null` when either draws nothing. Meshes in both targets are left out of `a`.
382
213
  */
383
214
  export declare const measureClearance: (root: AnyObject3D, a: Target, b: Target, options?: MeasureOptions) => Clearance | null;
384
- //#endregion
385
- export type { AnyObject3D, AuditHelpersOptions, AuditReport, AuditSceneOptions, AuditSummary, AuditSummaryOptions, BadGeometryOptions, BadGeometryRow, Bakes, BlackFrameOptions, BlackFrameWatch, Bounds, CensusOptions, Clearance, DrawLedger, DrawLedgerOptions, DrawLedgerRecording, EmptyMeshRow, GeometryRow, LedgerRenderer, LedgerRow, MeasureOptions, MeshRow, PassRow, ProbeRenderer, RecordDrawLedgerOptions, Skip, Target, TriangleOptions, ZFightingOptions, ZFightingRow };
215
+ export { type AnyObject3D, type AuditHelpersOptions, type AuditReport, type AuditSceneOptions, type AuditSummary, type AuditSummaryOptions, type BadGeometryOptions, type BadGeometryRow, type Bakes, type BlackFrameOptions, type BlackFrameWatch, type Bounds, type BudgetCheck, type BudgetMetric, type Budgets, type BudgetsInput, type CensusOptions, type Clearance, type DrawLedger, type DrawLedgerOptions, type DrawLedgerRecording, type EmptyMeshRow, type FrameBudget, type GeometryRow, type LedgerRenderer, type LedgerRow, type MeasureOptions, type MeshRow, type PartBudget, type PassRow, type ProbeRenderer, type RecordDrawLedgerOptions, type Skip, type Target, type TriangleOptions, type ZFightingOptions, type ZFightingRow, checkBudget, countDraws, countTriangles, defineBudgets, findBadGeometry, findEmptyMeshes, findZFighting, geometryCensus, listMeshes, parseBudgets, triangleCount, triangles };
386
216
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
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-Da-8SA1x.js";
2
- //#region src/patch.ts
1
+ import { _ as meshLabel, a as countDraws, b as triangleCount, c as listMeshes, d as findEmptyMeshes, f as asObject3D, g as isMesh, h as isInstanced, i as findZFighting, l as triangles, m as isBatched, n as defineBudgets, o as countTriangles, p as batchedTriangles, r as parseBudgets, s as geometryCensus, t as checkBudget, u as findBadGeometry, x as walk, y as pathOf } from "./budgets-DQggDhEu.js";
2
+ import { n as summarizeScene, t as auditScene } from "./audit-scene-DHTFd-zs.js";
3
+ // src/patch.ts
3
4
  /** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */
4
5
  const patch = (target, key, value) => {
5
6
  const own = Object.hasOwn(target, key);
@@ -10,8 +11,7 @@ const patch = (target, key, value) => {
10
11
  else delete target[key];
11
12
  };
12
13
  };
13
- //#endregion
14
- //#region src/black-frames.ts
14
+ // src/black-frames.ts
15
15
  /**
16
16
  * Black-frame probe: after each frame, samples a grid of the drawing buffer and counts frames
17
17
  * where several samples are pure black. Catches the flash a NaN pixel makes once bloom spreads it,
@@ -78,8 +78,7 @@ const watchBlackFrames = (renderer, options = {}) => {
78
78
  }
79
79
  };
80
80
  };
81
- //#endregion
82
- //#region src/frames.ts
81
+ // src/frames.ts
83
82
  /**
84
83
  * Waiting on animation frames without hanging: a tab in the background (an agent's browser, an
85
84
  * occluded window) gets none, so every wait gives up after a timeout and says why.
@@ -106,8 +105,7 @@ const nextFrame = (caller, timeout, hint) => new Promise((resolve, reject) => {
106
105
  resolve();
107
106
  });
108
107
  });
109
- //#endregion
110
- //#region src/draw-ledger.ts
108
+ // src/draw-ledger.ts
111
109
  const defaultGroupOf = (name) => name.split(/[:#]/)[0] || name;
112
110
  /** The object's name, or for an unnamed one its named ancestors, type and material. */
113
111
  const nameOf = (object, material) => object.name || `${[...pathOf(object, null, void 0, 2), object.type].join("/")} [${material.name || material.type}]`;
@@ -303,8 +301,7 @@ const printDrawLedger = (ledger) => {
303
301
  console.groupEnd();
304
302
  console.groupEnd();
305
303
  };
306
- //#endregion
307
- //#region src/measure.ts
304
+ // src/measure.ts
308
305
  const multiply = (a, b, out) => {
309
306
  for (let row = 0; row < 4; row += 1) for (let column = 0; column < 4; column += 1) {
310
307
  let sum = 0;
@@ -557,8 +554,7 @@ const measureClearance = (root, a, b, options = {}) => {
557
554
  between: [meshLabel(between[0], scene, tagKey), meshLabel(between[1], scene, tagKey)]
558
555
  };
559
556
  };
560
- //#endregion
561
- //#region src/console-helpers.ts
557
+ // src/console-helpers.ts
562
558
  const NAMESPACE = "threeAudit";
563
559
  const NAMES = [
564
560
  "auditRoot",
@@ -667,7 +663,6 @@ const installAuditHelpers = (root, options = {}) => {
667
663
  if (scope[NAMESPACE] === namespace) delete scope[NAMESPACE];
668
664
  };
669
665
  };
670
- //#endregion
671
- export { auditScene, beginDrawLedger, countDraws, countTriangles, findBadGeometry, findEmptyMeshes, findZFighting, geometryCensus, installAuditHelpers, ledgerTable, listMeshes, measureBounds, measureClearance, printDrawLedger, recordDrawLedger, summarizeScene, triangleCount, watchBlackFrames };
666
+ export { auditScene, beginDrawLedger, checkBudget, countDraws, countTriangles, defineBudgets, findBadGeometry, findEmptyMeshes, findZFighting, geometryCensus, installAuditHelpers, ledgerTable, listMeshes, measureBounds, measureClearance, parseBudgets, printDrawLedger, recordDrawLedger, summarizeScene, triangleCount, triangles, watchBlackFrames };
672
667
 
673
668
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/patch.ts","../src/black-frames.ts","../src/frames.ts","../src/draw-ledger.ts","../src/measure.ts","../src/console-helpers.ts"],"sourcesContent":["/** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */\nexport const patch = <Target extends object, Key extends keyof Target>(\n target: Target,\n key: Key,\n value: Target[Key],\n) => {\n const own = Object.hasOwn(target, key);\n const original = target[key];\n\n target[key] = value;\n\n return () => {\n if (own) {\n target[key] = original;\n } else {\n delete target[key];\n }\n };\n};\n","/**\n * Black-frame probe: after each frame, samples a grid of the drawing buffer and counts frames\n * where several samples are pure black. Catches the flash a NaN pixel makes once bloom spreads it,\n * which lasts one frame in twenty and never shows in a screenshot.\n *\n * WebGL only (`readPixels`). Each sample stalls the GPU pipeline, so watch, read, stop.\n */\nimport { patch } from \"./patch.ts\";\nimport type { AnyObject3D } from \"./scene.ts\";\n\n/** The `WebGLRenderer` methods the probe uses, typed loosely to fit any copy of three's types. */\nexport type ProbeRenderer = {\n render(scene: AnyObject3D, camera: object): void;\n getContext(): unknown;\n getRenderTarget(): unknown;\n};\n\nexport type BlackFrameOptions = {\n /** Samples per side: 3 reads a 3 × 3 grid at the quarter lines. */\n grid?: number;\n /** Black samples that make a frame black. */\n threshold?: number;\n};\n\nexport type BlackFrameWatch = {\n /** Frames sampled since the start or the last `reset()`. */\n readonly frames: number;\n /** Which of those frames (1-based) were black. */\n readonly blackFrames: readonly number[];\n reset: () => void;\n /** Restore the renderer and stop sampling. */\n stop: () => void;\n};\n\ntype ReadPixels = Pick<\n WebGLRenderingContext,\n \"RGBA\" | \"UNSIGNED_BYTE\" | \"drawingBufferHeight\" | \"drawingBufferWidth\" | \"readPixels\"\n>;\n\n/**\n * Sample every frame the app draws to the screen. Several screen renders in one frame (an overlay\n * redrawn after the composer) count once, as the last of them: the image the display shows.\n * Browser only: frames are told apart by `requestAnimationFrame`.\n */\nexport const watchBlackFrames = (\n renderer: ProbeRenderer,\n options: BlackFrameOptions = {},\n): BlackFrameWatch => {\n const context = renderer.getContext() as Partial<ReadPixels> | null;\n\n if (typeof context?.readPixels !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs a WebGLRenderer\");\n }\n\n if (typeof requestAnimationFrame !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs requestAnimationFrame\");\n }\n\n const gl = context as ReadPixels;\n const grid = options.grid ?? 3;\n const threshold = options.threshold ?? 3;\n const pixel = new Uint8Array(4);\n const render = renderer.render;\n let frames = 0;\n let black: number[] = [];\n let pending: boolean | null = null;\n\n const isBlack = () => {\n let count = 0;\n\n for (let i = 1; i <= grid; i += 1) {\n for (let j = 1; j <= grid; j += 1) {\n const x = Math.floor((gl.drawingBufferWidth * i) / (grid + 1));\n const y = Math.floor((gl.drawingBufferHeight * j) / (grid + 1));\n\n gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);\n\n if (pixel[0]! < 4 && pixel[1]! < 4 && pixel[2]! < 4 && pixel[3]! > 250) {\n count += 1;\n }\n }\n }\n\n return count >= threshold;\n };\n\n // Commit the frame's last screen sample once per animation frame.\n let raf = 0;\n const commit = () => {\n if (pending !== null) {\n frames += 1;\n\n if (pending) {\n black.push(frames);\n }\n\n pending = null;\n }\n\n raf = requestAnimationFrame(commit);\n };\n\n raf = requestAnimationFrame(commit);\n\n const restore = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n render.call(this, scene, camera);\n\n // Read before the browser presents (and, without preserveDrawingBuffer, clears) the buffer.\n if (renderer.getRenderTarget() === null) {\n pending = isBlack();\n }\n });\n\n return {\n get frames() {\n return frames;\n },\n get blackFrames() {\n return black;\n },\n reset: () => {\n frames = 0;\n black = [];\n pending = null;\n },\n stop: () => {\n cancelAnimationFrame(raf);\n restore();\n },\n };\n};\n","/**\n * Waiting on animation frames without hanging: a tab in the background (an agent's browser, an\n * occluded window) gets none, so every wait gives up after a timeout and says why.\n */\n\n/** Frames an agent-driven tab should see within this long, in ms. */\nexport const FRAME_TIMEOUT = 2000;\n\nconst noFrame = (caller: string, timeout: number, hint: string) => {\n const hidden = typeof document !== \"undefined\" && document.hidden;\n\n return new Error(\n `three-audit: ${caller} got no animation frame in ${timeout / 1000} s${\n hidden\n ? \" (document.hidden is true: browsers pause requestAnimationFrame in background tabs)\"\n : \"\"\n }. ${hint}`,\n );\n};\n\n/** The next animation frame, or an error naming the cause when none comes within `timeout`. */\nexport const nextFrame = (caller: string, timeout: number, hint: string) =>\n new Promise<void>((resolve, reject) => {\n if (typeof requestAnimationFrame !== \"function\") {\n reject(new Error(`three-audit: ${caller} needs requestAnimationFrame`));\n\n return;\n }\n\n let frame = 0;\n const timer = setTimeout(() => {\n cancelAnimationFrame(frame);\n reject(noFrame(caller, timeout, hint));\n }, timeout);\n\n frame = requestAnimationFrame(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n","/**\n * A ledger of every draw call over one frame: which object drew, in which pass. Exact counts from\n * the renderer itself, where `renderer.info` gives one total (and resets it every `render()`).\n *\n * WebGL only: it wraps `WebGLRenderer.renderBufferDirect`, which every draw goes through, and\n * `render()`, to tell passes apart. Draws with a depth or distance material are the shadow maps.\n */\nimport type { BufferGeometry, Material, Mesh, Object3D } from \"three\";\n\nimport { FRAME_TIMEOUT, nextFrame } from \"./frames.ts\";\nimport { patch } from \"./patch.ts\";\nimport {\n type AnyObject3D,\n batchedTriangles,\n isBatched,\n isInstanced,\n pathOf,\n triangleCount,\n} from \"./scene.ts\";\n\n/**\n * The two `WebGLRenderer` methods the ledger wraps. Typed loosely, so a renderer from any copy of\n * three's types fits.\n */\nexport type LedgerRenderer = {\n render(scene: AnyObject3D, camera: object): void;\n renderBufferDirect(\n camera: object,\n scene: object | null,\n geometry: object,\n material: object,\n object: AnyObject3D,\n group: unknown,\n ): void;\n /** For pass sizes: the target being drawn to, `null` for the screen. */\n getRenderTarget?(): unknown;\n /** For pass sizes: the screen's drawing buffer, device pixels. */\n getContext?(): unknown;\n};\n\nexport type DrawLedgerOptions = {\n /**\n * The group an object name files under. Default: up to the first `:` or `#`, so a bake's\n * `wagon:paint` and a strip's `grass#3` group as `wagon` and `grass`.\n */\n groupOf?: (name: string) => string;\n};\n\nexport type RecordDrawLedgerOptions = DrawLedgerOptions & {\n /**\n * Record one call of this (the app's own render, sync or async) instead of one animation frame.\n * For a tab in the background, where no frames come.\n */\n render?: () => unknown;\n /** How long to wait for each animation frame before failing, in ms. Default 2000. */\n timeout?: number;\n};\n\nexport type LedgerRow = {\n name: string;\n total: number;\n /** Draws per pass name. */\n passes: Record<string, number>;\n};\n\nexport type PassRow = {\n name: string;\n draws: number;\n /** Distinct objects drawn: for `shadow`, the casters. */\n objects: number;\n /** Triangles drawn, instances counted. */\n triangles: number;\n /** Sizes drawn to, in device pixels: `2880×1800`, or `screen 2880×1800`. */\n targets: string[];\n /** Every call was one small mesh through an orthographic camera: a post-processing pass. */\n fullscreen: boolean;\n};\n\nexport type DrawLedger = {\n /**\n * Pass names in the order they first drew: `shadow`, then each `render()` call as its scene's\n * name, or `render 1`, `render 2`… when unnamed (a post-processing chain is several).\n */\n passes: string[];\n /** By group, most draws first. */\n groups: LedgerRow[];\n /** By object name, most draws first. */\n objects: LedgerRow[];\n draws: number;\n /** Each pass's draws, triangles, casters and target sizes, in `passes` order. */\n passStats: PassRow[];\n /** `render()` calls that drew one fullscreen pass, and the pixels they shaded between them. */\n fullscreenPasses: number;\n fullscreenPixels: number;\n};\n\nexport type DrawLedgerRecording = {\n /** Restore the renderer and return what was drawn since `beginDrawLedger`. */\n end: () => DrawLedger;\n};\n\nconst defaultGroupOf = (name: string) => name.split(/[:#]/)[0] || name;\n\n/** The object's name, or for an unnamed one its named ancestors, type and material. */\nconst nameOf = (object: Object3D, material: Material) =>\n object.name ||\n `${[...pathOf(object, null, undefined, 2), object.type].join(\"/\")} [${material.name || material.type}]`;\n\nconst tally = (rows: Map<string, LedgerRow>, name: string, pass: string) => {\n const row = rows.get(name) ?? { name, total: 0, passes: {} };\n\n row.total += 1;\n row.passes[pass] = (row.passes[pass] ?? 0) + 1;\n rows.set(name, row);\n};\n\nconst sorted = (rows: Map<string, LedgerRow>) =>\n [...rows.values()].sort((a, b) => b.total - a.total);\n\n/** Triangles one draw makes: its material group's range, times its instances. */\nconst drawTriangles = (geometry: BufferGeometry, object: Object3D, group: unknown) => {\n const mesh = object as Mesh;\n\n if (mesh.isMesh !== true) {\n return 0;\n }\n\n if (isBatched(mesh)) {\n return batchedTriangles(mesh).triangles;\n }\n\n const range = group as { start?: number; count?: number } | null;\n const elements = geometry.index?.count ?? geometry.attributes.position?.count ?? 0;\n const own =\n range && typeof range.count === \"number\"\n ? Math.floor(Math.max(0, Math.min(range.count, elements - (range.start ?? 0))) / 3)\n : triangleCount(geometry);\n\n return own * (isInstanced(mesh) ? mesh.count : 1);\n};\n\ntype Size = { width: number; height: number; screen: boolean };\n\ntype PassTally = Omit<PassRow, \"objects\" | \"targets\" | \"fullscreen\"> & {\n objects: Set<Object3D>;\n targets: Set<string>;\n calls: number;\n fullscreenCalls: number;\n};\n\ntype Call = {\n pass: string;\n draws: number;\n orthographic: boolean;\n /** Vertices of the one draw so far. */\n vertices: number;\n size: Size | null;\n};\n\n/** Start counting every draw. Call `end()` after the frame to restore the renderer and read it. */\nexport const beginDrawLedger = (\n renderer: LedgerRenderer,\n options: DrawLedgerOptions = {},\n): DrawLedgerRecording => {\n if (typeof renderer.renderBufferDirect !== \"function\") {\n throw new TypeError(\"three-audit: the draw ledger needs a WebGLRenderer\");\n }\n\n const groupOf = options.groupOf ?? defaultGroupOf;\n const render = renderer.render;\n const renderBufferDirect = renderer.renderBufferDirect;\n const passes: string[] = [];\n const objects = new Map<string, LedgerRow>();\n const groups = new Map<string, LedgerRow>();\n const stats = new Map<string, PassTally>();\n const calls: Call[] = [];\n let renders = 0;\n let current = \"direct\";\n let draws = 0;\n let fullscreenPasses = 0;\n let fullscreenPixels = 0;\n\n const statOf = (pass: string) => {\n let stat = stats.get(pass);\n\n if (!stat) {\n stat = {\n name: pass,\n draws: 0,\n triangles: 0,\n objects: new Set(),\n targets: new Set(),\n calls: 0,\n fullscreenCalls: 0,\n };\n stats.set(pass, stat);\n }\n\n return stat;\n };\n\n const sizeNow = (): Size | null => {\n const target = renderer.getRenderTarget?.() as { width?: number; height?: number } | null;\n\n if (target && typeof target.width === \"number\" && typeof target.height === \"number\") {\n return { width: target.width, height: target.height, screen: false };\n }\n\n const gl = renderer.getContext?.() as\n | { drawingBufferWidth?: number; drawingBufferHeight?: number }\n | null\n | undefined;\n\n return target === null && typeof gl?.drawingBufferWidth === \"number\"\n ? { width: gl.drawingBufferWidth, height: gl.drawingBufferHeight ?? 0, screen: true }\n : null;\n };\n\n const restoreRender = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n const outer = current;\n\n renders += 1;\n current = (scene as Partial<Object3D>).name || `render ${renders}`;\n\n const call: Call = {\n pass: current,\n draws: 0,\n orthographic: (camera as { isOrthographicCamera?: boolean }).isOrthographicCamera === true,\n vertices: 0,\n size: null,\n };\n\n calls.push(call);\n\n try {\n render.call(this, scene, camera);\n } finally {\n calls.pop();\n current = outer;\n\n const stat = stats.get(call.pass);\n\n if (stat) {\n stat.calls += 1;\n\n // One small mesh through an orthographic camera: a fullscreen quad or triangle.\n if (call.draws === 1 && call.orthographic && call.vertices <= 6) {\n stat.fullscreenCalls += 1;\n fullscreenPasses += 1;\n fullscreenPixels += call.size ? call.size.width * call.size.height : 0;\n }\n }\n }\n });\n\n const restoreDraw = patch(\n renderer,\n \"renderBufferDirect\",\n function (this: unknown, camera, scene, geometry, material, object, group) {\n const shadow =\n (material as { isMeshDepthMaterial?: boolean }).isMeshDepthMaterial === true ||\n (material as { isMeshDistanceMaterial?: boolean }).isMeshDistanceMaterial === true;\n const pass = shadow ? \"shadow\" : current;\n const name = nameOf(object as unknown as Object3D, material as Material);\n\n if (!passes.includes(pass)) {\n passes.push(pass);\n }\n\n const stat = statOf(pass);\n const size = sizeNow();\n const drawn = geometry as BufferGeometry;\n\n stat.draws += 1;\n stat.triangles += drawTriangles(drawn, object as unknown as Object3D, group);\n stat.objects.add(object as unknown as Object3D);\n\n if (size) {\n stat.targets.add(`${size.screen ? \"screen \" : \"\"}${size.width}×${size.height}`);\n }\n\n const call = calls.at(-1);\n\n if (call && !shadow) {\n call.draws += 1;\n call.vertices = drawn.attributes?.position?.count ?? Infinity;\n call.size = size;\n }\n\n draws += 1;\n tally(objects, name, pass);\n tally(groups, groupOf(name), pass);\n renderBufferDirect.call(this, camera, scene, geometry, material, object, group);\n },\n );\n\n return {\n end: () => {\n restoreDraw();\n restoreRender();\n\n return {\n passes,\n groups: sorted(groups),\n objects: sorted(objects),\n draws,\n passStats: passes.map((pass) => {\n const stat = stats.get(pass)!;\n\n return {\n name: pass,\n draws: stat.draws,\n objects: stat.objects.size,\n triangles: stat.triangles,\n targets: [...stat.targets],\n fullscreen: stat.calls > 0 && stat.fullscreenCalls === stat.calls,\n };\n }),\n fullscreenPasses,\n fullscreenPixels,\n };\n },\n };\n};\n\n/**\n * Record exactly one frame of an app that renders from `requestAnimationFrame` (three's\n * `setAnimationLoop`, React Three Fiber): from one animation frame to the next. A demand-driven\n * loop that renders nothing that frame gives an empty ledger. Browser only.\n *\n * With `render`, records one call of it instead, frames or not. Without, fails after `timeout`\n * when no frame comes (a background tab), rather than waiting for ever.\n */\nexport const recordDrawLedger = async (\n renderer: LedgerRenderer,\n options: RecordDrawLedgerOptions = {},\n): Promise<DrawLedger> => {\n const { render, timeout = FRAME_TIMEOUT, ...ledgerOptions } = options;\n\n if (render) {\n const recording = beginDrawLedger(renderer, ledgerOptions);\n\n try {\n await render();\n } catch (error) {\n recording.end();\n throw error;\n }\n\n return recording.end();\n }\n\n const hint = \"Bring the tab to the front, or pass { render } to record one call of your own.\";\n\n await nextFrame(\"recordDrawLedger\", timeout, hint);\n\n const recording = beginDrawLedger(renderer, ledgerOptions);\n\n try {\n await nextFrame(\"recordDrawLedger\", timeout, hint);\n } catch (error) {\n recording.end();\n throw error;\n }\n\n return recording.end();\n};\n\n/** Rows shaped for `console.table`: one column per pass, then the total. */\nexport const ledgerTable = (rows: LedgerRow[], passes: string[]) =>\n rows.map((row) => ({\n name: row.name,\n ...Object.fromEntries(passes.map((pass) => [pass, row.passes[pass] ?? 0])),\n total: row.total,\n }));\n\n/** The ledger as two console tables: by group, and every object collapsed beneath. */\nexport const printDrawLedger = (ledger: DrawLedger) => {\n /* oxlint-disable no-console -- printing is this function's job */\n console.group(`Draw ledger: ${ledger.draws} draws in one frame`);\n console.table(ledgerTable(ledger.groups, ledger.passes));\n console.table(ledger.passStats.map((pass) => ({ ...pass, targets: pass.targets.join(\", \") })));\n\n if (ledger.fullscreenPasses > 0) {\n console.log(\n `${ledger.fullscreenPasses} fullscreen passes: ${(ledger.fullscreenPixels / 1e6).toFixed(1)} Mpx shaded`,\n );\n }\n\n console.groupCollapsed(\"Every object\");\n console.table(ledgerTable(ledger.objects, ledger.passes));\n console.groupEnd();\n console.groupEnd();\n /* oxlint-enable no-console */\n};\n","/**\n * Measuring a layout: an object's world envelope, and the gap between two objects. The questions\n * a scene raises before anything renders wrong: does the train fit under the crossbar, does the\n * lid clear the hinge.\n *\n * Measured from the vertices as drawn (instances and batches included), not from bounding boxes\n * transformed after the fact, which grow with every rotation.\n */\nimport type { BatchedMesh, Mesh, Object3D } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n type Bakes,\n isBatched,\n isInstanced,\n isMesh,\n meshLabel,\n walk,\n} from \"./scene.ts\";\n\nexport type MeasureOptions = {\n /** `userData` key a string target is looked up by. Default `studioObject`. */\n tagKey?: string;\n /** A bake's parts (default), or its merged meshes: see `Bakes`. The same space either way. */\n bakes?: Bakes | undefined;\n};\n\n/** What to measure: an object, or a tag (`userData[tagKey]`), or failing that an object name. */\nexport type Target = string | AnyObject3D;\n\nexport type Bounds = {\n min: [number, number, number];\n max: [number, number, number];\n size: [number, number, number];\n centre: [number, number, number];\n};\n\nexport type Clearance = {\n /**\n * The smallest gap between the two, in world units. Measured between triangles' bounding boxes:\n * exact for faces square to the axes, a slight underestimate for sloped ones. 0 when they touch\n * or intersect.\n */\n gap: number;\n /** The axis the gap is measured along, when it's straight along one; `null` when diagonal. */\n axis: \"x\" | \"y\" | \"z\" | null;\n /** The closest meshes, one from each side. */\n between: [string, string];\n};\n\n/** A 4 × 4 column-major matrix, as three stores them. */\ntype Elements = ArrayLike<number>;\n\nconst multiply = (a: Elements, b: Elements, out: number[]) => {\n for (let row = 0; row < 4; row += 1) {\n for (let column = 0; column < 4; column += 1) {\n let sum = 0;\n\n for (let k = 0; k < 4; k += 1) {\n sum += a[k * 4 + row]! * b[column * 4 + k]!;\n }\n\n out[column * 4 + row] = sum;\n }\n }\n\n return out;\n};\n\ntype Triangle = (\n ax: number,\n ay: number,\n az: number,\n bx: number,\n by: number,\n bz: number,\n cx: number,\n cy: number,\n cz: number,\n) => void;\n\n/** Every triangle a mesh draws, in world space: each instance of an instanced or batched mesh. */\nconst eachTriangle = (mesh: Mesh, visit: Triangle) => {\n const { index, attributes } = mesh.geometry;\n const position = attributes.position;\n\n if (!position) {\n return;\n }\n\n const world = mesh.matrixWorld.elements;\n const corner = [0, 0, 0, 0, 0, 0, 0, 0, 0];\n const emit = (matrix: Elements, first: number, count: number, indexed: boolean) => {\n for (let k = first; k + 2 < first + count; k += 3) {\n for (let c = 0; c < 3; c += 1) {\n const v = indexed ? 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 corner[c * 3 + axis] =\n matrix[axis]! * x + matrix[axis + 4]! * y + matrix[axis + 8]! * z + matrix[axis + 12]!;\n }\n }\n\n visit(\n ...(corner as [number, number, number, number, number, number, number, number, number]),\n );\n }\n };\n\n if (isBatched(mesh)) {\n const batch = mesh as BatchedMesh;\n const instance = Array.from({ length: 16 }, () => 0);\n const matrix = Array.from({ length: 16 }, () => 0);\n // `getMatrixAt` fills whatever has three's `fromArray`, so no Matrix4 is needed.\n const target = {\n fromArray(array: ArrayLike<number>, offset = 0) {\n for (let i = 0; i < 16; i += 1) {\n instance[i] = array[offset + i]!;\n }\n\n return target;\n },\n };\n\n for (let id = 0; id < batch.maxInstanceCount; id += 1) {\n let range: {\n vertexStart: number;\n vertexCount: number;\n indexStart: number;\n indexCount: number;\n };\n\n try {\n if (!batch.getVisibleAt(id)) {\n continue;\n }\n\n range = batch.getGeometryRangeAt(batch.getGeometryIdAt(id)) as typeof range;\n batch.getMatrixAt(id, target as never);\n } catch {\n continue;\n }\n\n multiply(world, instance, matrix);\n\n if (index) {\n emit(matrix, range.indexStart, range.indexCount, true);\n } else {\n emit(matrix, range.vertexStart, range.vertexCount, false);\n }\n }\n\n return;\n }\n\n const { start, count } = mesh.geometry.drawRange;\n const elements = index?.count ?? position.count;\n const first = Math.min(start, elements);\n const drawn = Math.min(elements, start + count) - first;\n\n if (isInstanced(mesh)) {\n const matrix = Array.from({ length: 16 }, () => 0);\n const instances = mesh.instanceMatrix.array;\n\n for (let i = 0; i < mesh.count; i += 1) {\n multiply(world, Array.prototype.slice.call(instances, i * 16, i * 16 + 16), matrix);\n emit(matrix, first, drawn, index !== null);\n }\n\n return;\n }\n\n emit(world, first, drawn, index !== null);\n};\n\n/** A box as [minX, minY, minZ, maxX, maxY, maxZ]. */\ntype Box = [number, number, number, number, number, number];\n\nconst emptyBox = (): Box => [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];\n\nconst grow = (box: Box, x: number, y: number, z: number) => {\n box[0] = Math.min(box[0], x);\n box[1] = Math.min(box[1], y);\n box[2] = Math.min(box[2], z);\n box[3] = Math.max(box[3], x);\n box[4] = Math.max(box[4], y);\n box[5] = Math.max(box[5], z);\n};\n\n/** Per axis, how far apart two boxes are (0 where they overlap). */\nconst apart = (\n a: ArrayLike<number>,\n ai: number,\n b: ArrayLike<number>,\n bi: number,\n out: number[],\n) => {\n for (let axis = 0; axis < 3; axis += 1) {\n out[axis] = Math.max(0, a[ai + axis]! - b[bi + axis + 3]!, b[bi + axis]! - a[ai + axis + 3]!);\n }\n\n return Math.hypot(out[0]!, out[1]!, out[2]!);\n};\n\nconst round = (value: number) => {\n const rounded = Math.round(value * 1000) / 1000;\n\n return rounded === 0 ? 0 : rounded;\n};\n\n/** The visible meshes a target stands for. */\nconst meshesOf = (\n root: Object3D,\n target: Target,\n { tagKey = \"studioObject\", bakes = \"sources\" }: MeasureOptions,\n role: string,\n) => {\n let objects: Object3D[];\n\n if (typeof target === \"string\") {\n const tagged: Object3D[] = [];\n const named: Object3D[] = [];\n\n walk(root, {}, (object) => {\n const tag: unknown = object.userData[tagKey];\n\n if (tag !== undefined && tag !== null && String(tag) === target) {\n tagged.push(object);\n } else if (object.name === target) {\n named.push(object);\n }\n });\n objects = tagged.length > 0 ? tagged : named;\n\n if (objects.length === 0) {\n throw new Error(\n `three-audit: ${role}: nothing is tagged userData.${tagKey} = \"${target}\" or named \"${target}\"`,\n );\n }\n } else {\n objects = [asObject3D(target)];\n }\n\n const meshes = new Set<Mesh>();\n\n for (const object of objects) {\n walk(object, { visibleOnly: true, bakes }, (child) => {\n if (isMesh(child)) {\n meshes.add(child);\n }\n });\n }\n\n return meshes;\n};\n\n/** A target's world envelope, from its visible meshes' vertices; `null` when it draws nothing. */\nexport const measureBounds = (\n root: AnyObject3D,\n target: Target,\n options: MeasureOptions = {},\n): Bounds | null => {\n const scene = asObject3D(root);\n\n scene.updateWorldMatrix(true, true);\n\n const box = emptyBox();\n\n for (const mesh of meshesOf(scene, target, options, \"bounds\")) {\n eachTriangle(mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {\n grow(box, ax, ay, az);\n grow(box, bx, by, bz);\n grow(box, cx, cy, cz);\n });\n }\n\n if (box[0] > box[3]) {\n return null;\n }\n\n const min = [box[0], box[1], box[2]].map(round) as Bounds[\"min\"];\n const max = [box[3], box[4], box[5]].map(round) as Bounds[\"max\"];\n\n return {\n min,\n max,\n size: [0, 1, 2].map((axis) => round(box[axis + 3]! - box[axis]!)) as Bounds[\"size\"],\n centre: [0, 1, 2].map((axis) => round((box[axis]! + box[axis + 3]!) / 2)) as Bounds[\"centre\"],\n };\n};\n\n/** Triangle boxes past this many pairs, a mesh pair is measured by its whole boxes instead. */\nconst PAIR_LIMIT = 4_000_000;\n\ntype Measured = { mesh: Mesh; box: Box; triangles: Float64Array | null };\n\n/**\n * The gap between two targets, and which of their meshes come closest. Throws when a target\n * matches nothing; `null` when either draws nothing. Meshes in both targets are left out of `a`.\n */\nexport const measureClearance = (\n root: AnyObject3D,\n a: Target,\n b: Target,\n options: MeasureOptions = {},\n): Clearance | null => {\n const scene = asObject3D(root);\n const tagKey = options.tagKey ?? \"studioObject\";\n\n scene.updateWorldMatrix(true, true);\n\n const inB = meshesOf(scene, b, options, \"clearance b\");\n const inA = [...meshesOf(scene, a, options, \"clearance a\")].filter((mesh) => !inB.has(mesh));\n const measure = (mesh: Mesh): Measured => {\n const box = emptyBox();\n\n eachTriangle(mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {\n grow(box, ax, ay, az);\n grow(box, bx, by, bz);\n grow(box, cx, cy, cz);\n });\n\n return { mesh, box, triangles: null };\n };\n const sideA = inA.map(measure).filter(({ box }) => box[0] <= box[3]);\n const sideB = [...inB].map(measure).filter(({ box }) => box[0] <= box[3]);\n\n if (sideA.length === 0 || sideB.length === 0) {\n return null;\n }\n\n const trianglesOf = (side: Measured) => {\n if (!side.triangles) {\n const boxes: number[] = [];\n\n eachTriangle(side.mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {\n boxes.push(\n Math.min(ax, bx, cx),\n Math.min(ay, by, cy),\n Math.min(az, bz, cz),\n Math.max(ax, bx, cx),\n Math.max(ay, by, cy),\n Math.max(az, bz, cz),\n );\n });\n side.triangles = Float64Array.from(boxes);\n }\n\n return side.triangles;\n };\n\n const axes = [0, 0, 0];\n const pairs = sideA\n .flatMap((left) =>\n sideB.map((right) => ({ left, right, distance: apart(left.box, 0, right.box, 0, axes) })),\n )\n .sort((x, y) => x.distance - y.distance);\n let best = Infinity;\n let bestAxes = [0, 0, 0];\n let between: [Mesh, Mesh] = [pairs[0]!.left.mesh, pairs[0]!.right.mesh];\n\n for (const { left, right, distance } of pairs) {\n if (distance >= best) {\n break;\n }\n\n const leftTriangles = trianglesOf(left);\n const rightTriangles = trianglesOf(right);\n // Only triangles that could beat the best so far, measured against the other mesh's box.\n const near = (triangles: Float64Array, other: Box) => {\n const kept: number[] = [];\n\n for (let t = 0; t < triangles.length; t += 6) {\n if (apart(triangles, t, other, 0, axes) < best) {\n kept.push(t);\n }\n }\n\n return kept;\n };\n const nearLeft = near(leftTriangles, right.box);\n const nearRight = near(rightTriangles, left.box);\n\n if (nearLeft.length * nearRight.length > PAIR_LIMIT) {\n if (distance < best) {\n best = distance;\n apart(left.box, 0, right.box, 0, axes);\n bestAxes = [...axes];\n between = [left.mesh, right.mesh];\n }\n\n continue;\n }\n\n for (const i of nearLeft) {\n for (const j of nearRight) {\n const gap = apart(leftTriangles, i, rightTriangles, j, axes);\n\n if (gap < best) {\n best = gap;\n bestAxes = [...axes];\n between = [left.mesh, right.mesh];\n }\n }\n }\n }\n\n const separated = bestAxes.flatMap((value, axis) => (value > 0 ? [axis] : []));\n\n return {\n gap: round(best),\n axis: separated.length === 1 ? ([\"x\", \"y\", \"z\"] as const)[separated[0]!]! : null,\n between: [meshLabel(between[0], scene, tagKey), meshLabel(between[1], scene, tagKey)],\n };\n};\n","/**\n * The checks as console globals, for a dev page or a headless browser's `eval`: `summary()`,\n * `tris()`, `meshes()`, `census()`, `audit()`, `zfight()`, `report()`, `bbox()`, `clearance()`,\n * and with a renderer `ledger()`, `beginLedger()` and `blackFrames()`. All of them also sit on\n * `threeAudit`, which is the only global with `globals: false`.\n */\nimport type { BatchedMesh } from \"three\";\n\nimport { auditScene, type AuditSummaryOptions, summarizeScene } from \"./audit-scene.ts\";\nimport { findBadGeometry } from \"./bad-geometry.ts\";\nimport { type ProbeRenderer, watchBlackFrames } from \"./black-frames.ts\";\nimport {\n beginDrawLedger,\n type DrawLedgerOptions,\n type LedgerRenderer,\n printDrawLedger,\n recordDrawLedger,\n type RecordDrawLedgerOptions,\n} from \"./draw-ledger.ts\";\nimport { FRAME_TIMEOUT, nextFrame } from \"./frames.ts\";\nimport { measureBounds, measureClearance, type Target } from \"./measure.ts\";\nimport type { AnyObject3D, Bakes, Skip } from \"./scene.ts\";\nimport { type CensusOptions, countTriangles, geometryCensus, listMeshes } from \"./triangles.ts\";\nimport { findZFighting } from \"./z-fighting.ts\";\n\nexport type AuditHelpersOptions = {\n /** A `WebGLRenderer`, for `ledger()`, `beginLedger()` and `blackFrames()`. */\n renderer?: (LedgerRenderer & ProbeRenderer) | undefined;\n skip?: Skip | undefined;\n tagKey?: string;\n /** What checks see of a bake: its parts (default) or its merged meshes. See `Bakes`. */\n bakes?: Bakes | undefined;\n /** Names geometries inside a batch in `census()`: see `geometryCensus`'s `label`. */\n batchLabel?: ((batch: BatchedMesh, geometryId: number) => string | undefined) | undefined;\n /**\n * Also put each helper on `globalThis` under its short name (`tris`, `report`…). Default true;\n * false leaves only `threeAudit`, for pages where short names collide with other tooling.\n */\n globals?: boolean;\n};\n\nconst NAMESPACE = \"threeAudit\";\n\nconst NAMES = [\n \"auditRoot\",\n \"summary\",\n \"tris\",\n \"meshes\",\n \"census\",\n \"audit\",\n \"zfight\",\n \"report\",\n \"bbox\",\n \"clearance\",\n \"ledger\",\n \"beginLedger\",\n \"blackFrames\",\n] as const;\n\nconst needsRenderer = (name: string) => () => {\n throw new Error(`three-audit: ${name}() needs installAuditHelpers(scene, { renderer })`);\n};\n\n/**\n * Put the helpers on `globalThis`, and on `globalThis.threeAudit`. Returns a function that\n * removes them.\n *\n * vanilla: installAuditHelpers(scene, { renderer })\n * R3F: const { scene, gl } = useThree();\n * useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);\n */\nexport const installAuditHelpers = (root: AnyObject3D, options: AuditHelpersOptions = {}) => {\n const { renderer, skip, batchLabel, bakes } = options;\n const tagKey = options.tagKey ?? \"studioObject\";\n\n const helpers: Record<(typeof NAMES)[number], unknown> = {\n auditRoot: root,\n summary: (tag?: string, summaryOptions: Pick<AuditSummaryOptions, \"gap\" | \"zFighting\"> = {}) =>\n summarizeScene(root, {\n skip,\n tagKey,\n bakes,\n ...summaryOptions,\n ...(tag === undefined ? {} : { tag }),\n }),\n tris: () => countTriangles(root, { skip }),\n meshes: () => listMeshes(root, { skip }),\n census: (censusOptions: Pick<CensusOptions, \"budget\" | \"label\"> = {}) =>\n geometryCensus(root, { skip, label: batchLabel, ...censusOptions }),\n audit: () => findBadGeometry(root, { skip, tagKey, bakes }),\n zfight: (gap?: number, self?: boolean) =>\n findZFighting(root, {\n skip,\n tagKey,\n bakes,\n ...(gap === undefined ? {} : { gap }),\n ...(self === undefined ? {} : { self }),\n }),\n report: (tag?: string) =>\n auditScene(root, { skip, tagKey, bakes, ...(tag === undefined ? {} : { tag }) }),\n bbox: (target: Target) => measureBounds(root, target, { tagKey, bakes }),\n clearance: (a: Target, b: Target) => measureClearance(root, a, b, { tagKey, bakes }),\n ledger: renderer\n ? async (ledgerOptions?: RecordDrawLedgerOptions) => {\n const ledger = await recordDrawLedger(renderer, ledgerOptions);\n\n printDrawLedger(ledger);\n\n return ledger;\n }\n : needsRenderer(\"ledger\"),\n beginLedger: renderer\n ? (ledgerOptions?: DrawLedgerOptions) => {\n const recording = beginDrawLedger(renderer, ledgerOptions);\n\n return {\n end: () => {\n const ledger = recording.end();\n\n printDrawLedger(ledger);\n\n return ledger;\n },\n };\n }\n : needsRenderer(\"beginLedger\"),\n blackFrames: renderer\n ? async (seconds = 10) => {\n await nextFrame(\n \"blackFrames()\",\n FRAME_TIMEOUT,\n \"Black frames only show in a visible tab: bring it to the front.\",\n );\n\n const watch = watchBlackFrames(renderer);\n\n return new Promise<{ frames: number; blackFrames: readonly number[] }>((resolve) => {\n setTimeout(() => {\n watch.stop();\n resolve({ frames: watch.frames, blackFrames: watch.blackFrames });\n }, seconds * 1000);\n });\n }\n : needsRenderer(\"blackFrames\"),\n };\n const namespace = { ...helpers };\n const scope = globalThis as Record<string, unknown>;\n\n if (options.globals !== false) {\n Object.assign(scope, helpers);\n }\n\n scope[NAMESPACE] = namespace;\n\n return () => {\n for (const name of NAMES) {\n if (scope[name] === helpers[name]) {\n delete scope[name];\n }\n }\n\n if (scope[NAMESPACE] === namespace) {\n delete scope[NAMESPACE];\n }\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/patch.ts","../src/black-frames.ts","../src/frames.ts","../src/draw-ledger.ts","../src/measure.ts","../src/console-helpers.ts"],"sourcesContent":["/** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */\nexport const patch = <Target extends object, Key extends keyof Target>(\n target: Target,\n key: Key,\n value: Target[Key],\n) => {\n const own = Object.hasOwn(target, key);\n const original = target[key];\n\n target[key] = value;\n\n return () => {\n if (own) {\n target[key] = original;\n } else {\n delete target[key];\n }\n };\n};\n","/**\n * Black-frame probe: after each frame, samples a grid of the drawing buffer and counts frames\n * where several samples are pure black. Catches the flash a NaN pixel makes once bloom spreads it,\n * which lasts one frame in twenty and never shows in a screenshot.\n *\n * WebGL only (`readPixels`). Each sample stalls the GPU pipeline, so watch, read, stop.\n */\nimport { patch } from \"./patch.ts\";\nimport type { AnyObject3D } from \"./scene.ts\";\n\n/** The `WebGLRenderer` methods the probe uses, typed loosely to fit any copy of three's types. */\nexport type ProbeRenderer = {\n render(scene: AnyObject3D, camera: object): void;\n getContext(): unknown;\n getRenderTarget(): unknown;\n};\n\nexport type BlackFrameOptions = {\n /** Samples per side: 3 reads a 3 × 3 grid at the quarter lines. */\n grid?: number;\n /** Black samples that make a frame black. */\n threshold?: number;\n};\n\nexport type BlackFrameWatch = {\n /** Frames sampled since the start or the last `reset()`. */\n readonly frames: number;\n /** Which of those frames (1-based) were black. */\n readonly blackFrames: readonly number[];\n reset: () => void;\n /** Restore the renderer and stop sampling. */\n stop: () => void;\n};\n\ntype ReadPixels = Pick<\n WebGLRenderingContext,\n \"RGBA\" | \"UNSIGNED_BYTE\" | \"drawingBufferHeight\" | \"drawingBufferWidth\" | \"readPixels\"\n>;\n\n/**\n * Sample every frame the app draws to the screen. Several screen renders in one frame (an overlay\n * redrawn after the composer) count once, as the last of them: the image the display shows.\n * Browser only: frames are told apart by `requestAnimationFrame`.\n */\nexport const watchBlackFrames = (\n renderer: ProbeRenderer,\n options: BlackFrameOptions = {},\n): BlackFrameWatch => {\n const context = renderer.getContext() as Partial<ReadPixels> | null;\n\n if (typeof context?.readPixels !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs a WebGLRenderer\");\n }\n\n if (typeof requestAnimationFrame !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs requestAnimationFrame\");\n }\n\n const gl = context as ReadPixels;\n const grid = options.grid ?? 3;\n const threshold = options.threshold ?? 3;\n const pixel = new Uint8Array(4);\n const render = renderer.render;\n let frames = 0;\n let black: number[] = [];\n let pending: boolean | null = null;\n\n const isBlack = () => {\n let count = 0;\n\n for (let i = 1; i <= grid; i += 1) {\n for (let j = 1; j <= grid; j += 1) {\n const x = Math.floor((gl.drawingBufferWidth * i) / (grid + 1));\n const y = Math.floor((gl.drawingBufferHeight * j) / (grid + 1));\n\n gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);\n\n if (pixel[0]! < 4 && pixel[1]! < 4 && pixel[2]! < 4 && pixel[3]! > 250) {\n count += 1;\n }\n }\n }\n\n return count >= threshold;\n };\n\n // Commit the frame's last screen sample once per animation frame.\n let raf = 0;\n const commit = () => {\n if (pending !== null) {\n frames += 1;\n\n if (pending) {\n black.push(frames);\n }\n\n pending = null;\n }\n\n raf = requestAnimationFrame(commit);\n };\n\n raf = requestAnimationFrame(commit);\n\n const restore = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n render.call(this, scene, camera);\n\n // Read before the browser presents (and, without preserveDrawingBuffer, clears) the buffer.\n if (renderer.getRenderTarget() === null) {\n pending = isBlack();\n }\n });\n\n return {\n get frames() {\n return frames;\n },\n get blackFrames() {\n return black;\n },\n reset: () => {\n frames = 0;\n black = [];\n pending = null;\n },\n stop: () => {\n cancelAnimationFrame(raf);\n restore();\n },\n };\n};\n","/**\n * Waiting on animation frames without hanging: a tab in the background (an agent's browser, an\n * occluded window) gets none, so every wait gives up after a timeout and says why.\n */\n\n/** Frames an agent-driven tab should see within this long, in ms. */\nexport const FRAME_TIMEOUT = 2000;\n\nconst noFrame = (caller: string, timeout: number, hint: string) => {\n const hidden = typeof document !== \"undefined\" && document.hidden;\n\n return new Error(\n `three-audit: ${caller} got no animation frame in ${timeout / 1000} s${\n hidden\n ? \" (document.hidden is true: browsers pause requestAnimationFrame in background tabs)\"\n : \"\"\n }. ${hint}`,\n );\n};\n\n/** The next animation frame, or an error naming the cause when none comes within `timeout`. */\nexport const nextFrame = (caller: string, timeout: number, hint: string) =>\n new Promise<void>((resolve, reject) => {\n if (typeof requestAnimationFrame !== \"function\") {\n reject(new Error(`three-audit: ${caller} needs requestAnimationFrame`));\n\n return;\n }\n\n let frame = 0;\n const timer = setTimeout(() => {\n cancelAnimationFrame(frame);\n reject(noFrame(caller, timeout, hint));\n }, timeout);\n\n frame = requestAnimationFrame(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n","/**\n * A ledger of every draw call over one frame: which object drew, in which pass. Exact counts from\n * the renderer itself, where `renderer.info` gives one total (and resets it every `render()`).\n *\n * WebGL only: it wraps `WebGLRenderer.renderBufferDirect`, which every draw goes through, and\n * `render()`, to tell passes apart. Draws with a depth or distance material are the shadow maps.\n */\nimport type { BufferGeometry, Material, Mesh, Object3D } from \"three\";\n\nimport { FRAME_TIMEOUT, nextFrame } from \"./frames.ts\";\nimport { patch } from \"./patch.ts\";\nimport {\n type AnyObject3D,\n batchedTriangles,\n isBatched,\n isInstanced,\n pathOf,\n triangleCount,\n} from \"./scene.ts\";\n\n/**\n * The two `WebGLRenderer` methods the ledger wraps. Typed loosely, so a renderer from any copy of\n * three's types fits.\n */\nexport type LedgerRenderer = {\n render(scene: AnyObject3D, camera: object): void;\n renderBufferDirect(\n camera: object,\n scene: object | null,\n geometry: object,\n material: object,\n object: AnyObject3D,\n group: unknown,\n ): void;\n /** For pass sizes: the target being drawn to, `null` for the screen. */\n getRenderTarget?(): unknown;\n /** For pass sizes: the screen's drawing buffer, device pixels. */\n getContext?(): unknown;\n};\n\nexport type DrawLedgerOptions = {\n /**\n * The group an object name files under. Default: up to the first `:` or `#`, so a bake's\n * `wagon:paint` and a strip's `grass#3` group as `wagon` and `grass`.\n */\n groupOf?: (name: string) => string;\n};\n\nexport type RecordDrawLedgerOptions = DrawLedgerOptions & {\n /**\n * Record one call of this (the app's own render, sync or async) instead of one animation frame.\n * For a tab in the background, where no frames come.\n */\n render?: () => unknown;\n /** How long to wait for each animation frame before failing, in ms. Default 2000. */\n timeout?: number;\n};\n\nexport type LedgerRow = {\n name: string;\n total: number;\n /** Draws per pass name. */\n passes: Record<string, number>;\n};\n\nexport type PassRow = {\n name: string;\n draws: number;\n /** Distinct objects drawn: for `shadow`, the casters. */\n objects: number;\n /** Triangles drawn, instances counted. */\n triangles: number;\n /** Sizes drawn to, in device pixels: `2880×1800`, or `screen 2880×1800`. */\n targets: string[];\n /** Every call was one small mesh through an orthographic camera: a post-processing pass. */\n fullscreen: boolean;\n};\n\nexport type DrawLedger = {\n /**\n * Pass names in the order they first drew: `shadow`, then each `render()` call as its scene's\n * name, or `render 1`, `render 2`… when unnamed (a post-processing chain is several).\n */\n passes: string[];\n /** By group, most draws first. */\n groups: LedgerRow[];\n /** By object name, most draws first. */\n objects: LedgerRow[];\n draws: number;\n /** Each pass's draws, triangles, casters and target sizes, in `passes` order. */\n passStats: PassRow[];\n /** `render()` calls that drew one fullscreen pass, and the pixels they shaded between them. */\n fullscreenPasses: number;\n fullscreenPixels: number;\n};\n\nexport type DrawLedgerRecording = {\n /** Restore the renderer and return what was drawn since `beginDrawLedger`. */\n end: () => DrawLedger;\n};\n\nconst defaultGroupOf = (name: string) => name.split(/[:#]/)[0] || name;\n\n/** The object's name, or for an unnamed one its named ancestors, type and material. */\nconst nameOf = (object: Object3D, material: Material) =>\n object.name ||\n `${[...pathOf(object, null, undefined, 2), object.type].join(\"/\")} [${material.name || material.type}]`;\n\nconst tally = (rows: Map<string, LedgerRow>, name: string, pass: string) => {\n const row = rows.get(name) ?? { name, total: 0, passes: {} };\n\n row.total += 1;\n row.passes[pass] = (row.passes[pass] ?? 0) + 1;\n rows.set(name, row);\n};\n\nconst sorted = (rows: Map<string, LedgerRow>) =>\n [...rows.values()].sort((a, b) => b.total - a.total);\n\n/** Triangles one draw makes: its material group's range, times its instances. */\nconst drawTriangles = (geometry: BufferGeometry, object: Object3D, group: unknown) => {\n const mesh = object as Mesh;\n\n if (mesh.isMesh !== true) {\n return 0;\n }\n\n if (isBatched(mesh)) {\n return batchedTriangles(mesh).triangles;\n }\n\n const range = group as { start?: number; count?: number } | null;\n const elements = geometry.index?.count ?? geometry.attributes.position?.count ?? 0;\n const own =\n range && typeof range.count === \"number\"\n ? Math.floor(Math.max(0, Math.min(range.count, elements - (range.start ?? 0))) / 3)\n : triangleCount(geometry);\n\n return own * (isInstanced(mesh) ? mesh.count : 1);\n};\n\ntype Size = { width: number; height: number; screen: boolean };\n\ntype PassTally = Omit<PassRow, \"objects\" | \"targets\" | \"fullscreen\"> & {\n objects: Set<Object3D>;\n targets: Set<string>;\n calls: number;\n fullscreenCalls: number;\n};\n\ntype Call = {\n pass: string;\n draws: number;\n orthographic: boolean;\n /** Vertices of the one draw so far. */\n vertices: number;\n size: Size | null;\n};\n\n/** Start counting every draw. Call `end()` after the frame to restore the renderer and read it. */\nexport const beginDrawLedger = (\n renderer: LedgerRenderer,\n options: DrawLedgerOptions = {},\n): DrawLedgerRecording => {\n if (typeof renderer.renderBufferDirect !== \"function\") {\n throw new TypeError(\"three-audit: the draw ledger needs a WebGLRenderer\");\n }\n\n const groupOf = options.groupOf ?? defaultGroupOf;\n const render = renderer.render;\n const renderBufferDirect = renderer.renderBufferDirect;\n const passes: string[] = [];\n const objects = new Map<string, LedgerRow>();\n const groups = new Map<string, LedgerRow>();\n const stats = new Map<string, PassTally>();\n const calls: Call[] = [];\n let renders = 0;\n let current = \"direct\";\n let draws = 0;\n let fullscreenPasses = 0;\n let fullscreenPixels = 0;\n\n const statOf = (pass: string) => {\n let stat = stats.get(pass);\n\n if (!stat) {\n stat = {\n name: pass,\n draws: 0,\n triangles: 0,\n objects: new Set(),\n targets: new Set(),\n calls: 0,\n fullscreenCalls: 0,\n };\n stats.set(pass, stat);\n }\n\n return stat;\n };\n\n const sizeNow = (): Size | null => {\n const target = renderer.getRenderTarget?.() as { width?: number; height?: number } | null;\n\n if (target && typeof target.width === \"number\" && typeof target.height === \"number\") {\n return { width: target.width, height: target.height, screen: false };\n }\n\n const gl = renderer.getContext?.() as\n | { drawingBufferWidth?: number; drawingBufferHeight?: number }\n | null\n | undefined;\n\n return target === null && typeof gl?.drawingBufferWidth === \"number\"\n ? { width: gl.drawingBufferWidth, height: gl.drawingBufferHeight ?? 0, screen: true }\n : null;\n };\n\n const restoreRender = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n const outer = current;\n\n renders += 1;\n current = (scene as Partial<Object3D>).name || `render ${renders}`;\n\n const call: Call = {\n pass: current,\n draws: 0,\n orthographic: (camera as { isOrthographicCamera?: boolean }).isOrthographicCamera === true,\n vertices: 0,\n size: null,\n };\n\n calls.push(call);\n\n try {\n render.call(this, scene, camera);\n } finally {\n calls.pop();\n current = outer;\n\n const stat = stats.get(call.pass);\n\n if (stat) {\n stat.calls += 1;\n\n // One small mesh through an orthographic camera: a fullscreen quad or triangle.\n if (call.draws === 1 && call.orthographic && call.vertices <= 6) {\n stat.fullscreenCalls += 1;\n fullscreenPasses += 1;\n fullscreenPixels += call.size ? call.size.width * call.size.height : 0;\n }\n }\n }\n });\n\n const restoreDraw = patch(\n renderer,\n \"renderBufferDirect\",\n function (this: unknown, camera, scene, geometry, material, object, group) {\n const shadow =\n (material as { isMeshDepthMaterial?: boolean }).isMeshDepthMaterial === true ||\n (material as { isMeshDistanceMaterial?: boolean }).isMeshDistanceMaterial === true;\n const pass = shadow ? \"shadow\" : current;\n const name = nameOf(object as unknown as Object3D, material as Material);\n\n if (!passes.includes(pass)) {\n passes.push(pass);\n }\n\n const stat = statOf(pass);\n const size = sizeNow();\n const drawn = geometry as BufferGeometry;\n\n stat.draws += 1;\n stat.triangles += drawTriangles(drawn, object as unknown as Object3D, group);\n stat.objects.add(object as unknown as Object3D);\n\n if (size) {\n stat.targets.add(`${size.screen ? \"screen \" : \"\"}${size.width}×${size.height}`);\n }\n\n const call = calls.at(-1);\n\n if (call && !shadow) {\n call.draws += 1;\n call.vertices = drawn.attributes?.position?.count ?? Infinity;\n call.size = size;\n }\n\n draws += 1;\n tally(objects, name, pass);\n tally(groups, groupOf(name), pass);\n renderBufferDirect.call(this, camera, scene, geometry, material, object, group);\n },\n );\n\n return {\n end: () => {\n restoreDraw();\n restoreRender();\n\n return {\n passes,\n groups: sorted(groups),\n objects: sorted(objects),\n draws,\n passStats: passes.map((pass) => {\n const stat = stats.get(pass)!;\n\n return {\n name: pass,\n draws: stat.draws,\n objects: stat.objects.size,\n triangles: stat.triangles,\n targets: [...stat.targets],\n fullscreen: stat.calls > 0 && stat.fullscreenCalls === stat.calls,\n };\n }),\n fullscreenPasses,\n fullscreenPixels,\n };\n },\n };\n};\n\n/**\n * Record exactly one frame of an app that renders from `requestAnimationFrame` (three's\n * `setAnimationLoop`, React Three Fiber): from one animation frame to the next. A demand-driven\n * loop that renders nothing that frame gives an empty ledger. Browser only.\n *\n * With `render`, records one call of it instead, frames or not. Without, fails after `timeout`\n * when no frame comes (a background tab), rather than waiting for ever.\n */\nexport const recordDrawLedger = async (\n renderer: LedgerRenderer,\n options: RecordDrawLedgerOptions = {},\n): Promise<DrawLedger> => {\n const { render, timeout = FRAME_TIMEOUT, ...ledgerOptions } = options;\n\n if (render) {\n const recording = beginDrawLedger(renderer, ledgerOptions);\n\n try {\n await render();\n } catch (error) {\n recording.end();\n throw error;\n }\n\n return recording.end();\n }\n\n const hint = \"Bring the tab to the front, or pass { render } to record one call of your own.\";\n\n await nextFrame(\"recordDrawLedger\", timeout, hint);\n\n const recording = beginDrawLedger(renderer, ledgerOptions);\n\n try {\n await nextFrame(\"recordDrawLedger\", timeout, hint);\n } catch (error) {\n recording.end();\n throw error;\n }\n\n return recording.end();\n};\n\n/** Rows shaped for `console.table`: one column per pass, then the total. */\nexport const ledgerTable = (rows: LedgerRow[], passes: string[]) =>\n rows.map((row) => ({\n name: row.name,\n ...Object.fromEntries(passes.map((pass) => [pass, row.passes[pass] ?? 0])),\n total: row.total,\n }));\n\n/** The ledger as two console tables: by group, and every object collapsed beneath. */\nexport const printDrawLedger = (ledger: DrawLedger) => {\n /* oxlint-disable no-console -- printing is this function's job */\n console.group(`Draw ledger: ${ledger.draws} draws in one frame`);\n console.table(ledgerTable(ledger.groups, ledger.passes));\n console.table(ledger.passStats.map((pass) => ({ ...pass, targets: pass.targets.join(\", \") })));\n\n if (ledger.fullscreenPasses > 0) {\n console.log(\n `${ledger.fullscreenPasses} fullscreen passes: ${(ledger.fullscreenPixels / 1e6).toFixed(1)} Mpx shaded`,\n );\n }\n\n console.groupCollapsed(\"Every object\");\n console.table(ledgerTable(ledger.objects, ledger.passes));\n console.groupEnd();\n console.groupEnd();\n /* oxlint-enable no-console */\n};\n","/**\n * Measuring a layout: an object's world envelope, and the gap between two objects. The questions\n * a scene raises before anything renders wrong: does the train fit under the crossbar, does the\n * lid clear the hinge.\n *\n * Measured from the vertices as drawn (instances and batches included), not from bounding boxes\n * transformed after the fact, which grow with every rotation.\n */\nimport type { BatchedMesh, Mesh, Object3D } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n type Bakes,\n isBatched,\n isInstanced,\n isMesh,\n meshLabel,\n walk,\n} from \"./scene.ts\";\n\nexport type MeasureOptions = {\n /** `userData` key a string target is looked up by. Default `studioObject`. */\n tagKey?: string;\n /** A bake's parts (default), or its merged meshes: see `Bakes`. The same space either way. */\n bakes?: Bakes | undefined;\n};\n\n/** What to measure: an object, or a tag (`userData[tagKey]`), or failing that an object name. */\nexport type Target = string | AnyObject3D;\n\nexport type Bounds = {\n min: [number, number, number];\n max: [number, number, number];\n size: [number, number, number];\n centre: [number, number, number];\n};\n\nexport type Clearance = {\n /**\n * The smallest gap between the two, in world units. Measured between triangles' bounding boxes:\n * exact for faces square to the axes, a slight underestimate for sloped ones. 0 when they touch\n * or intersect.\n */\n gap: number;\n /** The axis the gap is measured along, when it's straight along one; `null` when diagonal. */\n axis: \"x\" | \"y\" | \"z\" | null;\n /** The closest meshes, one from each side. */\n between: [string, string];\n};\n\n/** A 4 × 4 column-major matrix, as three stores them. */\ntype Elements = ArrayLike<number>;\n\nconst multiply = (a: Elements, b: Elements, out: number[]) => {\n for (let row = 0; row < 4; row += 1) {\n for (let column = 0; column < 4; column += 1) {\n let sum = 0;\n\n for (let k = 0; k < 4; k += 1) {\n sum += a[k * 4 + row]! * b[column * 4 + k]!;\n }\n\n out[column * 4 + row] = sum;\n }\n }\n\n return out;\n};\n\ntype Triangle = (\n ax: number,\n ay: number,\n az: number,\n bx: number,\n by: number,\n bz: number,\n cx: number,\n cy: number,\n cz: number,\n) => void;\n\n/** Every triangle a mesh draws, in world space: each instance of an instanced or batched mesh. */\nconst eachTriangle = (mesh: Mesh, visit: Triangle) => {\n const { index, attributes } = mesh.geometry;\n const position = attributes.position;\n\n if (!position) {\n return;\n }\n\n const world = mesh.matrixWorld.elements;\n const corner = [0, 0, 0, 0, 0, 0, 0, 0, 0];\n const emit = (matrix: Elements, first: number, count: number, indexed: boolean) => {\n for (let k = first; k + 2 < first + count; k += 3) {\n for (let c = 0; c < 3; c += 1) {\n const v = indexed ? 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 corner[c * 3 + axis] =\n matrix[axis]! * x + matrix[axis + 4]! * y + matrix[axis + 8]! * z + matrix[axis + 12]!;\n }\n }\n\n visit(\n ...(corner as [number, number, number, number, number, number, number, number, number]),\n );\n }\n };\n\n if (isBatched(mesh)) {\n const batch = mesh as BatchedMesh;\n const instance = Array.from({ length: 16 }, () => 0);\n const matrix = Array.from({ length: 16 }, () => 0);\n // `getMatrixAt` fills whatever has three's `fromArray`, so no Matrix4 is needed.\n const target = {\n fromArray(array: ArrayLike<number>, offset = 0) {\n for (let i = 0; i < 16; i += 1) {\n instance[i] = array[offset + i]!;\n }\n\n return target;\n },\n };\n\n for (let id = 0; id < batch.maxInstanceCount; id += 1) {\n let range: {\n vertexStart: number;\n vertexCount: number;\n indexStart: number;\n indexCount: number;\n };\n\n try {\n if (!batch.getVisibleAt(id)) {\n continue;\n }\n\n range = batch.getGeometryRangeAt(batch.getGeometryIdAt(id)) as typeof range;\n batch.getMatrixAt(id, target as never);\n } catch {\n continue;\n }\n\n multiply(world, instance, matrix);\n\n if (index) {\n emit(matrix, range.indexStart, range.indexCount, true);\n } else {\n emit(matrix, range.vertexStart, range.vertexCount, false);\n }\n }\n\n return;\n }\n\n const { start, count } = mesh.geometry.drawRange;\n const elements = index?.count ?? position.count;\n const first = Math.min(start, elements);\n const drawn = Math.min(elements, start + count) - first;\n\n if (isInstanced(mesh)) {\n const matrix = Array.from({ length: 16 }, () => 0);\n const instances = mesh.instanceMatrix.array;\n\n for (let i = 0; i < mesh.count; i += 1) {\n multiply(world, Array.prototype.slice.call(instances, i * 16, i * 16 + 16), matrix);\n emit(matrix, first, drawn, index !== null);\n }\n\n return;\n }\n\n emit(world, first, drawn, index !== null);\n};\n\n/** A box as [minX, minY, minZ, maxX, maxY, maxZ]. */\ntype Box = [number, number, number, number, number, number];\n\nconst emptyBox = (): Box => [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity];\n\nconst grow = (box: Box, x: number, y: number, z: number) => {\n box[0] = Math.min(box[0], x);\n box[1] = Math.min(box[1], y);\n box[2] = Math.min(box[2], z);\n box[3] = Math.max(box[3], x);\n box[4] = Math.max(box[4], y);\n box[5] = Math.max(box[5], z);\n};\n\n/** Per axis, how far apart two boxes are (0 where they overlap). */\nconst apart = (\n a: ArrayLike<number>,\n ai: number,\n b: ArrayLike<number>,\n bi: number,\n out: number[],\n) => {\n for (let axis = 0; axis < 3; axis += 1) {\n out[axis] = Math.max(0, a[ai + axis]! - b[bi + axis + 3]!, b[bi + axis]! - a[ai + axis + 3]!);\n }\n\n return Math.hypot(out[0]!, out[1]!, out[2]!);\n};\n\nconst round = (value: number) => {\n const rounded = Math.round(value * 1000) / 1000;\n\n return rounded === 0 ? 0 : rounded;\n};\n\n/** The visible meshes a target stands for. */\nconst meshesOf = (\n root: Object3D,\n target: Target,\n { tagKey = \"studioObject\", bakes = \"sources\" }: MeasureOptions,\n role: string,\n) => {\n let objects: Object3D[];\n\n if (typeof target === \"string\") {\n const tagged: Object3D[] = [];\n const named: Object3D[] = [];\n\n walk(root, {}, (object) => {\n const tag: unknown = object.userData[tagKey];\n\n if (tag !== undefined && tag !== null && String(tag) === target) {\n tagged.push(object);\n } else if (object.name === target) {\n named.push(object);\n }\n });\n objects = tagged.length > 0 ? tagged : named;\n\n if (objects.length === 0) {\n throw new Error(\n `three-audit: ${role}: nothing is tagged userData.${tagKey} = \"${target}\" or named \"${target}\"`,\n );\n }\n } else {\n objects = [asObject3D(target)];\n }\n\n const meshes = new Set<Mesh>();\n\n for (const object of objects) {\n walk(object, { visibleOnly: true, bakes }, (child) => {\n if (isMesh(child)) {\n meshes.add(child);\n }\n });\n }\n\n return meshes;\n};\n\n/** A target's world envelope, from its visible meshes' vertices; `null` when it draws nothing. */\nexport const measureBounds = (\n root: AnyObject3D,\n target: Target,\n options: MeasureOptions = {},\n): Bounds | null => {\n const scene = asObject3D(root);\n\n scene.updateWorldMatrix(true, true);\n\n const box = emptyBox();\n\n for (const mesh of meshesOf(scene, target, options, \"bounds\")) {\n eachTriangle(mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {\n grow(box, ax, ay, az);\n grow(box, bx, by, bz);\n grow(box, cx, cy, cz);\n });\n }\n\n if (box[0] > box[3]) {\n return null;\n }\n\n const min = [box[0], box[1], box[2]].map(round) as Bounds[\"min\"];\n const max = [box[3], box[4], box[5]].map(round) as Bounds[\"max\"];\n\n return {\n min,\n max,\n size: [0, 1, 2].map((axis) => round(box[axis + 3]! - box[axis]!)) as Bounds[\"size\"],\n centre: [0, 1, 2].map((axis) => round((box[axis]! + box[axis + 3]!) / 2)) as Bounds[\"centre\"],\n };\n};\n\n/** Triangle boxes past this many pairs, a mesh pair is measured by its whole boxes instead. */\nconst PAIR_LIMIT = 4_000_000;\n\ntype Measured = { mesh: Mesh; box: Box; triangles: Float64Array | null };\n\n/**\n * The gap between two targets, and which of their meshes come closest. Throws when a target\n * matches nothing; `null` when either draws nothing. Meshes in both targets are left out of `a`.\n */\nexport const measureClearance = (\n root: AnyObject3D,\n a: Target,\n b: Target,\n options: MeasureOptions = {},\n): Clearance | null => {\n const scene = asObject3D(root);\n const tagKey = options.tagKey ?? \"studioObject\";\n\n scene.updateWorldMatrix(true, true);\n\n const inB = meshesOf(scene, b, options, \"clearance b\");\n const inA = [...meshesOf(scene, a, options, \"clearance a\")].filter((mesh) => !inB.has(mesh));\n const measure = (mesh: Mesh): Measured => {\n const box = emptyBox();\n\n eachTriangle(mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {\n grow(box, ax, ay, az);\n grow(box, bx, by, bz);\n grow(box, cx, cy, cz);\n });\n\n return { mesh, box, triangles: null };\n };\n const sideA = inA.map(measure).filter(({ box }) => box[0] <= box[3]);\n const sideB = [...inB].map(measure).filter(({ box }) => box[0] <= box[3]);\n\n if (sideA.length === 0 || sideB.length === 0) {\n return null;\n }\n\n const trianglesOf = (side: Measured) => {\n if (!side.triangles) {\n const boxes: number[] = [];\n\n eachTriangle(side.mesh, (ax, ay, az, bx, by, bz, cx, cy, cz) => {\n boxes.push(\n Math.min(ax, bx, cx),\n Math.min(ay, by, cy),\n Math.min(az, bz, cz),\n Math.max(ax, bx, cx),\n Math.max(ay, by, cy),\n Math.max(az, bz, cz),\n );\n });\n side.triangles = Float64Array.from(boxes);\n }\n\n return side.triangles;\n };\n\n const axes = [0, 0, 0];\n const pairs = sideA\n .flatMap((left) =>\n sideB.map((right) => ({ left, right, distance: apart(left.box, 0, right.box, 0, axes) })),\n )\n .sort((x, y) => x.distance - y.distance);\n let best = Infinity;\n let bestAxes = [0, 0, 0];\n let between: [Mesh, Mesh] = [pairs[0]!.left.mesh, pairs[0]!.right.mesh];\n\n for (const { left, right, distance } of pairs) {\n if (distance >= best) {\n break;\n }\n\n const leftTriangles = trianglesOf(left);\n const rightTriangles = trianglesOf(right);\n // Only triangles that could beat the best so far, measured against the other mesh's box.\n const near = (triangles: Float64Array, other: Box) => {\n const kept: number[] = [];\n\n for (let t = 0; t < triangles.length; t += 6) {\n if (apart(triangles, t, other, 0, axes) < best) {\n kept.push(t);\n }\n }\n\n return kept;\n };\n const nearLeft = near(leftTriangles, right.box);\n const nearRight = near(rightTriangles, left.box);\n\n if (nearLeft.length * nearRight.length > PAIR_LIMIT) {\n if (distance < best) {\n best = distance;\n apart(left.box, 0, right.box, 0, axes);\n bestAxes = [...axes];\n between = [left.mesh, right.mesh];\n }\n\n continue;\n }\n\n for (const i of nearLeft) {\n for (const j of nearRight) {\n const gap = apart(leftTriangles, i, rightTriangles, j, axes);\n\n if (gap < best) {\n best = gap;\n bestAxes = [...axes];\n between = [left.mesh, right.mesh];\n }\n }\n }\n }\n\n const separated = bestAxes.flatMap((value, axis) => (value > 0 ? [axis] : []));\n\n return {\n gap: round(best),\n axis: separated.length === 1 ? ([\"x\", \"y\", \"z\"] as const)[separated[0]!]! : null,\n between: [meshLabel(between[0], scene, tagKey), meshLabel(between[1], scene, tagKey)],\n };\n};\n","/**\n * The checks as console globals, for a dev page or a headless browser's `eval`: `summary()`,\n * `tris()`, `meshes()`, `census()`, `audit()`, `zfight()`, `report()`, `bbox()`, `clearance()`,\n * and with a renderer `ledger()`, `beginLedger()` and `blackFrames()`. All of them also sit on\n * `threeAudit`, which is the only global with `globals: false`.\n */\nimport type { BatchedMesh } from \"three\";\n\nimport { auditScene, type AuditSummaryOptions, summarizeScene } from \"./audit-scene.ts\";\nimport { findBadGeometry } from \"./bad-geometry.ts\";\nimport { type ProbeRenderer, watchBlackFrames } from \"./black-frames.ts\";\nimport {\n beginDrawLedger,\n type DrawLedgerOptions,\n type LedgerRenderer,\n printDrawLedger,\n recordDrawLedger,\n type RecordDrawLedgerOptions,\n} from \"./draw-ledger.ts\";\nimport { FRAME_TIMEOUT, nextFrame } from \"./frames.ts\";\nimport { measureBounds, measureClearance, type Target } from \"./measure.ts\";\nimport type { AnyObject3D, Bakes, Skip } from \"./scene.ts\";\nimport { type CensusOptions, countTriangles, geometryCensus, listMeshes } from \"./triangles.ts\";\nimport { findZFighting } from \"./z-fighting.ts\";\n\nexport type AuditHelpersOptions = {\n /** A `WebGLRenderer`, for `ledger()`, `beginLedger()` and `blackFrames()`. */\n renderer?: (LedgerRenderer & ProbeRenderer) | undefined;\n skip?: Skip | undefined;\n tagKey?: string;\n /** What checks see of a bake: its parts (default) or its merged meshes. See `Bakes`. */\n bakes?: Bakes | undefined;\n /** Names geometries inside a batch in `census()`: see `geometryCensus`'s `label`. */\n batchLabel?: ((batch: BatchedMesh, geometryId: number) => string | undefined) | undefined;\n /**\n * Also put each helper on `globalThis` under its short name (`tris`, `report`…). Default true;\n * false leaves only `threeAudit`, for pages where short names collide with other tooling.\n */\n globals?: boolean;\n};\n\nconst NAMESPACE = \"threeAudit\";\n\nconst NAMES = [\n \"auditRoot\",\n \"summary\",\n \"tris\",\n \"meshes\",\n \"census\",\n \"audit\",\n \"zfight\",\n \"report\",\n \"bbox\",\n \"clearance\",\n \"ledger\",\n \"beginLedger\",\n \"blackFrames\",\n] as const;\n\nconst needsRenderer = (name: string) => () => {\n throw new Error(`three-audit: ${name}() needs installAuditHelpers(scene, { renderer })`);\n};\n\n/**\n * Put the helpers on `globalThis`, and on `globalThis.threeAudit`. Returns a function that\n * removes them.\n *\n * vanilla: installAuditHelpers(scene, { renderer })\n * R3F: const { scene, gl } = useThree();\n * useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);\n */\nexport const installAuditHelpers = (root: AnyObject3D, options: AuditHelpersOptions = {}) => {\n const { renderer, skip, batchLabel, bakes } = options;\n const tagKey = options.tagKey ?? \"studioObject\";\n\n const helpers: Record<(typeof NAMES)[number], unknown> = {\n auditRoot: root,\n summary: (tag?: string, summaryOptions: Pick<AuditSummaryOptions, \"gap\" | \"zFighting\"> = {}) =>\n summarizeScene(root, {\n skip,\n tagKey,\n bakes,\n ...summaryOptions,\n ...(tag === undefined ? {} : { tag }),\n }),\n tris: () => countTriangles(root, { skip }),\n meshes: () => listMeshes(root, { skip }),\n census: (censusOptions: Pick<CensusOptions, \"budget\" | \"label\"> = {}) =>\n geometryCensus(root, { skip, label: batchLabel, ...censusOptions }),\n audit: () => findBadGeometry(root, { skip, tagKey, bakes }),\n zfight: (gap?: number, self?: boolean) =>\n findZFighting(root, {\n skip,\n tagKey,\n bakes,\n ...(gap === undefined ? {} : { gap }),\n ...(self === undefined ? {} : { self }),\n }),\n report: (tag?: string) =>\n auditScene(root, { skip, tagKey, bakes, ...(tag === undefined ? {} : { tag }) }),\n bbox: (target: Target) => measureBounds(root, target, { tagKey, bakes }),\n clearance: (a: Target, b: Target) => measureClearance(root, a, b, { tagKey, bakes }),\n ledger: renderer\n ? async (ledgerOptions?: RecordDrawLedgerOptions) => {\n const ledger = await recordDrawLedger(renderer, ledgerOptions);\n\n printDrawLedger(ledger);\n\n return ledger;\n }\n : needsRenderer(\"ledger\"),\n beginLedger: renderer\n ? (ledgerOptions?: DrawLedgerOptions) => {\n const recording = beginDrawLedger(renderer, ledgerOptions);\n\n return {\n end: () => {\n const ledger = recording.end();\n\n printDrawLedger(ledger);\n\n return ledger;\n },\n };\n }\n : needsRenderer(\"beginLedger\"),\n blackFrames: renderer\n ? async (seconds = 10) => {\n await nextFrame(\n \"blackFrames()\",\n FRAME_TIMEOUT,\n \"Black frames only show in a visible tab: bring it to the front.\",\n );\n\n const watch = watchBlackFrames(renderer);\n\n return new Promise<{ frames: number; blackFrames: readonly number[] }>((resolve) => {\n setTimeout(() => {\n watch.stop();\n resolve({ frames: watch.frames, blackFrames: watch.blackFrames });\n }, seconds * 1000);\n });\n }\n : needsRenderer(\"blackFrames\"),\n };\n const namespace = { ...helpers };\n const scope = globalThis as Record<string, unknown>;\n\n if (options.globals !== false) {\n Object.assign(scope, helpers);\n }\n\n scope[NAMESPACE] = namespace;\n\n return () => {\n for (const name of NAMES) {\n if (scope[name] === helpers[name]) {\n delete scope[name];\n }\n }\n\n if (scope[NAMESPACE] === namespace) {\n delete scope[NAMESPACE];\n }\n };\n};\n"],"mappings":";;;;AACA,MAAa,SACX,QACA,KACA,UACG;CACH,MAAM,MAAM,OAAO,OAAO,QAAQ,GAAG;CACrC,MAAM,WAAW,OAAO;CAExB,OAAO,OAAO;CAEd,aAAa;EACX,IAAI,KACF,OAAO,OAAO;OAEd,OAAO,OAAO;CAElB;AACF;;;;;;;;;;;;;;AC0BA,MAAa,oBACX,UACA,UAA6B,CAAC,MACV;CACpB,MAAM,UAAU,SAAS,WAAW;CAEpC,IAAI,OAAO,SAAS,eAAe,YACjC,MAAM,IAAI,UAAU,0DAA0D;CAGhF,IAAI,OAAO,0BAA0B,YACnC,MAAM,IAAI,UAAU,gEAAgE;CAGtF,MAAM,KAAK;CACX,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,wBAAQ,IAAI,WAAW,CAAC;CAC9B,MAAM,SAAS,SAAS;CACxB,IAAI,SAAS;CACb,IAAI,QAAkB,CAAC;CACvB,IAAI,UAA0B;CAE9B,MAAM,gBAAgB;EACpB,IAAI,QAAQ;EAEZ,KAAK,IAAI,IAAI,GAAG,KAAK,MAAM,KAAK,GAC9B,KAAK,IAAI,IAAI,GAAG,KAAK,MAAM,KAAK,GAAG;GACjC,MAAM,IAAI,KAAK,MAAO,GAAG,qBAAqB,KAAM,OAAO,EAAE;GAC7D,MAAM,IAAI,KAAK,MAAO,GAAG,sBAAsB,KAAM,OAAO,EAAE;GAE9D,GAAG,WAAW,GAAG,GAAG,GAAG,GAAG,GAAG,MAAM,GAAG,eAAe,KAAK;GAE1D,IAAI,MAAM,KAAM,KAAK,MAAM,KAAM,KAAK,MAAM,KAAM,KAAK,MAAM,KAAM,KACjE,SAAS;EAEb;EAGF,OAAO,SAAS;CAClB;CAGA,IAAI,MAAM;CACV,MAAM,eAAe;EACnB,IAAI,YAAY,MAAM;GACpB,UAAU;GAEV,IAAI,SACF,MAAM,KAAK,MAAM;GAGnB,UAAU;EACZ;EAEA,MAAM,sBAAsB,MAAM;CACpC;CAEA,MAAM,sBAAsB,MAAM;CAElC,MAAM,UAAU,MAAM,UAAU,UAAU,SAAyB,OAAO,QAAQ;EAChF,OAAO,KAAK,MAAM,OAAO,MAAM;EAG/B,IAAI,SAAS,gBAAgB,MAAM,MACjC,UAAU,QAAQ;CAEtB,CAAC;CAED,OAAO;EACL,IAAI,SAAS;GACX,OAAO;EACT;EACA,IAAI,cAAc;GAChB,OAAO;EACT;EACA,aAAa;GACX,SAAS;GACT,QAAQ,CAAC;GACT,UAAU;EACZ;EACA,YAAY;GACV,qBAAqB,GAAG;GACxB,QAAQ;EACV;CACF;AACF;;;;;;;AC5HA,MAAa,gBAAgB;AAE7B,MAAM,WAAW,QAAgB,SAAiB,SAAiB;CACjE,MAAM,SAAS,OAAO,aAAa,eAAe,SAAS;CAE3D,uBAAO,IAAI,MACT,gBAAgB,OAAO,6BAA6B,UAAU,IAAK,IACjE,SACI,wFACA,GACL,IAAI,MACP;AACF;;AAGA,MAAa,aAAa,QAAgB,SAAiB,SACzD,IAAI,SAAe,SAAS,WAAW;CACrC,IAAI,OAAO,0BAA0B,YAAY;EAC/C,uBAAO,IAAI,MAAM,gBAAgB,OAAO,6BAA6B,CAAC;EAEtE;CACF;CAEA,IAAI,QAAQ;CACZ,MAAM,QAAQ,iBAAiB;EAC7B,qBAAqB,KAAK;EAC1B,OAAO,QAAQ,QAAQ,SAAS,IAAI,CAAC;CACvC,GAAG,OAAO;CAEV,QAAQ,4BAA4B;EAClC,aAAa,KAAK;EAClB,QAAQ;CACV,CAAC;AACH,CAAC;;AC8DH,MAAM,kBAAkB,SAAiB,KAAK,MAAM,MAAM,CAAC,CAAC,MAAM;;AAGlE,MAAM,UAAU,QAAkB,aAChC,OAAO,QACP,GAAG,CAAC,GAAG,OAAO,QAAQ,MAAM,KAAA,GAAW,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC,KAAK,GAAG,EAAE,IAAI,SAAS,QAAQ,SAAS,KAAK;AAEvG,MAAM,SAAS,MAA8B,MAAc,SAAiB;CAC1E,MAAM,MAAM,KAAK,IAAI,IAAI,KAAK;EAAE;EAAM,OAAO;EAAG,QAAQ,CAAC;CAAE;CAE3D,IAAI,SAAS;CACb,IAAI,OAAO,SAAS,IAAI,OAAO,SAAS,KAAK;CAC7C,KAAK,IAAI,MAAM,GAAG;AACpB;AAEA,MAAM,UAAU,SACd,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;;AAGrD,MAAM,iBAAiB,UAA0B,QAAkB,UAAmB;CACpF,MAAM,OAAO;CAEb,IAAI,KAAK,WAAW,MAClB,OAAO;CAGT,IAAI,UAAU,IAAI,GAChB,OAAO,iBAAiB,IAAI,CAAC,CAAC;CAGhC,MAAM,QAAQ;CACd,MAAM,WAAW,SAAS,OAAO,SAAS,SAAS,WAAW,UAAU,SAAS;CAMjF,QAJE,SAAS,OAAO,MAAM,UAAU,WAC5B,KAAK,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,OAAO,YAAY,MAAM,SAAS,EAAE,CAAC,IAAI,CAAC,IAChF,cAAc,QAAQ,MAEd,YAAY,IAAI,IAAI,KAAK,QAAQ;AACjD;;AAqBA,MAAa,mBACX,UACA,UAA6B,CAAC,MACN;CACxB,IAAI,OAAO,SAAS,uBAAuB,YACzC,MAAM,IAAI,UAAU,oDAAoD;CAG1E,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,SAAS,SAAS;CACxB,MAAM,qBAAqB,SAAS;CACpC,MAAM,SAAmB,CAAC;CAC1B,MAAM,0BAAU,IAAI,IAAuB;CAC3C,MAAM,yBAAS,IAAI,IAAuB;CAC1C,MAAM,wBAAQ,IAAI,IAAuB;CACzC,MAAM,QAAgB,CAAC;CACvB,IAAI,UAAU;CACd,IAAI,UAAU;CACd,IAAI,QAAQ;CACZ,IAAI,mBAAmB;CACvB,IAAI,mBAAmB;CAEvB,MAAM,UAAU,SAAiB;EAC/B,IAAI,OAAO,MAAM,IAAI,IAAI;EAEzB,IAAI,CAAC,MAAM;GACT,OAAO;IACL,MAAM;IACN,OAAO;IACP,WAAW;IACX,yBAAS,IAAI,IAAI;IACjB,yBAAS,IAAI,IAAI;IACjB,OAAO;IACP,iBAAiB;GACnB;GACA,MAAM,IAAI,MAAM,IAAI;EACtB;EAEA,OAAO;CACT;CAEA,MAAM,gBAA6B;EACjC,MAAM,SAAS,SAAS,kBAAkB;EAE1C,IAAI,UAAU,OAAO,OAAO,UAAU,YAAY,OAAO,OAAO,WAAW,UACzE,OAAO;GAAE,OAAO,OAAO;GAAO,QAAQ,OAAO;GAAQ,QAAQ;EAAM;EAGrE,MAAM,KAAK,SAAS,aAAa;EAKjC,OAAO,WAAW,QAAQ,OAAO,IAAI,uBAAuB,WACxD;GAAE,OAAO,GAAG;GAAoB,QAAQ,GAAG,uBAAuB;GAAG,QAAQ;EAAK,IAClF;CACN;CAEA,MAAM,gBAAgB,MAAM,UAAU,UAAU,SAAyB,OAAO,QAAQ;EACtF,MAAM,QAAQ;EAEd,WAAW;EACX,UAAW,MAA4B,QAAQ,UAAU;EAEzD,MAAM,OAAa;GACjB,MAAM;GACN,OAAO;GACP,cAAe,OAA8C,yBAAyB;GACtF,UAAU;GACV,MAAM;EACR;EAEA,MAAM,KAAK,IAAI;EAEf,IAAI;GACF,OAAO,KAAK,MAAM,OAAO,MAAM;EACjC,UAAU;GACR,MAAM,IAAI;GACV,UAAU;GAEV,MAAM,OAAO,MAAM,IAAI,KAAK,IAAI;GAEhC,IAAI,MAAM;IACR,KAAK,SAAS;IAGd,IAAI,KAAK,UAAU,KAAK,KAAK,gBAAgB,KAAK,YAAY,GAAG;KAC/D,KAAK,mBAAmB;KACxB,oBAAoB;KACpB,oBAAoB,KAAK,OAAO,KAAK,KAAK,QAAQ,KAAK,KAAK,SAAS;IACvE;GACF;EACF;CACF,CAAC;CAED,MAAM,cAAc,MAClB,UACA,sBACA,SAAyB,QAAQ,OAAO,UAAU,UAAU,QAAQ,OAAO;EACzE,MAAM,SACH,SAA+C,wBAAwB,QACvE,SAAkD,2BAA2B;EAChF,MAAM,OAAO,SAAS,WAAW;EACjC,MAAM,OAAO,OAAO,QAA+B,QAAoB;EAEvE,IAAI,CAAC,OAAO,SAAS,IAAI,GACvB,OAAO,KAAK,IAAI;EAGlB,MAAM,OAAO,OAAO,IAAI;EACxB,MAAM,OAAO,QAAQ;EACrB,MAAM,QAAQ;EAEd,KAAK,SAAS;EACd,KAAK,aAAa,cAAc,OAAO,QAA+B,KAAK;EAC3E,KAAK,QAAQ,IAAI,MAA6B;EAE9C,IAAI,MACF,KAAK,QAAQ,IAAI,GAAG,KAAK,SAAS,YAAY,KAAK,KAAK,MAAM,GAAG,KAAK,QAAQ;EAGhF,MAAM,OAAO,MAAM,GAAG,EAAE;EAExB,IAAI,QAAQ,CAAC,QAAQ;GACnB,KAAK,SAAS;GACd,KAAK,WAAW,MAAM,YAAY,UAAU,SAAS;GACrD,KAAK,OAAO;EACd;EAEA,SAAS;EACT,MAAM,SAAS,MAAM,IAAI;EACzB,MAAM,QAAQ,QAAQ,IAAI,GAAG,IAAI;EACjC,mBAAmB,KAAK,MAAM,QAAQ,OAAO,UAAU,UAAU,QAAQ,KAAK;CAChF,CACF;CAEA,OAAO,EACL,WAAW;EACT,YAAY;EACZ,cAAc;EAEd,OAAO;GACL;GACA,QAAQ,OAAO,MAAM;GACrB,SAAS,OAAO,OAAO;GACvB;GACA,WAAW,OAAO,KAAK,SAAS;IAC9B,MAAM,OAAO,MAAM,IAAI,IAAI;IAE3B,OAAO;KACL,MAAM;KACN,OAAO,KAAK;KACZ,SAAS,KAAK,QAAQ;KACtB,WAAW,KAAK;KAChB,SAAS,CAAC,GAAG,KAAK,OAAO;KACzB,YAAY,KAAK,QAAQ,KAAK,KAAK,oBAAoB,KAAK;IAC9D;GACF,CAAC;GACD;GACA;EACF;CACF,EACF;AACF;;;;;;;;;AAUA,MAAa,mBAAmB,OAC9B,UACA,UAAmC,CAAC,MACZ;CACxB,MAAM,EAAE,QAAQ,UAAU,eAAe,GAAG,kBAAkB;CAE9D,IAAI,QAAQ;EACV,MAAM,YAAY,gBAAgB,UAAU,aAAa;EAEzD,IAAI;GACF,MAAM,OAAO;EACf,SAAS,OAAO;GACd,UAAU,IAAI;GACd,MAAM;EACR;EAEA,OAAO,UAAU,IAAI;CACvB;CAEA,MAAM,OAAO;CAEb,MAAM,UAAU,oBAAoB,SAAS,IAAI;CAEjD,MAAM,YAAY,gBAAgB,UAAU,aAAa;CAEzD,IAAI;EACF,MAAM,UAAU,oBAAoB,SAAS,IAAI;CACnD,SAAS,OAAO;EACd,UAAU,IAAI;EACd,MAAM;CACR;CAEA,OAAO,UAAU,IAAI;AACvB;;AAGA,MAAa,eAAe,MAAmB,WAC7C,KAAK,KAAK,SAAS;CACjB,MAAM,IAAI;CACV,GAAG,OAAO,YAAY,OAAO,KAAK,SAAS,CAAC,MAAM,IAAI,OAAO,SAAS,CAAC,CAAC,CAAC;CACzE,OAAO,IAAI;AACb,EAAE;;AAGJ,MAAa,mBAAmB,WAAuB;CAErD,QAAQ,MAAM,gBAAgB,OAAO,MAAM,oBAAoB;CAC/D,QAAQ,MAAM,YAAY,OAAO,QAAQ,OAAO,MAAM,CAAC;CACvD,QAAQ,MAAM,OAAO,UAAU,KAAK,UAAU;EAAE,GAAG;EAAM,SAAS,KAAK,QAAQ,KAAK,IAAI;CAAE,EAAE,CAAC;CAE7F,IAAI,OAAO,mBAAmB,GAC5B,QAAQ,IACN,GAAG,OAAO,iBAAiB,uBAAuB,OAAO,mBAAmB,IAAA,CAAK,QAAQ,CAAC,EAAE,YAC9F;CAGF,QAAQ,eAAe,cAAc;CACrC,QAAQ,MAAM,YAAY,OAAO,SAAS,OAAO,MAAM,CAAC;CACxD,QAAQ,SAAS;CACjB,QAAQ,SAAS;AAEnB;;ACpVA,MAAM,YAAY,GAAa,GAAa,QAAkB;CAC5D,KAAK,IAAI,MAAM,GAAG,MAAM,GAAG,OAAO,GAChC,KAAK,IAAI,SAAS,GAAG,SAAS,GAAG,UAAU,GAAG;EAC5C,IAAI,MAAM;EAEV,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAC1B,OAAO,EAAE,IAAI,IAAI,OAAQ,EAAE,SAAS,IAAI;EAG1C,IAAI,SAAS,IAAI,OAAO;CAC1B;CAGF,OAAO;AACT;;AAeA,MAAM,gBAAgB,MAAY,UAAoB;CACpD,MAAM,EAAE,OAAO,eAAe,KAAK;CACnC,MAAM,WAAW,WAAW;CAE5B,IAAI,CAAC,UACH;CAGF,MAAM,QAAQ,KAAK,YAAY;CAC/B,MAAM,SAAS;EAAC;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;EAAG;CAAC;CACzC,MAAM,QAAQ,QAAkB,OAAe,OAAe,YAAqB;EACjF,KAAK,IAAI,IAAI,OAAO,IAAI,IAAI,QAAQ,OAAO,KAAK,GAAG;GACjD,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,GAAG;IAC7B,MAAM,IAAI,UAAU,MAAO,KAAK,IAAI,CAAC,IAAI,IAAI;IAC7C,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,GACnC,OAAO,IAAI,IAAI,QACb,OAAO,QAAS,IAAI,OAAO,OAAO,KAAM,IAAI,OAAO,OAAO,KAAM,IAAI,OAAO,OAAO;GAExF;GAEA,MACE,GAAI,MACN;EACF;CACF;CAEA,IAAI,UAAU,IAAI,GAAG;EACnB,MAAM,QAAQ;EACd,MAAM,WAAW,MAAM,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;EACnD,MAAM,SAAS,MAAM,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;EAEjD,MAAM,SAAS,EACb,UAAU,OAA0B,SAAS,GAAG;GAC9C,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,KAAK,GAC3B,SAAS,KAAK,MAAM,SAAS;GAG/B,OAAO;EACT,EACF;EAEA,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,kBAAkB,MAAM,GAAG;GACrD,IAAI;GAOJ,IAAI;IACF,IAAI,CAAC,MAAM,aAAa,EAAE,GACxB;IAGF,QAAQ,MAAM,mBAAmB,MAAM,gBAAgB,EAAE,CAAC;IAC1D,MAAM,YAAY,IAAI,MAAe;GACvC,QAAQ;IACN;GACF;GAEA,SAAS,OAAO,UAAU,MAAM;GAEhC,IAAI,OACF,KAAK,QAAQ,MAAM,YAAY,MAAM,YAAY,IAAI;QAErD,KAAK,QAAQ,MAAM,aAAa,MAAM,aAAa,KAAK;EAE5D;EAEA;CACF;CAEA,MAAM,EAAE,OAAO,UAAU,KAAK,SAAS;CACvC,MAAM,WAAW,OAAO,SAAS,SAAS;CAC1C,MAAM,QAAQ,KAAK,IAAI,OAAO,QAAQ;CACtC,MAAM,QAAQ,KAAK,IAAI,UAAU,QAAQ,KAAK,IAAI;CAElD,IAAI,YAAY,IAAI,GAAG;EACrB,MAAM,SAAS,MAAM,KAAK,EAAE,QAAQ,GAAG,SAAS,CAAC;EACjD,MAAM,YAAY,KAAK,eAAe;EAEtC,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,OAAO,KAAK,GAAG;GACtC,SAAS,OAAO,MAAM,UAAU,MAAM,KAAK,WAAW,IAAI,IAAI,IAAI,KAAK,EAAE,GAAG,MAAM;GAClF,KAAK,QAAQ,OAAO,OAAO,UAAU,IAAI;EAC3C;EAEA;CACF;CAEA,KAAK,OAAO,OAAO,OAAO,UAAU,IAAI;AAC1C;AAKA,MAAM,iBAAsB;CAAC;CAAU;CAAU;CAAU;CAAW;CAAW;AAAS;AAE1F,MAAM,QAAQ,KAAU,GAAW,GAAW,MAAc;CAC1D,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC;CAC3B,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC;CAC3B,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC;CAC3B,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC;CAC3B,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC;CAC3B,IAAI,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC;AAC7B;;AAGA,MAAM,SACJ,GACA,IACA,GACA,IACA,QACG;CACH,KAAK,IAAI,OAAO,GAAG,OAAO,GAAG,QAAQ,GACnC,IAAI,QAAQ,KAAK,IAAI,GAAG,EAAE,KAAK,QAAS,EAAE,KAAK,OAAO,IAAK,EAAE,KAAK,QAAS,EAAE,KAAK,OAAO,EAAG;CAG9F,OAAO,KAAK,MAAM,IAAI,IAAK,IAAI,IAAK,IAAI,EAAG;AAC7C;AAEA,MAAM,SAAS,UAAkB;CAC/B,MAAM,UAAU,KAAK,MAAM,QAAQ,GAAI,IAAI;CAE3C,OAAO,YAAY,IAAI,IAAI;AAC7B;;AAGA,MAAM,YACJ,MACA,QACA,EAAE,SAAS,gBAAgB,QAAQ,aACnC,SACG;CACH,IAAI;CAEJ,IAAI,OAAO,WAAW,UAAU;EAC9B,MAAM,SAAqB,CAAC;EAC5B,MAAM,QAAoB,CAAC;EAE3B,KAAK,MAAM,CAAC,IAAI,WAAW;GACzB,MAAM,MAAe,OAAO,SAAS;GAErC,IAAI,QAAQ,KAAA,KAAa,QAAQ,QAAQ,OAAO,GAAG,MAAM,QACvD,OAAO,KAAK,MAAM;QACb,IAAI,OAAO,SAAS,QACzB,MAAM,KAAK,MAAM;EAErB,CAAC;EACD,UAAU,OAAO,SAAS,IAAI,SAAS;EAEvC,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,MACR,gBAAgB,KAAK,+BAA+B,OAAO,MAAM,OAAO,cAAc,OAAO,EAC/F;CAEJ,OACE,UAAU,CAAC,WAAW,MAAM,CAAC;CAG/B,MAAM,yBAAS,IAAI,IAAU;CAE7B,KAAK,MAAM,UAAU,SACnB,KAAK,QAAQ;EAAE,aAAa;EAAM;CAAM,IAAI,UAAU;EACpD,IAAI,OAAO,KAAK,GACd,OAAO,IAAI,KAAK;CAEpB,CAAC;CAGH,OAAO;AACT;;AAGA,MAAa,iBACX,MACA,QACA,UAA0B,CAAC,MACT;CAClB,MAAM,QAAQ,WAAW,IAAI;CAE7B,MAAM,kBAAkB,MAAM,IAAI;CAElC,MAAM,MAAM,SAAS;CAErB,KAAK,MAAM,QAAQ,SAAS,OAAO,QAAQ,SAAS,QAAQ,GAC1D,aAAa,OAAO,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO;EACzD,KAAK,KAAK,IAAI,IAAI,EAAE;EACpB,KAAK,KAAK,IAAI,IAAI,EAAE;EACpB,KAAK,KAAK,IAAI,IAAI,EAAE;CACtB,CAAC;CAGH,IAAI,IAAI,KAAK,IAAI,IACf,OAAO;CAMT,OAAO;EACL,KAJU;GAAC,IAAI;GAAI,IAAI;GAAI,IAAI;EAAE,CAAC,CAAC,IAAI,KAIrC;EACF,KAJU;GAAC,IAAI;GAAI,IAAI;GAAI,IAAI;EAAE,CAAC,CAAC,IAAI,KAIrC;EACF,MAAM;GAAC;GAAG;GAAG;EAAC,CAAC,CAAC,KAAK,SAAS,MAAM,IAAI,OAAO,KAAM,IAAI,KAAM,CAAC;EAChE,QAAQ;GAAC;GAAG;GAAG;EAAC,CAAC,CAAC,KAAK,SAAS,OAAO,IAAI,QAAS,IAAI,OAAO,MAAO,CAAC,CAAC;CAC1E;AACF;;AAGA,MAAM,aAAa;;;;;AAQnB,MAAa,oBACX,MACA,GACA,GACA,UAA0B,CAAC,MACN;CACrB,MAAM,QAAQ,WAAW,IAAI;CAC7B,MAAM,SAAS,QAAQ,UAAU;CAEjC,MAAM,kBAAkB,MAAM,IAAI;CAElC,MAAM,MAAM,SAAS,OAAO,GAAG,SAAS,aAAa;CACrD,MAAM,MAAM,CAAC,GAAG,SAAS,OAAO,GAAG,SAAS,aAAa,CAAC,CAAC,CAAC,QAAQ,SAAS,CAAC,IAAI,IAAI,IAAI,CAAC;CAC3F,MAAM,WAAW,SAAyB;EACxC,MAAM,MAAM,SAAS;EAErB,aAAa,OAAO,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO;GACzD,KAAK,KAAK,IAAI,IAAI,EAAE;GACpB,KAAK,KAAK,IAAI,IAAI,EAAE;GACpB,KAAK,KAAK,IAAI,IAAI,EAAE;EACtB,CAAC;EAED,OAAO;GAAE;GAAM;GAAK,WAAW;EAAK;CACtC;CACA,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,CAAC,QAAQ,EAAE,UAAU,IAAI,MAAM,IAAI,EAAE;CACnE,MAAM,QAAQ,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,QAAQ,EAAE,UAAU,IAAI,MAAM,IAAI,EAAE;CAExE,IAAI,MAAM,WAAW,KAAK,MAAM,WAAW,GACzC,OAAO;CAGT,MAAM,eAAe,SAAmB;EACtC,IAAI,CAAC,KAAK,WAAW;GACnB,MAAM,QAAkB,CAAC;GAEzB,aAAa,KAAK,OAAO,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO;IAC9D,MAAM,KACJ,KAAK,IAAI,IAAI,IAAI,EAAE,GACnB,KAAK,IAAI,IAAI,IAAI,EAAE,GACnB,KAAK,IAAI,IAAI,IAAI,EAAE,GACnB,KAAK,IAAI,IAAI,IAAI,EAAE,GACnB,KAAK,IAAI,IAAI,IAAI,EAAE,GACnB,KAAK,IAAI,IAAI,IAAI,EAAE,CACrB;GACF,CAAC;GACD,KAAK,YAAY,aAAa,KAAK,KAAK;EAC1C;EAEA,OAAO,KAAK;CACd;CAEA,MAAM,OAAO;EAAC;EAAG;EAAG;CAAC;CACrB,MAAM,QAAQ,MACX,SAAS,SACR,MAAM,KAAK,WAAW;EAAE;EAAM;EAAO,UAAU,MAAM,KAAK,KAAK,GAAG,MAAM,KAAK,GAAG,IAAI;CAAE,EAAE,CAC1F,CAAC,CACA,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;CACzC,IAAI,OAAO;CACX,IAAI,WAAW;EAAC;EAAG;EAAG;CAAC;CACvB,IAAI,UAAwB,CAAC,MAAM,EAAE,CAAE,KAAK,MAAM,MAAM,EAAE,CAAE,MAAM,IAAI;CAEtE,KAAK,MAAM,EAAE,MAAM,OAAO,cAAc,OAAO;EAC7C,IAAI,YAAY,MACd;EAGF,MAAM,gBAAgB,YAAY,IAAI;EACtC,MAAM,iBAAiB,YAAY,KAAK;EAExC,MAAM,QAAQ,WAAyB,UAAe;GACpD,MAAM,OAAiB,CAAC;GAExB,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK,GACzC,IAAI,MAAM,WAAW,GAAG,OAAO,GAAG,IAAI,IAAI,MACxC,KAAK,KAAK,CAAC;GAIf,OAAO;EACT;EACA,MAAM,WAAW,KAAK,eAAe,MAAM,GAAG;EAC9C,MAAM,YAAY,KAAK,gBAAgB,KAAK,GAAG;EAE/C,IAAI,SAAS,SAAS,UAAU,SAAS,YAAY;GACnD,IAAI,WAAW,MAAM;IACnB,OAAO;IACP,MAAM,KAAK,KAAK,GAAG,MAAM,KAAK,GAAG,IAAI;IACrC,WAAW,CAAC,GAAG,IAAI;IACnB,UAAU,CAAC,KAAK,MAAM,MAAM,IAAI;GAClC;GAEA;EACF;EAEA,KAAK,MAAM,KAAK,UACd,KAAK,MAAM,KAAK,WAAW;GACzB,MAAM,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,IAAI;GAE3D,IAAI,MAAM,MAAM;IACd,OAAO;IACP,WAAW,CAAC,GAAG,IAAI;IACnB,UAAU,CAAC,KAAK,MAAM,MAAM,IAAI;GAClC;EACF;CAEJ;CAEA,MAAM,YAAY,SAAS,SAAS,OAAO,SAAU,QAAQ,IAAI,CAAC,IAAI,IAAI,CAAC,CAAE;CAE7E,OAAO;EACL,KAAK,MAAM,IAAI;EACf,MAAM,UAAU,WAAW,IAAK;GAAC;GAAK;GAAK;EAAG,CAAC,CAAW,UAAU,MAAQ;EAC5E,SAAS,CAAC,UAAU,QAAQ,IAAI,OAAO,MAAM,GAAG,UAAU,QAAQ,IAAI,OAAO,MAAM,CAAC;CACtF;AACF;;ACzXA,MAAM,YAAY;AAElB,MAAM,QAAQ;CACZ;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,iBAAiB,eAAuB;CAC5C,MAAM,IAAI,MAAM,gBAAgB,KAAK,kDAAkD;AACzF;;;;;;;;;AAUA,MAAa,uBAAuB,MAAmB,UAA+B,CAAC,MAAM;CAC3F,MAAM,EAAE,UAAU,MAAM,YAAY,UAAU;CAC9C,MAAM,SAAS,QAAQ,UAAU;CAEjC,MAAM,UAAmD;EACvD,WAAW;EACX,UAAU,KAAc,iBAAiE,CAAC,MACxF,eAAe,MAAM;GACnB;GACA;GACA;GACA,GAAG;GACH,GAAI,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,IAAI;EACrC,CAAC;EACH,YAAY,eAAe,MAAM,EAAE,KAAK,CAAC;EACzC,cAAc,WAAW,MAAM,EAAE,KAAK,CAAC;EACvC,SAAS,gBAAyD,CAAC,MACjE,eAAe,MAAM;GAAE;GAAM,OAAO;GAAY,GAAG;EAAc,CAAC;EACpE,aAAa,gBAAgB,MAAM;GAAE;GAAM;GAAQ;EAAM,CAAC;EAC1D,SAAS,KAAc,SACrB,cAAc,MAAM;GAClB;GACA;GACA;GACA,GAAI,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,IAAI;GACnC,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK;EACvC,CAAC;EACH,SAAS,QACP,WAAW,MAAM;GAAE;GAAM;GAAQ;GAAO,GAAI,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,IAAI;EAAG,CAAC;EACjF,OAAO,WAAmB,cAAc,MAAM,QAAQ;GAAE;GAAQ;EAAM,CAAC;EACvE,YAAY,GAAW,MAAc,iBAAiB,MAAM,GAAG,GAAG;GAAE;GAAQ;EAAM,CAAC;EACnF,QAAQ,WACJ,OAAO,kBAA4C;GACjD,MAAM,SAAS,MAAM,iBAAiB,UAAU,aAAa;GAE7D,gBAAgB,MAAM;GAEtB,OAAO;EACT,IACA,cAAc,QAAQ;EAC1B,aAAa,YACR,kBAAsC;GACrC,MAAM,YAAY,gBAAgB,UAAU,aAAa;GAEzD,OAAO,EACL,WAAW;IACT,MAAM,SAAS,UAAU,IAAI;IAE7B,gBAAgB,MAAM;IAEtB,OAAO;GACT,EACF;EACF,IACA,cAAc,aAAa;EAC/B,aAAa,WACT,OAAO,UAAU,OAAO;GACtB,MAAM,UACJ,iBACA,eACA,iEACF;GAEA,MAAM,QAAQ,iBAAiB,QAAQ;GAEvC,OAAO,IAAI,SAA6D,YAAY;IAClF,iBAAiB;KACf,MAAM,KAAK;KACX,QAAQ;MAAE,QAAQ,MAAM;MAAQ,aAAa,MAAM;KAAY,CAAC;IAClE,GAAG,UAAU,GAAI;GACnB,CAAC;EACH,IACA,cAAc,aAAa;CACjC;CACA,MAAM,YAAY,EAAE,GAAG,QAAQ;CAC/B,MAAM,QAAQ;CAEd,IAAI,QAAQ,YAAY,OACtB,OAAO,OAAO,OAAO,OAAO;CAG9B,MAAM,aAAa;CAEnB,aAAa;EACX,KAAK,MAAM,QAAQ,OACjB,IAAI,MAAM,UAAU,QAAQ,OAC1B,OAAO,MAAM;EAIjB,IAAI,MAAM,eAAe,WACvB,OAAO,MAAM;CAEjB;AACF"}
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
4
4
  import { BufferAttribute, BufferGeometry, InterleavedBuffer, InterleavedBufferAttribute } from "three";
5
- //#region src/node/draco.ts
5
+ // src/node/draco.ts
6
6
  /**
7
7
  * Draco for `GLTFLoader` in Node. three's `DRACOLoader` decodes in Web Workers, which Node lacks;
8
8
  * this is its decode run in-process on `draco3dgltf` (Google's Node build of the decoder),
@@ -85,8 +85,7 @@ const createNodeDracoLoader = async () => {
85
85
  }
86
86
  };
87
87
  };
88
- //#endregion
89
- //#region src/node/load.ts
88
+ // src/node/load.ts
90
89
  /**
91
90
  * Load a glTF model in Node for the checks: `.glb`, or `.gltf` with its buffers beside it.
92
91
  *
@@ -210,7 +209,6 @@ const parseModel = async (bytes, directory = ".") => {
210
209
  };
211
210
  /** Read and parse a `.glb` or `.gltf` file into a three scene, ready for the checks. */
212
211
  const loadModel = async (path) => parseModel(await readFile(path), dirname(path));
213
- //#endregion
214
212
  export { parseModel as n, createNodeDracoLoader as r, loadModel as t };
215
213
 
216
- //# sourceMappingURL=load-nqfCU3-k.js.map
214
+ //# sourceMappingURL=load-D4bV0-2t.js.map