@zkmake/three-audit 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +78 -49
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -52,7 +52,10 @@ or a `setupFiles` module:
52
52
  ```ts
53
53
  import "@zkmake/three-audit/vitest";
54
54
 
55
+ import { expect, test } from "vitest";
56
+
55
57
  import budgets from "../src/budgets.ts";
58
+ import { buildKart } from "../src/kart.ts";
56
59
 
57
60
  test("the kart", () => {
58
61
  const kart = buildKart();
@@ -81,10 +84,10 @@ A failure says what to fix, as the CLI does:
81
84
  ```
82
85
 
83
86
  `toFitBudget` takes `{ skip, top }` (meshes listed, default 5). A frame budget's `calls` counts as
84
- `draws`. The matchers work negated (`.not`) and as asymmetric matchers
85
- (`expect.toFitBudget(…)`), and their types come with the import. `installMatchers()` registers
86
- them again, for a setup that resets `expect`; `matchers` is the object for `expect.extend`. The
87
- entry re-exports `triangles`. Vitest 5 or later.
87
+ `draws`; a budget that sets none of triangles, draws (calls) or meshes throws. The matchers work
88
+ negated (`.not`) and as asymmetric matchers (`expect.toFitBudget(…)`), and their types come with
89
+ the import. `installMatchers()` registers them again, for a setup that resets `expect`; `matchers`
90
+ is the object for `expect.extend`. The entry re-exports `triangles`. Vitest 5 or later.
88
91
 
89
92
  ## Budgets
90
93
 
@@ -115,12 +118,13 @@ export default defineBudgets({
115
118
  | `parts.*.draws` | Draw calls one main pass makes for it (`countDraws`) |
116
119
  | `parts.*.meshes` | Visible meshes in it |
117
120
 
118
- `@zkmake/three-audit/budgets` holds `defineBudgets` and `parseBudgets` alone, with no imports, so
119
- the app's own code can import the file (its HUD reads it) and ship a few hundred bytes, not the
120
- checks; the main entry exports both too. `defineBudgets` returns them frozen, with each part in the type: `budgets.parts.truck` is an error
121
+ `@zkmake/three-audit/budgets` holds `defineBudgets` and `parseBudgets` (and their types) alone,
122
+ with no imports, so the app's own code can import the file (its HUD reads it) and ship a few hundred
123
+ bytes, not the checks; the main entry exports both too. `frame` is required, `parts` optional.
124
+ `defineBudgets` returns them frozen, with each part in the type: `budgets.parts.truck` is an error
121
125
  when no truck is listed. `parseBudgets(value)` checks budgets from anywhere else (JSON) and throws
122
- on a bad key. `checkBudget(object, budget)` measures an object against one, a row per limit:
123
- `{ metric, actual, limit, over }`.
126
+ on a bad key. `checkBudget(object, budget, { skip }?)`, in the main entry, measures an object
127
+ against one, a row per limit: `{ metric, actual, limit, over }`.
124
128
 
125
129
  - In a test: `expect(kart).toFitBudget(budgets.parts.kart)`.
126
130
  - In the HUD: `mountPerfHud(monitor, { budgets })` (or `<PerfHud budgets={budgets} />`) applies the
@@ -147,9 +151,11 @@ windmill.glb 4,812 triangles, 14 draws
147
151
  …
148
152
  ```
149
153
 
150
- It exits 1 when any model fails a check (2 when one can't be read), so it can gate CI. Options:
151
- `--gap <m>`, `--self`, `--skip <regex>` (leave out objects by name, e.g. decals), `--top <n>`,
152
- `--json`, `--no-fail`.
154
+ It exits 1 when any model fails a check (2 when one can't be read), so it can gate CI. Empty meshes
155
+ are listed (`!`) but don't fail. Options: `--gap <m>`, `--self`, `--skip <regex>` (leave out
156
+ objects by name, e.g. decals), `--top <n>` (default 5, 0 for none), `--json` (an array of
157
+ `{ file, report, budget }`, `report` being `auditScene`'s), `--no-fail`,
158
+ `-h` / `--help`, `-v` / `--version`.
153
159
 
154
160
  `--budgets <file>` also holds each model to the app's [budgets](#budgets): a JSON file, or a module
155
161
  whose default export is them (`.js`, or `.ts` where Node strips types). A model answers to the part
@@ -174,12 +180,18 @@ In a test, load a model the same way:
174
180
  ```ts
175
181
  import { findZFighting } from "@zkmake/three-audit";
176
182
  import { loadModel } from "@zkmake/three-audit/node";
183
+ import { expect, test } from "vitest";
177
184
 
178
185
  test("windmill.glb has no z-fighting", async () => {
179
186
  expect(findZFighting(await loadModel("assets/windmill.glb"))).toEqual([]);
180
187
  });
181
188
  ```
182
189
 
190
+ `parseModel(bytes, directory?)` does the same from bytes already read (GLB or glTF JSON, its
191
+ buffers resolved from `directory`). `createNodeDracoLoader()` is the in-process Draco decoder it
192
+ uses, for your own `GLTFLoader` in Node: it has the two methods `GLTFLoader` calls on a
193
+ `DRACOLoader`, so pass it with a cast (`loader.setDRACOLoader(draco as never)`).
194
+
183
195
  glTF is in metres, which is what `--gap`'s default assumes. Instanced meshes
184
196
  (`EXT_mesh_gpu_instancing`) aren't checked for z-fighting; skinned and morphing meshes are
185
197
  checked in their rest pose.
@@ -200,23 +212,24 @@ const { scene, gl } = useThree();
200
212
  useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);
201
213
  ```
202
214
 
203
- | Global | Returns |
204
- | --------------------- | ----------------------------------------------------------------------------------- |
205
- | `summary(tag?)` | One count per check: the call to run after every edit |
206
- | `tris()` | Triangles one pass draws: visible meshes, instances and batches counted, no culling |
207
- | `meshes()` | Every visible mesh: triangles, instances, total, most first |
208
- | `census({ budget? })` | Geometries by triangles drawn, batches broken down, hidden meshes included |
209
- | `audit()` | Meshes with NaN positions or normals, or zero-length normals |
210
- | `zfight(gap?, self?)` | Pairs of meshes that z-fight |
211
- | `report(tag?)` | Every check in one JSON-safe object |
212
- | `bbox(target)` | An object's world envelope: min, max, size, centre |
213
- | `clearance(a, b)` | The gap between two objects, and which of their meshes come closest |
214
- | `ledger(options?)` | One frame's draw calls by object and pass, and each pass's size (WebGL) |
215
- | `beginLedger()` | `{ end() }`: draw calls between the two, for any span you choose (WebGL) |
216
- | `blackFrames(10)` | Frames in the next 10 s that came out black (WebGL) |
217
-
218
- A `tag` or `target` is a `userData.studioObject` value (or your `tagKey`), falling back to an
219
- object's name; `bbox` and `clearance` also take the object itself.
215
+ | Global | Returns |
216
+ | ----------------------------- | ------------------------------------------------------------------------------------------------ |
217
+ | `summary(tag?, options?)` | One count per check: the call to run after every edit. `{ zFighting: false }` skips the slow one |
218
+ | `tris()` | Triangles one pass draws: visible meshes, instances and batches counted, no culling |
219
+ | `meshes()` | Every visible mesh: triangles, instances, total, most first |
220
+ | `census({ budget?, label? })` | Geometries by triangles drawn, batches broken down, hidden meshes included |
221
+ | `audit()` | Meshes with NaN positions or normals, or zero-length normals |
222
+ | `zfight(gap?, self?)` | Pairs of meshes that z-fight |
223
+ | `report(tag?)` | Every check in one JSON-safe object |
224
+ | `bbox(target)` | An object's world envelope: min, max, size, centre |
225
+ | `clearance(a, b)` | The gap between two objects, and which of their meshes come closest |
226
+ | `ledger(options?)` | One frame's draw calls by object and pass, and each pass's size (WebGL) |
227
+ | `beginLedger()` | `{ end() }`: draw calls between the two, for any span you choose (WebGL) |
228
+ | `blackFrames(10)` | `{ frames, blackFrames }`: frames in the next 10 s that came out black (WebGL) |
229
+ | `auditRoot` | The root the helpers were installed on |
230
+
231
+ A `tag` is a `userData.studioObject` value (or your `tagKey`); a tag nothing carries throws. A
232
+ `target` (`bbox`, `clearance`) is a tag too, falling back to an object's name, or the object itself.
220
233
 
221
234
  ```js
222
235
  threeAudit.summary("bridge");
@@ -227,6 +240,11 @@ threeAudit.clearance("train", "tower");
227
240
 
228
241
  Each one is also on `threeAudit` (`threeAudit.report()`). Where short names like `report` collide
229
242
  with other tooling, `installAuditHelpers(scene, { globals: false })` installs only `threeAudit`.
243
+ The other options: `renderer` (the WebGL renderer `ledger`, `beginLedger` and `blackFrames` need;
244
+ without it they throw), `skip`, `tagKey`, [`bakes`](#baked-groups), and `batchLabel`, which names
245
+ the geometries inside a batch in `census()` (`geometryCensus`'s `label`).
246
+ [three-devtools](../three-devtools) installs them behind `?debug` (its `audit` option), and
247
+ [three-studio](../three-studio)'s pages install them on their own scene.
230
248
 
231
249
  From a headless browser: `agent-browser eval 'JSON.stringify(threeAudit.report())'`.
232
250
 
@@ -270,7 +288,9 @@ between each other instead of over each other's ends.
270
288
  ### `findBadGeometry(root, options?)`
271
289
 
272
290
  Vertices with a non-finite position or normal, and zero-length normals on triangles with area.
273
- Either lights a pixel NaN, and bloom smears one NaN pixel into a black block.
291
+ Either lights a pixel NaN, and bloom smears one NaN pixel into a black block. Hidden meshes are
292
+ checked too. A row is `{ mesh, under, nonFinite, zeroNormals }`, `under` being the tagged part it
293
+ sits in (or its parent's name). Options: `skip`, `tagKey`, `bakes`.
274
294
 
275
295
  ### `summarizeScene(root, options?)`
276
296
 
@@ -280,10 +300,12 @@ emptyMeshes }`. `tag` narrows it to one tagged object. `zFighting: false` skips
280
300
 
281
301
  ### `countTriangles`, `listMeshes`, `geometryCensus`, `countDraws`, `triangles`
282
302
 
283
- Where a triangle budget goes. `triangles(target)` is the one a test reaches for: a geometry's own
284
- triangles (`triangleCount`, within its draw range), or what an object draws (`countTriangles`). Name meshes and geometries (`mesh.name`, `geometry.name`) so the rows
285
- say something. A mesh row's `name` is its named ancestors, then its own name, or its geometry's name
286
- or type when it has none (`CharacterHands/left/BoxGeometry`).
303
+ Where a triangle budget goes. `countTriangles` counts what one pass draws, with no culling; a shadow
304
+ pass adds roughly as much again per shadow-casting light. `triangles(target)` is the one a test
305
+ reaches for: a geometry's own triangles (`triangleCount`, within its draw range), or what an object
306
+ draws (`countTriangles`). Name meshes and geometries (`mesh.name`, `geometry.name`) so the rows say
307
+ something. A mesh row's `name` is its named ancestors, then its own name, or its geometry's name or
308
+ type when it has none (`CharacterHands/left/BoxGeometry`).
287
309
 
288
310
  `geometryCensus` lists a `BatchedMesh` one geometry at a time: visible instances × triangles each,
289
311
  which is where a batch's budget actually goes. Rows are `batch#id` unless `label(batch, geometryId)`
@@ -296,29 +318,32 @@ treadmill_belt 570 513 292,410 0.306 true
296
318
  treadmill_coupler 570 297 169,290 0.177 false
297
319
  ```
298
320
 
299
- `countDraws` counts the draw calls one main pass makes: one per visible mesh, line or point cloud,
300
- one per material group when a mesh has several. It doesn't cull and doesn't count shadows; the draw
301
- ledger counts the real thing.
321
+ `countDraws` counts the draw calls one main pass makes: one per visible mesh, line, point cloud or
322
+ sprite, one per material group when a mesh has several. It doesn't cull and doesn't count shadows;
323
+ the draw ledger counts the real thing.
302
324
 
303
- ### `findEmptyMeshes(root)`
325
+ ### `findEmptyMeshes(root, options?)`
304
326
 
305
327
  Meshes with no vertices, such as what merging nothing leaves. Each one still costs a draw call and
306
328
  a geometry to dispose. A zero draw range doesn't count, since that's usually a pool waiting to fill.
329
+ Hidden meshes are included; it takes `findBadGeometry`'s options.
307
330
 
308
- ### `measureBounds(root, target)` / `measureClearance(root, a, b)`
331
+ ### `measureBounds(root, target, options?)` / `measureClearance(root, a, b, options?)`
309
332
 
310
333
  An object's world envelope, and the smallest gap between two objects, from their vertices as drawn:
311
334
  instances and batches included, rotations exact. The gap is measured between triangles' bounding
312
335
  boxes, which is exact for faces square to the axes and slightly under for sloped ones. So a tower
313
336
  merged into one mesh still measures to its crossbar, not to the box around the whole tower. `axis`
314
- says which way the gap runs when it's straight along one axis. Draw ranges are respected; a shadow pass adds roughly as much again per light.
337
+ says which way the gap runs when it's straight along one axis (`null` when diagonal). Draw ranges
338
+ are respected. A target matching nothing throws; one that draws nothing gives `null`. Options:
339
+ `tagKey`, `bakes`.
315
340
 
316
341
  ### `recordDrawLedger(renderer)` / `beginDrawLedger(renderer)`
317
342
 
318
343
  Wraps `WebGLRenderer.renderBufferDirect` and counts every draw by object name, by group (the name up
319
- to its first `:` or `#`), and by pass: `shadow`, then each `render()` call by scene name, or
320
- `render 1`, `render 2`… An unnamed object goes by its named ancestors, type and material
321
- (`CharacterHands/Mesh [skin]`).
344
+ to its first `:` or `#`, or your `groupOf(name)`), and by pass: `shadow`, then each `render()` call
345
+ by scene name, or `render 1`, `render 2`… An unnamed object goes by its named ancestors, type and
346
+ material (`CharacterHands/Mesh [skin]`).
322
347
 
323
348
  Each row (object or group) carries its draws per pass (`passes`) and its triangles, in all
324
349
  (`triangles`) and per pass (`passTriangles`): what a frame really draws of each part, so the city
@@ -327,7 +352,8 @@ pass, after culling them one by one.
327
352
 
328
353
  `recordDrawLedger` records exactly one animation frame, and fails after `timeout` (2 s) when no
329
354
  frame comes. `{ render }` records one call of your own render instead, frames or not.
330
- `beginDrawLedger` returns `{ end() }` for any span. `printDrawLedger` prints the tables.
355
+ `beginDrawLedger` returns `{ end() }` for any span. `printDrawLedger` prints the tables;
356
+ `ledgerTable(rows, passes)` is one of them as rows for `console.table`.
331
357
 
332
358
  `passStats` sizes each pass: draws, distinct objects (for `shadow`, the casters), triangles, and
333
359
  the targets drawn to in device pixels (`screen 2880×1800` for the canvas). A pass is `fullscreen`
@@ -338,14 +364,17 @@ pass. `fullscreenPasses` and `fullscreenPixels` total them: 8 fullscreen passes
338
364
  ### `watchBlackFrames(renderer)`
339
365
 
340
366
  After each render to the screen, reads a 3 × 3 grid of pixels and counts frames where three or more
341
- are pure black. Use a visible browser: headless ones haven't reproduced black frames. Each read
342
- stalls the GPU, so watch, read `blackFrames`, `stop()`.
367
+ are pure black (options `grid` and `threshold` set the two numbers). Use a visible browser:
368
+ headless ones haven't reproduced black frames. Each read stalls the GPU, so watch, read `frames`
369
+ and `blackFrames` (which frames, 1-based), `stop()`; `reset()` starts the count over.
343
370
 
344
371
  ### `auditScene(root, options?)`
345
372
 
346
- `{ triangles, zFighting, badGeometry, meshes }` in one call. Rows carry the live mesh as a
347
- non-enumerable property (`row.mesh`, `row.meshes`), so they print and serialise without dragging the
348
- scene along.
373
+ `{ triangles, draws, zFighting, badGeometry, emptyMeshes, meshes }` in one call, `meshes` the
374
+ costliest `top` (default 10). `tag` narrows it to one tagged object. Rows carry the live mesh as a
375
+ non-enumerable property (`row.mesh` on a mesh row, `row.meshes` and `row.pairs` on a z-fighting
376
+ row, `row.object` on a bad or empty one), so they print and serialise without dragging the scene
377
+ along.
349
378
 
350
379
  ## Baked groups
351
380
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkmake/three-audit",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Scene checks for three.js: triangle counts, cost census, NaN normals, z-fighting, black frames, a one-frame draw-call ledger, and per-app budgets with Vitest matchers. Zero dependencies.",
5
5
  "keywords": [
6
6
  "audit",