@zkmake/three-audit 0.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"budgets-DQggDhEu.js","names":[],"sources":["../src/scene.ts","../src/bad-geometry.ts","../src/triangles.ts","../src/z-fighting.ts","../src/budgets.ts"],"sourcesContent":["/**\n * Scene-graph plumbing every check shares: the walk, mesh kinds, triangle counts, tags and names.\n *\n * Nothing here imports three at runtime; objects are recognised by three's own `is*` flags, so the\n * checks work on whatever copy of three the app bundles.\n */\nimport type { BatchedMesh, BufferGeometry, InstancedMesh, Material, Mesh, Object3D } from \"three\";\n\n/**\n * Any three.js object, from whichever copy of three's types the app has. The checks take this, so\n * an app on a newer `@types/three` than this package was built against passes its scene as is.\n */\nexport type AnyObject3D = { readonly isObject3D: true; readonly children: readonly object[] };\n\n/** The object as three's own type, for the walk. */\nexport const asObject3D = (object: AnyObject3D) => object as unknown as Object3D;\n\n/** Leave an object and everything under it out of a check. */\nexport type Skip = (object: Object3D) => boolean;\n\n/**\n * What a check sees of a bake (`@zkmake/three-batch`'s `bake` and `<Baked>`), which hides its\n * parts and draws one merged mesh per material instead. `sources`: the parts, as if shown, and not\n * the merge, so rows name real parts. `merged`: what the renderer draws.\n */\nexport type Bakes = \"sources\" | \"merged\";\n\nexport type WalkOptions = {\n skip?: Skip | undefined;\n /** Prune hidden objects and their subtrees, as the renderer does. */\n visibleOnly?: boolean;\n /** With `sources`: prune bake results, and walk the parts a bake hid as though visible. */\n bakes?: Bakes | undefined;\n};\n\n/**\n * A part a bake hid in favour of its merge: flagged `userData.bakeSource` by three-batch, or (for\n * bakes from before that flag) inside an object that holds a `userData.bakedResult` merge.\n */\nexport const isBakeSource = (object: Object3D) => {\n if (object.userData.bakeSource === true) {\n return true;\n }\n\n for (let at = object.parent; at; at = at.parent) {\n if (at.children.some((child) => child.userData.bakedResult === true)) {\n return true;\n }\n }\n\n return false;\n};\n\n/** Depth-first, in child order. A skipped (or, with `visibleOnly`, hidden) object prunes its subtree. */\nexport const walk = (root: Object3D, options: WalkOptions, visit: (object: Object3D) => void) => {\n const stack = [root];\n const sources = options.bakes === \"sources\";\n\n while (stack.length > 0) {\n const object = stack.pop()!;\n\n if (\n (sources && object.userData.bakedResult === true) ||\n (options.visibleOnly === true && !object.visible && !(sources && isBakeSource(object))) ||\n options.skip?.(object) === true\n ) {\n continue;\n }\n\n visit(object);\n\n for (let i = object.children.length - 1; i >= 0; i -= 1) {\n stack.push(object.children[i]!);\n }\n }\n};\n\nexport const isMesh = (object: Object3D): object is Mesh => (object as Mesh).isMesh === true;\n\n/** Anything the renderer draws: a mesh, line, point cloud or sprite. */\nexport const isDrawable = (object: Object3D) => {\n const flags = object as Partial<Record<\"isMesh\" | \"isLine\" | \"isPoints\" | \"isSprite\", boolean>>;\n\n return (\n flags.isMesh === true ||\n flags.isLine === true ||\n flags.isPoints === true ||\n flags.isSprite === true\n );\n};\n\nexport const isInstanced = (mesh: Mesh): mesh is InstancedMesh =>\n (mesh as InstancedMesh).isInstancedMesh === true;\n\nexport const isBatched = (mesh: Mesh): mesh is BatchedMesh =>\n (mesh as unknown as BatchedMesh).isBatchedMesh === true;\n\n/** Triangles one draw of this geometry makes: indexed or not, within its draw range. */\nexport const triangleCount = (geometry: BufferGeometry) => {\n const elements = geometry.index?.count ?? geometry.attributes.position?.count ?? 0;\n const { start, count } = geometry.drawRange;\n const drawn = Math.min(elements, start + count) - start;\n\n return Math.max(0, Math.floor(drawn / 3));\n};\n\n/**\n * A batch's visible instances by the geometry each draws: how many, and that geometry's triangles\n * (its range in the shared buffer). In first-drawn order.\n */\nexport const batchedGeometries = (batch: BatchedMesh) => {\n const geometries = new Map<number, { instances: number; triangles: number }>();\n\n for (let id = 0; id < batch.maxInstanceCount; id += 1) {\n let geometryId: number;\n\n // Deleted and never-used ids throw.\n try {\n if (!batch.getVisibleAt(id)) {\n continue;\n }\n\n geometryId = batch.getGeometryIdAt(id);\n } catch {\n continue;\n }\n\n const known = geometries.get(geometryId);\n\n if (known) {\n known.instances += 1;\n continue;\n }\n\n const range = batch.getGeometryRangeAt(geometryId) as {\n count?: number;\n indexCount: number;\n vertexCount: number;\n };\n const elements = range.count ?? (batch.geometry.index ? range.indexCount : range.vertexCount);\n\n geometries.set(geometryId, { instances: 1, triangles: Math.floor(elements / 3) });\n }\n\n return geometries;\n};\n\n/** Triangles a batch draws across its visible instances, and how many instances that is. */\nexport const batchedTriangles = (batch: BatchedMesh) => {\n let triangles = 0;\n let instances = 0;\n\n for (const geometry of batchedGeometries(batch).values()) {\n triangles += geometry.triangles * geometry.instances;\n instances += geometry.instances;\n }\n\n return { instances, triangles };\n};\n\n/** The tag of the nearest tagged ancestor (the object itself included), or `null`. */\nexport const tagOf = (object: Object3D, tagKey: string) => {\n for (let at: Object3D | null = object; at; at = at.parent) {\n const tag: unknown = at.userData[tagKey];\n\n if (tag !== undefined && tag !== null) {\n return String(tag);\n }\n }\n\n return null;\n};\n\n/**\n * A skip that keeps only what's tagged `tag`: prunes objects tagged otherwise, and meshes outside\n * any object tagged `tag`. Throws when nothing under `root` carries the tag.\n */\nexport const onlyTag = (root: Object3D, tag: string, tagKey: string, skip?: Skip): Skip => {\n let found = false;\n\n walk(root, { skip }, (object) => {\n const own: unknown = object.userData[tagKey];\n\n found ||= own !== undefined && own !== null && String(own) === tag;\n });\n\n if (!found) {\n throw new Error(`three-audit: nothing is tagged userData.${tagKey} = \"${tag}\"`);\n }\n\n return (object) => {\n if (skip?.(object) === true) {\n return true;\n }\n\n const own: unknown = object.userData[tagKey];\n\n if (own !== undefined && own !== null && String(own) !== tag) {\n return true;\n }\n\n return isDrawable(object) && tagOf(object, tagKey) !== tag;\n };\n};\n\nexport const hasTag = (root: Object3D, tagKey: string, skip?: Skip) => {\n let found = false;\n\n walk(root, { skip }, (object) => {\n found ||= object.userData[tagKey] !== undefined && object.userData[tagKey] !== null;\n });\n\n return found;\n};\n\nexport const meshName = (mesh: Mesh) => mesh.name || mesh.geometry.name || mesh.geometry.type;\n\n/**\n * The named ancestors above an object, outermost first, from `root` (included when named) down:\n * at most the nearest `depth`, with `…` for the rest. An unnamed ancestor carrying `tagKey` goes\n * by its tag.\n */\nexport const pathOf = (object: Object3D, root: Object3D | null, tagKey?: string, depth = 3) => {\n const names: string[] = [];\n\n for (let at = object.parent; at; at = at === root ? null : at.parent) {\n const tag: unknown = tagKey === undefined ? undefined : at.userData[tagKey];\n const name = at.name || (tag === undefined || tag === null ? \"\" : String(tag));\n\n if (name) {\n names.unshift(name);\n }\n }\n\n return names.length > depth ? [\"…\", ...names.slice(-depth)] : names;\n};\n\n/** `path/to/mesh [material]`: enough to find a mesh in the scene without walking it by hand. */\nexport const meshLabel = (mesh: Mesh, root: Object3D | null, tagKey?: string) =>\n `${[...pathOf(mesh, root, tagKey), meshName(mesh)].join(\"/\")} [${materialName(mesh)}]`;\n\nexport const materialName = (mesh: Mesh) => {\n const material = (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material) as\n | Material\n | undefined;\n\n return material ? material.name || material.type : \"none\";\n};\n\n/** Adds a property JSON and `console.table` leave out: a live object that would serialise the scene. */\nexport const withHidden = <Row extends object, Key extends string, Value>(\n row: Row,\n key: Key,\n value: Value,\n) => Object.defineProperty(row, key, { value, enumerable: false }) as Row & Record<Key, Value>;\n","/**\n * Geometry that lights a pixel NaN: a non-finite position or normal, or a zero-length normal on a\n * triangle with area. Bloom and other blurs spread one NaN pixel into a black block.\n */\nimport type { BufferGeometry, Mesh } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n type Bakes,\n isMesh,\n meshLabel,\n meshName,\n type Skip,\n tagOf,\n walk,\n withHidden,\n} from \"./scene.ts\";\n\nexport type BadGeometryOptions = {\n skip?: Skip | undefined;\n /** `userData` key naming the part a mesh belongs to, for the `under` column. */\n tagKey?: string;\n /** A bake's parts (default), or its merged meshes: see `Bakes`. Not both, or each counts twice. */\n bakes?: Bakes | undefined;\n};\n\nexport type BadGeometryRow = {\n mesh: string;\n /** The tagged part it sits in, or its parent's name. */\n under: string;\n /** Vertices with a NaN or infinite position or normal component. */\n nonFinite: number;\n /** Vertices of triangles with area whose normal has no length. */\n zeroNormals: number;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly object: Mesh;\n};\n\nconst corner = (geometry: BufferGeometry, k: number) => geometry.index?.getX(k) ?? k;\n\nconst check = (geometry: BufferGeometry) => {\n const position = geometry.attributes.position!;\n const normal = geometry.attributes.normal;\n const flagged = new Uint8Array(position.count);\n let nonFinite = 0;\n let zeroNormals = 0;\n\n for (let v = 0; v < position.count; v += 1) {\n const finite =\n Number.isFinite(position.getX(v)) &&\n Number.isFinite(position.getY(v)) &&\n Number.isFinite(position.getZ(v)) &&\n (!normal ||\n (Number.isFinite(normal.getX(v)) &&\n Number.isFinite(normal.getY(v)) &&\n Number.isFinite(normal.getZ(v))));\n\n if (!finite) {\n flagged[v] = 1;\n nonFinite += 1;\n }\n }\n\n if (!normal) {\n return { nonFinite, zeroNormals };\n }\n\n const corners = geometry.index?.count ?? position.count;\n\n for (let k = 0; k + 2 < corners; k += 3) {\n const a = corner(geometry, k);\n const b = corner(geometry, k + 1);\n const c = corner(geometry, k + 2);\n const ux = position.getX(b) - position.getX(a);\n const uy = position.getY(b) - position.getY(a);\n const uz = position.getZ(b) - position.getZ(a);\n const vx = position.getX(c) - position.getX(a);\n const vy = position.getY(c) - position.getY(a);\n const vz = position.getZ(c) - position.getZ(a);\n const area = Math.hypot(uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx);\n\n if (!(area > 1e-8)) {\n continue;\n }\n\n for (const v of [a, b, c]) {\n if (flagged[v] === 0 && Math.hypot(normal.getX(v), normal.getY(v), normal.getZ(v)) < 1e-4) {\n flagged[v] = 1;\n zeroNormals += 1;\n }\n }\n }\n\n return { nonFinite, zeroNormals };\n};\n\n/** Meshes with geometry that renders NaN, hidden ones included. `[]` is a pass. */\nexport const findBadGeometry = (\n root: AnyObject3D,\n options: BadGeometryOptions = {},\n): BadGeometryRow[] => {\n const tagKey = options.tagKey ?? \"studioObject\";\n const rows: BadGeometryRow[] = [];\n\n walk(asObject3D(root), { skip: options.skip, bakes: options.bakes ?? \"sources\" }, (object) => {\n if (!isMesh(object) || !object.geometry.attributes.position) {\n return;\n }\n\n const { nonFinite, zeroNormals } = check(object.geometry);\n\n if (nonFinite > 0 || zeroNormals > 0) {\n const under = tagOf(object, tagKey) ?? object.parent?.name ?? \"\";\n\n rows.push(\n withHidden({ mesh: meshName(object), under, nonFinite, zeroNormals }, \"object\", object),\n );\n }\n });\n\n return rows;\n};\n\nexport type EmptyMeshRow = {\n /** `path/to/mesh [material]`. */\n mesh: string;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly object: Mesh;\n};\n\n/**\n * Meshes with no vertices, hidden ones included: what a merge of nothing leaves behind. They draw\n * nothing, but each one is still a draw call and a leak to dispose. A draw range of zero doesn't\n * count: that's usually a pool waiting to fill.\n */\nexport const findEmptyMeshes = (\n root: AnyObject3D,\n options: BadGeometryOptions = {},\n): EmptyMeshRow[] => {\n const scene = asObject3D(root);\n const rows: EmptyMeshRow[] = [];\n\n walk(scene, { skip: options.skip, bakes: options.bakes ?? \"sources\" }, (object) => {\n if (isMesh(object) && (object.geometry.attributes.position?.count ?? 0) === 0) {\n rows.push(\n withHidden(\n { mesh: meshLabel(object, scene, options.tagKey ?? \"studioObject\") },\n \"object\",\n object,\n ),\n );\n }\n });\n\n return rows;\n};\n","/**\n * Where a triangle budget goes: the total one pass draws, the meshes drawing it, and the\n * geometries behind them.\n */\nimport type { BatchedMesh, BufferGeometry, Mesh } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n batchedGeometries,\n batchedTriangles,\n isBatched,\n isDrawable,\n isInstanced,\n isMesh,\n materialName,\n meshName,\n pathOf,\n type Skip,\n triangleCount,\n walk,\n withHidden,\n} from \"./scene.ts\";\n\nexport type TriangleOptions = { skip?: Skip | undefined };\n\nexport type MeshRow = {\n /** `path/to/mesh`: its named ancestors, then its name (or its geometry's name or type). */\n name: string;\n material: string;\n /** Triangles in one copy. A batch's is an average: `geometryCensus` breaks it down. */\n triangles: number;\n instances: number;\n total: number;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly mesh: Mesh;\n};\n\nexport type GeometryRow = {\n /**\n * `geometry.name`, or its type and the start of its uuid. For a geometry in a batch, the batch's\n * `label` for it, or `batch#id`.\n */\n geometry: string;\n /** Copies drawn: every mesh using it, instances counted. */\n uses: number;\n triangles: number;\n total: number;\n /** Its part of the census's total, 0–1. */\n share: number;\n /** With `budget`: whether `share` is over it. */\n overBudget?: boolean;\n};\n\nexport type CensusOptions = TriangleOptions & {\n /**\n * Names a geometry inside a batch, e.g. from the app's own table of what it added. Default\n * `batch#id`, the batch named by its path.\n */\n label?: ((batch: BatchedMesh, geometryId: number) => string | undefined) | undefined;\n /** Flag rows whose share of the total is over this, 0–1: `0.2` for anything over 20%. */\n budget?: number | undefined;\n};\n\nconst cost = (mesh: Mesh) => {\n if (isBatched(mesh)) {\n return batchedTriangles(mesh);\n }\n\n const instances = isInstanced(mesh) ? mesh.count : 1;\n\n return { instances, triangles: triangleCount(mesh.geometry) * instances };\n};\n\n/**\n * Triangles one pass draws: visible meshes, instances counted, no frustum culling. A shadow pass\n * adds roughly as much again for each shadow-casting light.\n */\nexport const countTriangles = (root: AnyObject3D, options: TriangleOptions = {}) => {\n let total = 0;\n\n walk(asObject3D(root), { skip: options.skip, visibleOnly: true }, (object) => {\n if (isMesh(object)) {\n total += cost(object).triangles;\n }\n });\n\n return total;\n};\n\n/**\n * The triangles in a geometry (one draw, within its draw range: `triangleCount`) or that an object\n * draws (visible meshes, instances counted: `countTriangles`). The one helper a test needs.\n */\nexport const triangles = (\n target: AnyObject3D | { readonly isBufferGeometry: true },\n options: TriangleOptions = {},\n) =>\n \"isBufferGeometry\" in target\n ? triangleCount(target as unknown as BufferGeometry)\n : countTriangles(target, options);\n\n/** Every visible mesh, most triangles drawn first. */\nexport const listMeshes = (scene: AnyObject3D, options: TriangleOptions = {}): MeshRow[] => {\n const root = asObject3D(scene);\n const rows: MeshRow[] = [];\n\n walk(root, { skip: options.skip, visibleOnly: true }, (object) => {\n if (!isMesh(object)) {\n return;\n }\n\n const { instances, triangles } = cost(object);\n const row = {\n name: [...pathOf(object, root), meshName(object)].join(\"/\"),\n material: materialName(object),\n triangles: instances > 0 ? Math.round(triangles / instances) : 0,\n instances,\n total: triangles,\n };\n\n rows.push(withHidden(row, \"mesh\", object));\n });\n\n return rows.sort((a, b) => b.total - a.total);\n};\n\n/**\n * Geometries by triangles drawn, hidden meshes included, most first. Name geometries\n * (`geometry.name`) to make this readable. A batch counts each geometry in it separately, by its\n * visible instances: the row a budget is really spent on.\n */\nexport const geometryCensus = (root: AnyObject3D, options: CensusOptions = {}): GeometryRow[] => {\n const scene = asObject3D(root);\n const rows = new Map<string, GeometryRow>();\n const add = (key: string, triangles: number, uses: number) => {\n const row = rows.get(key) ?? { geometry: key, uses: 0, triangles, total: 0, share: 0 };\n\n row.uses += uses;\n row.total += triangles * uses;\n rows.set(key, row);\n };\n\n walk(scene, { skip: options.skip }, (object) => {\n if (!isMesh(object) || !object.geometry.attributes.position) {\n return;\n }\n\n if (isBatched(object)) {\n const name = [...pathOf(object, scene), meshName(object)].join(\"/\");\n\n for (const [id, { instances, triangles }] of batchedGeometries(object)) {\n add(options.label?.(object, id) ?? `${name}#${id}`, triangles, instances);\n }\n\n return;\n }\n\n const geometry = object.geometry;\n\n add(\n geometry.name || `${geometry.type}:${geometry.uuid.slice(0, 6)}`,\n triangleCount(geometry),\n isInstanced(object) ? object.count : 1,\n );\n });\n\n const all = [...rows.values()];\n const sum = all.reduce((total, row) => total + row.total, 0);\n\n for (const row of all) {\n row.share = sum > 0 ? Math.round((row.total / sum) * 1000) / 1000 : 0;\n\n if (options.budget !== undefined) {\n row.overBudget = row.total / Math.max(sum, 1) > options.budget;\n }\n }\n\n return all.sort((a, b) => b.total - a.total);\n};\n\n/**\n * Draw calls one main pass makes: one per visible mesh, line or point cloud, or one per material\n * group for a mesh with several materials. Before frustum culling; each shadow-casting light adds\n * one per caster. `recordDrawLedger` counts the real thing.\n */\nexport const countDraws = (root: AnyObject3D, options: TriangleOptions = {}) => {\n let draws = 0;\n\n walk(asObject3D(root), { skip: options.skip, visibleOnly: true }, (object) => {\n if (!isDrawable(object)) {\n return;\n }\n\n const drawable = object as Partial<Mesh>;\n\n const material = drawable.material;\n\n if (Array.isArray(material)) {\n const groups = drawable.geometry?.groups ?? [];\n\n draws += groups.filter(\n (group) => material[group.materialIndex ?? 0]?.visible !== false,\n ).length;\n } else if (material?.visible !== false) {\n draws += 1;\n }\n });\n\n return draws;\n};\n","/**\n * Z-fighting found by geometry instead of by eye: pairs of meshes with triangles in (nearly) one\n * plane, facing the same way, overlapping. Those flicker wherever depth precision runs out.\n *\n * Triangles are bucketed by their quantised plane (normal and offset), so only triangles in\n * neighbouring buckets are compared. A pair counts only when each triangle's corners lie within\n * `gap` of the other's plane, measured at the triangles: the offsets are measured from the world\n * origin, and far from it two faces a degree or two apart can share one while metres apart where\n * they stand. Overlap in the shared plane is a separating-axis test, then clipped for its area.\n *\n * A bake's hidden parts are checked in place of its merge (`bakes`). Left out, as they can't\n * fight: other hidden objects, instanced and batched meshes (and geometry a\n * shader positions, `InstancedBufferGeometry`), pairs where neither side writes depth or one\n * doesn't test it, and pairs where either material has a polygon offset (the usual decal fix).\n */\nimport type { Material, Mesh } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n type Bakes,\n hasTag,\n isBatched,\n isInstanced,\n isMesh,\n meshLabel,\n type Skip,\n tagOf,\n walk,\n withHidden,\n} from \"./scene.ts\";\n\nexport type ZFightingOptions = {\n /**\n * How close two planes count as one, in world units. 0.004 (4 mm at 1 unit = 1 m) is about what\n * depth precision loses with a camera ~50 m out. Scale it with your units.\n */\n gap?: number;\n /** Also compare triangles within one mesh: for merged geometry whose parts share a buffer. */\n self?: boolean;\n /**\n * Only meshes inside a tagged object (`userData[tagKey]`), so a studio's ground and props stay\n * out. Defaults to true when anything under the root is tagged, false otherwise.\n */\n tagged?: boolean;\n tagKey?: string;\n skip?: Skip | undefined;\n /**\n * A bake's parts (default), or its merged meshes: see `Bakes`. The parts name what to fix; the\n * merge is the same faces under one name.\n */\n bakes?: Bakes | undefined;\n};\n\nexport type ZFightingRow = {\n /** `path/to/mesh [material] @ (x, y, z)`: named ancestors, and the centre in world space. */\n a: string;\n b: string;\n /** Overlapping triangle pairs between the two. */\n triangles: number;\n /**\n * The planes they share, most overlap first: `z = -3.000, facing +z` when axis-aligned, else\n * `(nx, ny, nz) · p = d`. Both faces point along the normal: opposite-facing faces never pair.\n */\n planes: string[];\n /** Where they overlap: the area in world units², and its centre in world space. */\n overlap: { area: number; centre: [number, number, number] };\n /** Identical pairs (same labels, triangles and planes) folded into this row. */\n count: number;\n /** Not enumerable: kept out of JSON and `console.table`. The first pair folded in. */\n readonly meshes: readonly [Mesh, Mesh];\n /** Not enumerable: every pair folded in. */\n readonly pairs: readonly (readonly [Mesh, Mesh])[];\n};\n\n/** Normals quantised to 1/40: neighbouring buckets are searched, so this only sets bucket size. */\nconst QUANT = 40;\n/** Normals this aligned count as parallel (about 2.6°). */\nconst PARALLEL = 0.999;\n\ntype Part = {\n mesh: Mesh;\n label: string;\n min: number[];\n max: number[];\n /** Some material writes depth / tests it / offsets it. */\n writes: boolean;\n tests: boolean;\n offset: boolean;\n};\n\nconst round = (value: number, places = 2) => {\n const scale = 10 ** places;\n const rounded = Math.round(value * scale) / scale;\n\n return rounded === 0 ? 0 : rounded;\n};\n\nconst labelOf = (part: Part) => {\n const centre = part.min.map((low, axis) => round((low + part.max[axis]!) / 2));\n\n return `${part.label} @ (${centre.join(\", \")})`;\n};\n\nconst fixed = (value: number) => round(value, 3).toFixed(3);\n\n/** `z = -3.000, facing +z` for an axis-aligned plane, `(nx, ny, nz) · p = d` for any other. */\nconst planeLabel = (nx: number, ny: number, nz: number, d: number) => {\n for (const [axis, value] of [\n [\"x\", nx],\n [\"y\", ny],\n [\"z\", nz],\n ] as const) {\n if (Math.abs(value) > 0.9999) {\n return `${axis} = ${fixed(d / value)}, facing ${value > 0 ? \"+\" : \"-\"}${axis}`;\n }\n }\n\n return `(${fixed(nx)}, ${fixed(ny)}, ${fixed(nz)}) · p = ${fixed(d)}`;\n};\n\nconst materialsOf = (mesh: Mesh) =>\n (Array.isArray(mesh.material) ? mesh.material : [mesh.material]).filter(\n (material): material is Material => material !== undefined && material !== null,\n );\n\n/** Whether two parts can fight: one writes depth the other tests, and neither is offset. */\nconst canFight = (a: Part, b: Part) =>\n !a.offset && !b.offset && ((a.writes && b.tests) || (b.writes && a.tests));\n\n/** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */\nexport const findZFighting = (\n scene: AnyObject3D,\n options: ZFightingOptions = {},\n): ZFightingRow[] => {\n const root = asObject3D(scene);\n const gap = options.gap ?? 0.004;\n const self = options.self ?? false;\n const tagKey = options.tagKey ?? \"studioObject\";\n const tagged = options.tagged ?? hasTag(root, tagKey, options.skip);\n\n root.updateWorldMatrix(true, true);\n\n const parts: Part[] = [];\n // Per triangle: its part, plane (nx, ny, nz, d) and world corners (9 numbers).\n const owner: number[] = [];\n const planes: number[] = [];\n const corners: number[] = [];\n const keys: [number, number, number, number][] = [];\n const buckets = new Map<string, number[]>();\n const keyOf = (x: number, y: number, z: number, d: number) => `${x},${y},${z},${d}`;\n\n walk(\n root,\n { skip: options.skip, visibleOnly: true, bakes: options.bakes ?? \"sources\" },\n (object) => {\n if (\n !isMesh(object) ||\n isInstanced(object) ||\n isBatched(object) ||\n (object.geometry as { isInstancedBufferGeometry?: boolean }).isInstancedBufferGeometry ===\n true ||\n !object.geometry.attributes.position\n ) {\n return;\n }\n\n if (tagged && tagOf(object, tagKey) === null) {\n return;\n }\n\n const materials = materialsOf(object).filter((material) => material.visible !== false);\n\n if (materials.length === 0) {\n return;\n }\n\n const part = parts.length;\n const { index, attributes } = object.geometry;\n const position = attributes.position!;\n const e = object.matrixWorld.elements;\n const min = [Infinity, Infinity, Infinity];\n const max = [-Infinity, -Infinity, -Infinity];\n const count = index?.count ?? position.count;\n const world = [0, 0, 0, 0, 0, 0, 0, 0, 0];\n\n parts.push({\n mesh: object,\n label: meshLabel(object, root, tagKey),\n min,\n max,\n writes: materials.some((material) => material.depthWrite !== false),\n tests: materials.some((material) => material.depthTest !== false),\n offset: materials.some(\n (material) =>\n material.polygonOffset === true &&\n (material.polygonOffsetFactor !== 0 || material.polygonOffsetUnits !== 0),\n ),\n });\n\n for (let k = 0; k + 2 < count; k += 3) {\n for (let c = 0; c < 3; c += 1) {\n const v = index?.getX(k + c) ?? k + c;\n const x = position.getX(v);\n const y = position.getY(v);\n const z = position.getZ(v);\n\n for (let axis = 0; axis < 3; axis += 1) {\n const value = e[axis]! * x + e[axis + 4]! * y + e[axis + 8]! * z + e[axis + 12]!;\n\n world[c * 3 + axis] = value;\n min[axis] = Math.min(min[axis]!, value);\n max[axis] = Math.max(max[axis]!, value);\n }\n }\n\n const [ax, ay, az, bx, by, bz, cx, cy, cz] = world as [\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n ];\n const ux = bx - ax;\n const uy = by - ay;\n const uz = bz - az;\n const vx = cx - ax;\n const vy = cy - ay;\n const vz = cz - az;\n let nx = uy * vz - uz * vy;\n let ny = uz * vx - ux * vz;\n let nz = ux * vy - uy * vx;\n const length = Math.hypot(nx, ny, nz);\n\n // Slivers under ~1e-5 m² show nothing.\n if (!(length / 2 >= 1e-5)) {\n continue;\n }\n\n nx /= length;\n ny /= length;\n nz /= length;\n\n const d = nx * ax + ny * ay + nz * az;\n const id = owner.length;\n const key: [number, number, number, number] = [\n Math.round(nx * QUANT),\n Math.round(ny * QUANT),\n Math.round(nz * QUANT),\n Math.round(d / gap),\n ];\n const bucket = buckets.get(keyOf(...key));\n\n owner.push(part);\n planes.push(nx, ny, nz, d);\n corners.push(...world);\n keys.push(key);\n\n if (bucket) {\n bucket.push(id);\n } else {\n buckets.set(keyOf(...key), [id]);\n }\n }\n },\n );\n\n // Overlap in s's plane, with a margin so triangles merely sharing an edge don't count.\n const margin = gap / 2;\n const flat = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];\n // s's plane axes from the last `overlaps`: u, then v.\n const frame = [0, 0, 0, 0, 0, 0];\n\n const overlaps = (s: number, t: number) => {\n const S = s * 9;\n const T = t * 9;\n let ux = corners[S + 3]! - corners[S]!;\n let uy = corners[S + 4]! - corners[S + 1]!;\n let uz = corners[S + 5]! - corners[S + 2]!;\n const length = Math.hypot(ux, uy, uz) || 1;\n\n ux /= length;\n uy /= length;\n uz /= length;\n\n const nx = planes[s * 4]!;\n const ny = planes[s * 4 + 1]!;\n const nz = planes[s * 4 + 2]!;\n const vx = ny * uz - nz * uy;\n const vy = nz * ux - nx * uz;\n const vz = nx * uy - ny * ux;\n\n frame[0] = ux;\n frame[1] = uy;\n frame[2] = uz;\n frame[3] = vx;\n frame[4] = vy;\n frame[5] = vz;\n\n // Corners 0–2 are s, 3–5 are t, as (x, y) in the plane.\n for (let i = 0; i < 6; i += 1) {\n const at = i < 3 ? S + i * 3 : T + (i - 3) * 3;\n const px = corners[at]!;\n const py = corners[at + 1]!;\n const pz = corners[at + 2]!;\n\n flat[i * 2] = px * ux + py * uy + pz * uz;\n flat[i * 2 + 1] = px * vx + py * vy + pz * vz;\n }\n\n for (let edge = 0; edge < 6; edge += 1) {\n const from = edge;\n const to = edge < 3 ? (edge + 1) % 3 : 3 + ((edge + 1) % 3);\n const ex = flat[to * 2]! - flat[from * 2]!;\n const ey = flat[to * 2 + 1]! - flat[from * 2 + 1]!;\n const edgeLength = Math.hypot(ex, ey) || 1;\n const axisX = -ey / edgeLength;\n const axisY = ex / edgeLength;\n let p0 = Infinity;\n let p1 = -Infinity;\n let q0 = Infinity;\n let q1 = -Infinity;\n\n for (let i = 0; i < 6; i += 1) {\n const value = flat[i * 2]! * axisX + flat[i * 2 + 1]! * axisY;\n\n if (i < 3) {\n p0 = Math.min(p0, value);\n p1 = Math.max(p1, value);\n } else {\n q0 = Math.min(q0, value);\n q1 = Math.max(q1, value);\n }\n }\n\n if (Math.min(p1, q1) - Math.max(p0, q0) < margin) {\n return false;\n }\n }\n\n return true;\n };\n\n /**\n * The overlap `overlaps` just found, as area and centroid in s's plane: t's triangle clipped to\n * s's (Sutherland–Hodgman), then the shoelace formula.\n */\n const clipped = () => {\n const turn =\n (flat[2]! - flat[0]!) * (flat[5]! - flat[1]!) - (flat[4]! - flat[0]!) * (flat[3]! - flat[1]!);\n // s's corners counter-clockwise.\n const order = turn >= 0 ? [0, 1, 2] : [0, 2, 1];\n let polygon = flat.slice(6, 12);\n\n for (let edge = 0; edge < 3 && polygon.length >= 6; edge += 1) {\n const ax = flat[order[edge]! * 2]!;\n const ay = flat[order[edge]! * 2 + 1]!;\n const ex = flat[order[(edge + 1) % 3]! * 2]! - ax;\n const ey = flat[order[(edge + 1) % 3]! * 2 + 1]! - ay;\n const next: number[] = [];\n const n = polygon.length / 2;\n\n for (let i = 0; i < n; i += 1) {\n const px = polygon[i * 2]!;\n const py = polygon[i * 2 + 1]!;\n const qx = polygon[((i + 1) % n) * 2]!;\n const qy = polygon[((i + 1) % n) * 2 + 1]!;\n const dp = ex * (py - ay) - ey * (px - ax);\n const dq = ex * (qy - ay) - ey * (qx - ax);\n\n if (dp >= 0) {\n next.push(px, py);\n }\n\n if (dp >= 0 !== dq >= 0) {\n const k = dp / (dp - dq);\n\n next.push(px + (qx - px) * k, py + (qy - py) * k);\n }\n }\n\n polygon = next;\n }\n\n let twice = 0;\n let cx = 0;\n let cy = 0;\n const n = polygon.length / 2;\n\n for (let i = 0; i < n; i += 1) {\n const x0 = polygon[i * 2]!;\n const y0 = polygon[i * 2 + 1]!;\n const x1 = polygon[((i + 1) % n) * 2]!;\n const y1 = polygon[((i + 1) % n) * 2 + 1]!;\n const cross = x0 * y1 - x1 * y0;\n\n twice += cross;\n cx += (x0 + x1) * cross;\n cy += (y0 + y1) * cross;\n }\n\n return Math.abs(twice) < 1e-12\n ? { area: 0, x: 0, y: 0 }\n : { area: Math.abs(twice) / 2, x: cx / (3 * twice), y: cy / (3 * twice) };\n };\n\n // Every corner of t within `gap` of s's plane, measured where t stands.\n const onPlane = (s: number, t: number) => {\n const nx = planes[s * 4]!;\n const ny = planes[s * 4 + 1]!;\n const nz = planes[s * 4 + 2]!;\n const d = planes[s * 4 + 3]!;\n\n for (let c = 0; c < 3; c += 1) {\n const at = t * 9 + c * 3;\n\n if (Math.abs(nx * corners[at]! + ny * corners[at + 1]! + nz * corners[at + 2]! - d) > gap) {\n return false;\n }\n }\n\n return true;\n };\n\n type Pair = {\n a: number;\n b: number;\n triangles: number;\n area: number;\n /** Area-weighted sums of the overlap's centre. */\n centre: [number, number, number];\n planes: Map<string, number>;\n };\n\n const pairs = new Map<string, Pair>();\n\n // Every neighbouring bucket, so near-equal planes that round apart still meet.\n for (let s = 0; s < owner.length; s += 1) {\n const [kx, ky, kz, kd] = keys[s]!;\n\n for (let i = -1; i <= 1; i += 1) {\n for (let j = -1; j <= 1; j += 1) {\n for (let k = -1; k <= 1; k += 1) {\n for (let l = -1; l <= 1; l += 1) {\n for (const t of buckets.get(keyOf(kx + i, ky + j, kz + k, kd + l)) ?? []) {\n const a = owner[s]!;\n const b = owner[t]!;\n\n if (\n t <= s ||\n (a === b && !self) ||\n !canFight(parts[a]!, parts[b]!) ||\n planes[s * 4]! * planes[t * 4]! +\n planes[s * 4 + 1]! * planes[t * 4 + 1]! +\n planes[s * 4 + 2]! * planes[t * 4 + 2]! <\n PARALLEL ||\n !onPlane(s, t) ||\n !onPlane(t, s) ||\n !overlaps(s, t)\n ) {\n continue;\n }\n\n const key = a < b ? `${a}:${b}` : `${b}:${a}`;\n const pair = pairs.get(key) ?? {\n a: Math.min(a, b),\n b: Math.max(a, b),\n triangles: 0,\n area: 0,\n centre: [0, 0, 0],\n planes: new Map<string, number>(),\n };\n const nx = planes[s * 4]!;\n const ny = planes[s * 4 + 1]!;\n const nz = planes[s * 4 + 2]!;\n const d = planes[s * 4 + 3]!;\n const { area, x, y } = clipped();\n const plane = planeLabel(nx, ny, nz, d);\n\n pair.triangles += 1;\n pair.area += area;\n pair.centre[0] += area * (x * frame[0]! + y * frame[3]! + d * nx);\n pair.centre[1] += area * (x * frame[1]! + y * frame[4]! + d * ny);\n pair.centre[2] += area * (x * frame[2]! + y * frame[5]! + d * nz);\n pair.planes.set(plane, (pair.planes.get(plane) ?? 0) + area);\n pairs.set(key, pair);\n }\n }\n }\n }\n }\n }\n\n // Fold identical pairs (pooled copies of one prop, all at one spot) into one row each.\n const rows = new Map<string, ZFightingRow & { pairs: (readonly [Mesh, Mesh])[] }>();\n\n for (const pair of [...pairs.values()].sort((x, y) => y.triangles - x.triangles)) {\n const meshes = [parts[pair.a]!.mesh, parts[pair.b]!.mesh] as const;\n const a = labelOf(parts[pair.a]!);\n const b = labelOf(parts[pair.b]!);\n const planeLabels = [...pair.planes.entries()]\n .sort((x, y) => y[1] - x[1])\n .map(([plane]) => plane);\n const key = JSON.stringify([a, b, pair.triangles, planeLabels]);\n const folded = rows.get(key);\n\n if (folded) {\n folded.count += 1;\n folded.pairs.push(meshes);\n continue;\n }\n\n const weight = pair.area > 0 ? pair.area : 1;\n const row = withHidden(\n withHidden(\n {\n a,\n b,\n triangles: pair.triangles,\n planes: planeLabels,\n overlap: {\n area: round(pair.area, 4),\n centre: pair.centre.map((sum) => round(sum / weight)) as [number, number, number],\n },\n count: 1,\n },\n \"meshes\",\n meshes,\n ),\n \"pairs\",\n [meshes],\n );\n\n rows.set(key, row);\n }\n\n return [...rows.values()];\n};\n","/**\n * One budgets file per app: what a frame may cost, and what each part of the scene may. The\n * matchers in `@zkmake/three-audit/vitest`, the `three-audit --budgets` CLI and three-meter's HUD\n * all read it, so the numbers live in one place.\n */\nimport { type AnyObject3D, type Skip } from \"./scene.ts\";\nimport { countDraws, countTriangles, listMeshes } from \"./triangles.ts\";\n\n/** What one frame may cost, all parts together. */\nexport type FrameBudget = {\n /** The frame rate to hold: three-meter's `targetFps`. */\n fps?: number | undefined;\n /** Draw calls a frame makes. */\n calls?: number | undefined;\n triangles?: number | undefined;\n /** Texture memory in MB. No reader checks it yet; kept for a perf bench. */\n textureMB?: number | undefined;\n};\n\n/** What one part (a model, a prop, a kart) may cost. */\nexport type PartBudget = {\n triangles?: number | undefined;\n /** Draw calls one main pass makes for it: see `countDraws`. */\n draws?: number | undefined;\n /** Visible meshes in it. */\n meshes?: number | undefined;\n};\n\nexport type BudgetsInput<Part extends string = string> = {\n frame: FrameBudget;\n parts?: Record<Part, PartBudget>;\n};\n\n/** `defineBudgets`'s result: frozen, every part named in its type. */\nexport type Budgets<Part extends string = string> = {\n readonly frame: Readonly<FrameBudget>;\n readonly parts: { readonly [Name in Part]: Readonly<PartBudget> };\n};\n\nconst FRAME_KEYS = [\"fps\", \"calls\", \"triangles\", \"textureMB\"] as const;\nconst PART_KEYS = [\"triangles\", \"draws\", \"meshes\"] as const;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** A copy of `value` with only `keys`, each a number ≥ 0, frozen. Throws naming `where`. */\nconst limits = <Key extends string>(value: unknown, keys: readonly Key[], where: string) => {\n if (!isRecord(value)) {\n throw new TypeError(`three-audit: budgets ${where} must be an object`);\n }\n\n const out: Partial<Record<Key, number>> = {};\n\n for (const [key, limit] of Object.entries(value)) {\n if (!keys.includes(key as Key)) {\n throw new TypeError(\n `three-audit: budgets ${where} has \"${key}\"; expected ${keys.map((name) => `\"${name}\"`).join(\", \")}`,\n );\n }\n\n if (limit === undefined) {\n continue;\n }\n\n if (typeof limit !== \"number\" || !(limit >= 0) || !Number.isFinite(limit)) {\n throw new TypeError(`three-audit: budgets ${where}.${key} must be a number ≥ 0`);\n }\n\n out[key as Key] = limit;\n }\n\n return Object.freeze(out);\n};\n\n/**\n * Checks budgets read from anywhere (a JSON file, a module) and returns them frozen. Throws a\n * `TypeError` naming the first bad key.\n */\nexport const parseBudgets = (value: unknown): Budgets => {\n if (!isRecord(value)) {\n throw new TypeError(\"three-audit: budgets must be an object with `frame` and `parts`\");\n }\n\n const parts: Record<string, Readonly<PartBudget>> = {};\n\n if (value.parts !== undefined) {\n if (!isRecord(value.parts)) {\n throw new TypeError(\"three-audit: budgets parts must be an object of part budgets\");\n }\n\n for (const [name, part] of Object.entries(value.parts)) {\n parts[name] = limits(part, PART_KEYS, `parts.${name}`);\n }\n }\n\n return Object.freeze({\n frame: limits(value.frame ?? {}, FRAME_KEYS, \"frame\"),\n parts: Object.freeze(parts),\n });\n};\n\n/**\n * The app's budgets, typed and frozen: one file every reader imports.\n *\n * export default defineBudgets({\n * frame: { fps: 60, calls: 150, triangles: 250_000 },\n * parts: { kart: { triangles: 7_500, draws: 12 } },\n * });\n *\n * `budgets.parts.kart` is typed, and a part not listed is a type error.\n */\nexport const defineBudgets = <const Part extends string = never>(\n budgets: BudgetsInput<Part>,\n): Budgets<Part> => parseBudgets(budgets) as Budgets<Part>;\n\nexport type BudgetMetric = \"triangles\" | \"draws\" | \"meshes\";\n\nexport type BudgetCheck = {\n metric: BudgetMetric;\n actual: number;\n limit: number;\n over: boolean;\n};\n\n/**\n * An object measured against a part budget (or the frame's `triangles` and `calls`), one row per\n * limit the budget sets. Nothing over is a pass.\n */\nexport const checkBudget = (\n root: AnyObject3D,\n budget: PartBudget | FrameBudget,\n options: { skip?: Skip | undefined } = {},\n): BudgetCheck[] => {\n const part = budget as PartBudget & FrameBudget;\n const draws = part.draws ?? part.calls;\n const rows: BudgetCheck[] = [];\n const add = (metric: BudgetMetric, limit: number | undefined, measure: () => number) => {\n if (limit !== undefined) {\n const actual = measure();\n\n rows.push({ metric, actual, limit, over: actual > limit });\n }\n };\n\n add(\"triangles\", part.triangles, () => countTriangles(root, options));\n add(\"draws\", draws, () => countDraws(root, options));\n add(\"meshes\", part.meshes, () => listMeshes(root, options).length);\n\n return rows;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"budgets-BtXN-bkX.js","names":[],"sources":["../src/scene.ts","../src/bad-geometry.ts","../src/triangles.ts","../src/z-fighting.ts","../src/budgets.ts"],"sourcesContent":["/**\n * Scene-graph plumbing every check shares: the walk, mesh kinds, triangle counts, tags and names.\n *\n * Nothing here imports three at runtime; objects are recognised by three's own `is*` flags, so the\n * checks work on whatever copy of three the app bundles.\n */\nimport type { BatchedMesh, BufferGeometry, InstancedMesh, Material, Mesh, Object3D } from \"three\";\n\n/**\n * Any three.js object, from whichever copy of three's types the app has. The checks take this, so\n * an app on a newer `@types/three` than this package was built against passes its scene as is.\n */\nexport type AnyObject3D = { readonly isObject3D: true; readonly children: readonly object[] };\n\n/** The object as three's own type, for the walk. */\nexport const asObject3D = (object: AnyObject3D) => object as unknown as Object3D;\n\n/** Leave an object and everything under it out of a check. */\nexport type Skip = (object: Object3D) => boolean;\n\n/**\n * What a check sees of a bake (`@zkmake/three-batch`'s `bake` and `<Baked>`), which hides its\n * parts and draws one merged mesh per material instead. `sources`: the parts, as if shown, and not\n * the merge, so rows name real parts. `merged`: what the renderer draws.\n */\nexport type Bakes = \"sources\" | \"merged\";\n\nexport type WalkOptions = {\n skip?: Skip | undefined;\n /** Prune hidden objects and their subtrees, as the renderer does. */\n visibleOnly?: boolean;\n /** With `sources`: prune bake results, and walk the parts a bake hid as though visible. */\n bakes?: Bakes | undefined;\n};\n\n/**\n * A part a bake hid in favour of its merge: flagged `userData.bakeSource` by three-batch, or (for\n * bakes from before that flag) inside an object that holds a `userData.bakedResult` merge.\n */\nexport const isBakeSource = (object: Object3D) => {\n if (object.userData.bakeSource === true) {\n return true;\n }\n\n for (let at = object.parent; at; at = at.parent) {\n if (at.children.some((child) => child.userData.bakedResult === true)) {\n return true;\n }\n }\n\n return false;\n};\n\n/** Depth-first, in child order. A skipped (or, with `visibleOnly`, hidden) object prunes its subtree. */\nexport const walk = (root: Object3D, options: WalkOptions, visit: (object: Object3D) => void) => {\n const stack = [root];\n const sources = options.bakes === \"sources\";\n\n while (stack.length > 0) {\n const object = stack.pop()!;\n\n if (\n (sources && object.userData.bakedResult === true) ||\n (options.visibleOnly === true && !object.visible && !(sources && isBakeSource(object))) ||\n options.skip?.(object) === true\n ) {\n continue;\n }\n\n visit(object);\n\n for (let i = object.children.length - 1; i >= 0; i -= 1) {\n stack.push(object.children[i]!);\n }\n }\n};\n\nexport const isMesh = (object: Object3D): object is Mesh => (object as Mesh).isMesh === true;\n\n/** Anything the renderer draws: a mesh, line, point cloud or sprite. */\nexport const isDrawable = (object: Object3D) => {\n const flags = object as Partial<Record<\"isMesh\" | \"isLine\" | \"isPoints\" | \"isSprite\", boolean>>;\n\n return (\n flags.isMesh === true ||\n flags.isLine === true ||\n flags.isPoints === true ||\n flags.isSprite === true\n );\n};\n\nexport const isInstanced = (mesh: Mesh): mesh is InstancedMesh =>\n (mesh as InstancedMesh).isInstancedMesh === true;\n\nexport const isBatched = (mesh: Mesh): mesh is BatchedMesh =>\n (mesh as unknown as BatchedMesh).isBatchedMesh === true;\n\n/** Triangles one draw of this geometry makes: indexed or not, within its draw range. */\nexport const triangleCount = (geometry: BufferGeometry) => {\n const elements = geometry.index?.count ?? geometry.attributes.position?.count ?? 0;\n const { start, count } = geometry.drawRange;\n const drawn = Math.min(elements, start + count) - start;\n\n return Math.max(0, Math.floor(drawn / 3));\n};\n\n/**\n * A batch's visible instances by the geometry each draws: how many, and that geometry's triangles\n * (its range in the shared buffer). In first-drawn order.\n */\nexport const batchedGeometries = (batch: BatchedMesh) => {\n const geometries = new Map<number, { instances: number; triangles: number }>();\n\n for (let id = 0; id < batch.maxInstanceCount; id += 1) {\n let geometryId: number;\n\n // Deleted and never-used ids throw.\n try {\n if (!batch.getVisibleAt(id)) {\n continue;\n }\n\n geometryId = batch.getGeometryIdAt(id);\n } catch {\n continue;\n }\n\n const known = geometries.get(geometryId);\n\n if (known) {\n known.instances += 1;\n continue;\n }\n\n const range = batch.getGeometryRangeAt(geometryId) as {\n count?: number;\n indexCount: number;\n vertexCount: number;\n };\n const elements = range.count ?? (batch.geometry.index ? range.indexCount : range.vertexCount);\n\n geometries.set(geometryId, { instances: 1, triangles: Math.floor(elements / 3) });\n }\n\n return geometries;\n};\n\n/** Triangles a batch draws across its visible instances, and how many instances that is. */\nexport const batchedTriangles = (batch: BatchedMesh) => {\n let triangles = 0;\n let instances = 0;\n\n for (const geometry of batchedGeometries(batch).values()) {\n triangles += geometry.triangles * geometry.instances;\n instances += geometry.instances;\n }\n\n return { instances, triangles };\n};\n\n/** The tag of the nearest tagged ancestor (the object itself included), or `null`. */\nexport const tagOf = (object: Object3D, tagKey: string) => {\n for (let at: Object3D | null = object; at; at = at.parent) {\n const tag: unknown = at.userData[tagKey];\n\n if (tag !== undefined && tag !== null) {\n return String(tag);\n }\n }\n\n return null;\n};\n\n/**\n * A skip that keeps only what's tagged `tag`: prunes objects tagged otherwise, and meshes outside\n * any object tagged `tag`. Throws when nothing under `root` carries the tag.\n */\nexport const onlyTag = (root: Object3D, tag: string, tagKey: string, skip?: Skip): Skip => {\n let found = false;\n\n walk(root, { skip }, (object) => {\n const own: unknown = object.userData[tagKey];\n\n found ||= own !== undefined && own !== null && String(own) === tag;\n });\n\n if (!found) {\n throw new Error(`three-audit: nothing is tagged userData.${tagKey} = \"${tag}\"`);\n }\n\n return (object) => {\n if (skip?.(object) === true) {\n return true;\n }\n\n const own: unknown = object.userData[tagKey];\n\n if (own !== undefined && own !== null && String(own) !== tag) {\n return true;\n }\n\n return isDrawable(object) && tagOf(object, tagKey) !== tag;\n };\n};\n\nexport const hasTag = (root: Object3D, tagKey: string, skip?: Skip) => {\n let found = false;\n\n walk(root, { skip }, (object) => {\n found ||= object.userData[tagKey] !== undefined && object.userData[tagKey] !== null;\n });\n\n return found;\n};\n\nexport const meshName = (mesh: Mesh) => mesh.name || mesh.geometry.name || mesh.geometry.type;\n\n/**\n * The named ancestors above an object, outermost first, from `root` (included when named) down:\n * at most the nearest `depth`, with `…` for the rest. An unnamed ancestor carrying `tagKey` goes\n * by its tag.\n */\nexport const pathOf = (object: Object3D, root: Object3D | null, tagKey?: string, depth = 3) => {\n const names: string[] = [];\n\n for (let at = object.parent; at; at = at === root ? null : at.parent) {\n const tag: unknown = tagKey === undefined ? undefined : at.userData[tagKey];\n const name = at.name || (tag === undefined || tag === null ? \"\" : String(tag));\n\n if (name) {\n names.unshift(name);\n }\n }\n\n return names.length > depth ? [\"…\", ...names.slice(-depth)] : names;\n};\n\n/** `path/to/mesh [material]`: enough to find a mesh in the scene without walking it by hand. */\nexport const meshLabel = (mesh: Mesh, root: Object3D | null, tagKey?: string) =>\n `${[...pathOf(mesh, root, tagKey), meshName(mesh)].join(\"/\")} [${materialName(mesh)}]`;\n\nexport const materialName = (mesh: Mesh) => {\n const material = (Array.isArray(mesh.material) ? mesh.material[0] : mesh.material) as\n | Material\n | undefined;\n\n return material ? material.name || material.type : \"none\";\n};\n\n/** Adds a property JSON and `console.table` leave out: a live object that would serialise the scene. */\nexport const withHidden = <Row extends object, Key extends string, Value>(\n row: Row,\n key: Key,\n value: Value,\n) => Object.defineProperty(row, key, { value, enumerable: false }) as Row & Record<Key, Value>;\n","/**\n * Geometry that lights a pixel NaN: a non-finite position or normal, or a zero-length normal on a\n * triangle with area. Bloom and other blurs spread one NaN pixel into a black block.\n */\nimport type { BufferGeometry, Mesh } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n type Bakes,\n isMesh,\n meshLabel,\n meshName,\n type Skip,\n tagOf,\n walk,\n withHidden,\n} from \"./scene.ts\";\n\nexport type BadGeometryOptions = {\n skip?: Skip | undefined;\n /** `userData` key naming the part a mesh belongs to, for the `under` column. */\n tagKey?: string;\n /** A bake's parts (default), or its merged meshes: see `Bakes`. Not both, or each counts twice. */\n bakes?: Bakes | undefined;\n};\n\nexport type BadGeometryRow = {\n mesh: string;\n /** The tagged part it sits in, or its parent's name. */\n under: string;\n /** Vertices with a NaN or infinite position or normal component. */\n nonFinite: number;\n /** Vertices of triangles with area whose normal has no length. */\n zeroNormals: number;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly object: Mesh;\n};\n\nconst corner = (geometry: BufferGeometry, k: number) => geometry.index?.getX(k) ?? k;\n\nconst check = (geometry: BufferGeometry) => {\n const position = geometry.attributes.position!;\n const normal = geometry.attributes.normal;\n const flagged = new Uint8Array(position.count);\n let nonFinite = 0;\n let zeroNormals = 0;\n\n for (let v = 0; v < position.count; v += 1) {\n const finite =\n Number.isFinite(position.getX(v)) &&\n Number.isFinite(position.getY(v)) &&\n Number.isFinite(position.getZ(v)) &&\n (!normal ||\n (Number.isFinite(normal.getX(v)) &&\n Number.isFinite(normal.getY(v)) &&\n Number.isFinite(normal.getZ(v))));\n\n if (!finite) {\n flagged[v] = 1;\n nonFinite += 1;\n }\n }\n\n if (!normal) {\n return { nonFinite, zeroNormals };\n }\n\n const corners = geometry.index?.count ?? position.count;\n\n for (let k = 0; k + 2 < corners; k += 3) {\n const a = corner(geometry, k);\n const b = corner(geometry, k + 1);\n const c = corner(geometry, k + 2);\n const ux = position.getX(b) - position.getX(a);\n const uy = position.getY(b) - position.getY(a);\n const uz = position.getZ(b) - position.getZ(a);\n const vx = position.getX(c) - position.getX(a);\n const vy = position.getY(c) - position.getY(a);\n const vz = position.getZ(c) - position.getZ(a);\n const area = Math.hypot(uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx);\n\n if (!(area > 1e-8)) {\n continue;\n }\n\n for (const v of [a, b, c]) {\n if (flagged[v] === 0 && Math.hypot(normal.getX(v), normal.getY(v), normal.getZ(v)) < 1e-4) {\n flagged[v] = 1;\n zeroNormals += 1;\n }\n }\n }\n\n return { nonFinite, zeroNormals };\n};\n\n/** Meshes with geometry that renders NaN, hidden ones included. `[]` is a pass. */\nexport const findBadGeometry = (\n root: AnyObject3D,\n options: BadGeometryOptions = {},\n): BadGeometryRow[] => {\n const tagKey = options.tagKey ?? \"studioObject\";\n const rows: BadGeometryRow[] = [];\n\n walk(asObject3D(root), { skip: options.skip, bakes: options.bakes ?? \"sources\" }, (object) => {\n if (!isMesh(object) || !object.geometry.attributes.position) {\n return;\n }\n\n const { nonFinite, zeroNormals } = check(object.geometry);\n\n if (nonFinite > 0 || zeroNormals > 0) {\n const under = tagOf(object, tagKey) ?? object.parent?.name ?? \"\";\n\n rows.push(\n withHidden({ mesh: meshName(object), under, nonFinite, zeroNormals }, \"object\", object),\n );\n }\n });\n\n return rows;\n};\n\nexport type EmptyMeshRow = {\n /** `path/to/mesh [material]`. */\n mesh: string;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly object: Mesh;\n};\n\n/**\n * Meshes with no vertices, hidden ones included: what a merge of nothing leaves behind. They draw\n * nothing, but each one is still a draw call and a leak to dispose. A draw range of zero doesn't\n * count: that's usually a pool waiting to fill.\n */\nexport const findEmptyMeshes = (\n root: AnyObject3D,\n options: BadGeometryOptions = {},\n): EmptyMeshRow[] => {\n const scene = asObject3D(root);\n const rows: EmptyMeshRow[] = [];\n\n walk(scene, { skip: options.skip, bakes: options.bakes ?? \"sources\" }, (object) => {\n if (isMesh(object) && (object.geometry.attributes.position?.count ?? 0) === 0) {\n rows.push(\n withHidden(\n { mesh: meshLabel(object, scene, options.tagKey ?? \"studioObject\") },\n \"object\",\n object,\n ),\n );\n }\n });\n\n return rows;\n};\n","/**\n * Where a triangle budget goes: the total one pass draws, the meshes drawing it, and the\n * geometries behind them.\n */\nimport type { BatchedMesh, BufferGeometry, Mesh } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n batchedGeometries,\n batchedTriangles,\n isBatched,\n isDrawable,\n isInstanced,\n isMesh,\n materialName,\n meshName,\n pathOf,\n type Skip,\n triangleCount,\n walk,\n withHidden,\n} from \"./scene.ts\";\n\nexport type TriangleOptions = { skip?: Skip | undefined };\n\nexport type MeshRow = {\n /** `path/to/mesh`: its named ancestors, then its name (or its geometry's name or type). */\n name: string;\n material: string;\n /** Triangles in one copy. A batch's is an average: `geometryCensus` breaks it down. */\n triangles: number;\n instances: number;\n total: number;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly mesh: Mesh;\n};\n\nexport type GeometryRow = {\n /**\n * `geometry.name`, or its type and the start of its uuid. For a geometry in a batch, the batch's\n * `label` for it, or `batch#id`.\n */\n geometry: string;\n /** Copies drawn: every mesh using it, instances counted. */\n uses: number;\n triangles: number;\n total: number;\n /** Its part of the census's total, 0–1. */\n share: number;\n /** With `budget`: whether `share` is over it. */\n overBudget?: boolean;\n};\n\nexport type CensusOptions = TriangleOptions & {\n /**\n * Names a geometry inside a batch, e.g. from the app's own table of what it added. Default\n * `batch#id`, the batch named by its path.\n */\n label?: ((batch: BatchedMesh, geometryId: number) => string | undefined) | undefined;\n /** Flag rows whose share of the total is over this, 0–1: `0.2` for anything over 20%. */\n budget?: number | undefined;\n};\n\nconst cost = (mesh: Mesh) => {\n if (isBatched(mesh)) {\n return batchedTriangles(mesh);\n }\n\n const instances = isInstanced(mesh) ? mesh.count : 1;\n\n return { instances, triangles: triangleCount(mesh.geometry) * instances };\n};\n\n/**\n * Triangles one pass draws: visible meshes, instances counted, no frustum culling. A shadow pass\n * adds roughly as much again for each shadow-casting light.\n */\nexport const countTriangles = (root: AnyObject3D, options: TriangleOptions = {}) => {\n let total = 0;\n\n walk(asObject3D(root), { skip: options.skip, visibleOnly: true }, (object) => {\n if (isMesh(object)) {\n total += cost(object).triangles;\n }\n });\n\n return total;\n};\n\n/**\n * The triangles in a geometry (one draw, within its draw range: `triangleCount`) or that an object\n * draws (visible meshes, instances counted: `countTriangles`). The one helper a test needs.\n */\nexport const triangles = (\n target: AnyObject3D | { readonly isBufferGeometry: true },\n options: TriangleOptions = {},\n) =>\n \"isBufferGeometry\" in target\n ? triangleCount(target as unknown as BufferGeometry)\n : countTriangles(target, options);\n\n/** Every visible mesh, most triangles drawn first. */\nexport const listMeshes = (scene: AnyObject3D, options: TriangleOptions = {}): MeshRow[] => {\n const root = asObject3D(scene);\n const rows: MeshRow[] = [];\n\n walk(root, { skip: options.skip, visibleOnly: true }, (object) => {\n if (!isMesh(object)) {\n return;\n }\n\n const { instances, triangles } = cost(object);\n const row = {\n name: [...pathOf(object, root), meshName(object)].join(\"/\"),\n material: materialName(object),\n triangles: instances > 0 ? Math.round(triangles / instances) : 0,\n instances,\n total: triangles,\n };\n\n rows.push(withHidden(row, \"mesh\", object));\n });\n\n return rows.sort((a, b) => b.total - a.total);\n};\n\n/**\n * Geometries by triangles drawn, hidden meshes included, most first. Name geometries\n * (`geometry.name`) to make this readable. A batch counts each geometry in it separately, by its\n * visible instances: the row a budget is really spent on.\n */\nexport const geometryCensus = (root: AnyObject3D, options: CensusOptions = {}): GeometryRow[] => {\n const scene = asObject3D(root);\n const rows = new Map<string, GeometryRow>();\n const add = (key: string, triangles: number, uses: number) => {\n const row = rows.get(key) ?? { geometry: key, uses: 0, triangles, total: 0, share: 0 };\n\n row.uses += uses;\n row.total += triangles * uses;\n rows.set(key, row);\n };\n\n walk(scene, { skip: options.skip }, (object) => {\n if (!isMesh(object) || !object.geometry.attributes.position) {\n return;\n }\n\n if (isBatched(object)) {\n const name = [...pathOf(object, scene), meshName(object)].join(\"/\");\n\n for (const [id, { instances, triangles }] of batchedGeometries(object)) {\n add(options.label?.(object, id) ?? `${name}#${id}`, triangles, instances);\n }\n\n return;\n }\n\n const geometry = object.geometry;\n\n add(\n geometry.name || `${geometry.type}:${geometry.uuid.slice(0, 6)}`,\n triangleCount(geometry),\n isInstanced(object) ? object.count : 1,\n );\n });\n\n const all = [...rows.values()];\n const sum = all.reduce((total, row) => total + row.total, 0);\n\n for (const row of all) {\n row.share = sum > 0 ? Math.round((row.total / sum) * 1000) / 1000 : 0;\n\n if (options.budget !== undefined) {\n row.overBudget = row.total / Math.max(sum, 1) > options.budget;\n }\n }\n\n return all.sort((a, b) => b.total - a.total);\n};\n\n/**\n * Draw calls one main pass makes: one per visible mesh, line or point cloud, or one per material\n * group for a mesh with several materials. Before frustum culling; each shadow-casting light adds\n * one per caster. `recordDrawLedger` counts the real thing.\n */\nexport const countDraws = (root: AnyObject3D, options: TriangleOptions = {}) => {\n let draws = 0;\n\n walk(asObject3D(root), { skip: options.skip, visibleOnly: true }, (object) => {\n if (!isDrawable(object)) {\n return;\n }\n\n const drawable = object as Partial<Mesh>;\n\n const material = drawable.material;\n\n if (Array.isArray(material)) {\n const groups = drawable.geometry?.groups ?? [];\n\n draws += groups.filter(\n (group) => material[group.materialIndex ?? 0]?.visible !== false,\n ).length;\n } else if (material?.visible !== false) {\n draws += 1;\n }\n });\n\n return draws;\n};\n","/**\n * Z-fighting found by geometry instead of by eye: pairs of meshes with triangles in (nearly) one\n * plane, facing the same way, overlapping. Those flicker wherever depth precision runs out.\n *\n * Triangles are bucketed by their quantised plane (normal and offset), so only triangles in\n * neighbouring buckets are compared. A pair counts only when each triangle's corners lie within\n * `gap` of the other's plane, measured at the triangles: the offsets are measured from the world\n * origin, and far from it two faces a degree or two apart can share one while metres apart where\n * they stand. Overlap in the shared plane is a separating-axis test, then clipped for its area.\n *\n * A bake's hidden parts are checked in place of its merge (`bakes`). Left out, as they can't\n * fight: other hidden objects, instanced and batched meshes (and geometry a\n * shader positions, `InstancedBufferGeometry`), pairs where neither side writes depth or one\n * doesn't test it, and pairs where either material has a polygon offset (the usual decal fix).\n */\nimport type { Material, Mesh } from \"three\";\n\nimport {\n type AnyObject3D,\n asObject3D,\n type Bakes,\n hasTag,\n isBatched,\n isInstanced,\n isMesh,\n meshLabel,\n type Skip,\n tagOf,\n walk,\n withHidden,\n} from \"./scene.ts\";\n\nexport type ZFightingOptions = {\n /**\n * How close two planes count as one, in world units. 0.004 (4 mm at 1 unit = 1 m) is about what\n * depth precision loses with a camera ~50 m out. Scale it with your units.\n */\n gap?: number;\n /** Also compare triangles within one mesh: for merged geometry whose parts share a buffer. */\n self?: boolean;\n /**\n * Only meshes inside a tagged object (`userData[tagKey]`), so a studio's ground and props stay\n * out. Defaults to true when anything under the root is tagged, false otherwise.\n */\n tagged?: boolean;\n tagKey?: string;\n skip?: Skip | undefined;\n /**\n * A bake's parts (default), or its merged meshes: see `Bakes`. The parts name what to fix; the\n * merge is the same faces under one name.\n */\n bakes?: Bakes | undefined;\n};\n\nexport type ZFightingRow = {\n /** `path/to/mesh [material] @ (x, y, z)`: named ancestors, and the centre in world space. */\n a: string;\n b: string;\n /** Overlapping triangle pairs between the two. */\n triangles: number;\n /**\n * The planes they share, most overlap first: `z = -3.000, facing +z` when axis-aligned, else\n * `(nx, ny, nz) · p = d`. Both faces point along the normal: opposite-facing faces never pair.\n */\n planes: string[];\n /** Where they overlap: the area in world units², and its centre in world space. */\n overlap: { area: number; centre: [number, number, number] };\n /** Identical pairs (same labels, triangles and planes) folded into this row. */\n count: number;\n /** Not enumerable: kept out of JSON and `console.table`. The first pair folded in. */\n readonly meshes: readonly [Mesh, Mesh];\n /** Not enumerable: every pair folded in. */\n readonly pairs: readonly (readonly [Mesh, Mesh])[];\n};\n\n/** Normals quantised to 1/40: neighbouring buckets are searched, so this only sets bucket size. */\nconst QUANT = 40;\n/** Normals this aligned count as parallel (about 2.6°). */\nconst PARALLEL = 0.999;\n\ntype Part = {\n mesh: Mesh;\n label: string;\n min: number[];\n max: number[];\n /** Some material writes depth / tests it / offsets it. */\n writes: boolean;\n tests: boolean;\n offset: boolean;\n};\n\nconst round = (value: number, places = 2) => {\n const scale = 10 ** places;\n const rounded = Math.round(value * scale) / scale;\n\n return rounded === 0 ? 0 : rounded;\n};\n\nconst labelOf = (part: Part) => {\n const centre = part.min.map((low, axis) => round((low + part.max[axis]!) / 2));\n\n return `${part.label} @ (${centre.join(\", \")})`;\n};\n\nconst fixed = (value: number) => round(value, 3).toFixed(3);\n\n/** `z = -3.000, facing +z` for an axis-aligned plane, `(nx, ny, nz) · p = d` for any other. */\nconst planeLabel = (nx: number, ny: number, nz: number, d: number) => {\n for (const [axis, value] of [\n [\"x\", nx],\n [\"y\", ny],\n [\"z\", nz],\n ] as const) {\n if (Math.abs(value) > 0.9999) {\n return `${axis} = ${fixed(d / value)}, facing ${value > 0 ? \"+\" : \"-\"}${axis}`;\n }\n }\n\n return `(${fixed(nx)}, ${fixed(ny)}, ${fixed(nz)}) · p = ${fixed(d)}`;\n};\n\nconst materialsOf = (mesh: Mesh) =>\n (Array.isArray(mesh.material) ? mesh.material : [mesh.material]).filter(\n (material): material is Material => material !== undefined && material !== null,\n );\n\n/** Whether two parts can fight: one writes depth the other tests, and neither is offset. */\nconst canFight = (a: Part, b: Part) =>\n !a.offset && !b.offset && ((a.writes && b.tests) || (b.writes && a.tests));\n\n/** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */\nexport const findZFighting = (\n scene: AnyObject3D,\n options: ZFightingOptions = {},\n): ZFightingRow[] => {\n const root = asObject3D(scene);\n const gap = options.gap ?? 0.004;\n const self = options.self ?? false;\n const tagKey = options.tagKey ?? \"studioObject\";\n const tagged = options.tagged ?? hasTag(root, tagKey, options.skip);\n\n root.updateWorldMatrix(true, true);\n\n const parts: Part[] = [];\n // Per triangle: its part, plane (nx, ny, nz, d) and world corners (9 numbers).\n const owner: number[] = [];\n const planes: number[] = [];\n const corners: number[] = [];\n const keys: [number, number, number, number][] = [];\n const buckets = new Map<string, number[]>();\n const keyOf = (x: number, y: number, z: number, d: number) => `${x},${y},${z},${d}`;\n\n walk(\n root,\n { skip: options.skip, visibleOnly: true, bakes: options.bakes ?? \"sources\" },\n (object) => {\n if (\n !isMesh(object) ||\n isInstanced(object) ||\n isBatched(object) ||\n (object.geometry as { isInstancedBufferGeometry?: boolean }).isInstancedBufferGeometry ===\n true ||\n !object.geometry.attributes.position\n ) {\n return;\n }\n\n if (tagged && tagOf(object, tagKey) === null) {\n return;\n }\n\n const materials = materialsOf(object).filter((material) => material.visible !== false);\n\n if (materials.length === 0) {\n return;\n }\n\n const part = parts.length;\n const { index, attributes } = object.geometry;\n const position = attributes.position!;\n const e = object.matrixWorld.elements;\n const min = [Infinity, Infinity, Infinity];\n const max = [-Infinity, -Infinity, -Infinity];\n const count = index?.count ?? position.count;\n const world = [0, 0, 0, 0, 0, 0, 0, 0, 0];\n\n parts.push({\n mesh: object,\n label: meshLabel(object, root, tagKey),\n min,\n max,\n writes: materials.some((material) => material.depthWrite !== false),\n tests: materials.some((material) => material.depthTest !== false),\n offset: materials.some(\n (material) =>\n material.polygonOffset === true &&\n (material.polygonOffsetFactor !== 0 || material.polygonOffsetUnits !== 0),\n ),\n });\n\n for (let k = 0; k + 2 < count; k += 3) {\n for (let c = 0; c < 3; c += 1) {\n const v = index?.getX(k + c) ?? k + c;\n const x = position.getX(v);\n const y = position.getY(v);\n const z = position.getZ(v);\n\n for (let axis = 0; axis < 3; axis += 1) {\n const value = e[axis]! * x + e[axis + 4]! * y + e[axis + 8]! * z + e[axis + 12]!;\n\n world[c * 3 + axis] = value;\n min[axis] = Math.min(min[axis]!, value);\n max[axis] = Math.max(max[axis]!, value);\n }\n }\n\n const [ax, ay, az, bx, by, bz, cx, cy, cz] = world as [\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n number,\n ];\n const ux = bx - ax;\n const uy = by - ay;\n const uz = bz - az;\n const vx = cx - ax;\n const vy = cy - ay;\n const vz = cz - az;\n let nx = uy * vz - uz * vy;\n let ny = uz * vx - ux * vz;\n let nz = ux * vy - uy * vx;\n const length = Math.hypot(nx, ny, nz);\n\n // Slivers under ~1e-5 m² show nothing.\n if (!(length / 2 >= 1e-5)) {\n continue;\n }\n\n nx /= length;\n ny /= length;\n nz /= length;\n\n const d = nx * ax + ny * ay + nz * az;\n const id = owner.length;\n const key: [number, number, number, number] = [\n Math.round(nx * QUANT),\n Math.round(ny * QUANT),\n Math.round(nz * QUANT),\n Math.round(d / gap),\n ];\n const bucket = buckets.get(keyOf(...key));\n\n owner.push(part);\n planes.push(nx, ny, nz, d);\n corners.push(...world);\n keys.push(key);\n\n if (bucket) {\n bucket.push(id);\n } else {\n buckets.set(keyOf(...key), [id]);\n }\n }\n },\n );\n\n // Overlap in s's plane, with a margin so triangles merely sharing an edge don't count.\n const margin = gap / 2;\n const flat = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];\n // s's plane axes from the last `overlaps`: u, then v.\n const frame = [0, 0, 0, 0, 0, 0];\n\n const overlaps = (s: number, t: number) => {\n const S = s * 9;\n const T = t * 9;\n let ux = corners[S + 3]! - corners[S]!;\n let uy = corners[S + 4]! - corners[S + 1]!;\n let uz = corners[S + 5]! - corners[S + 2]!;\n const length = Math.hypot(ux, uy, uz) || 1;\n\n ux /= length;\n uy /= length;\n uz /= length;\n\n const nx = planes[s * 4]!;\n const ny = planes[s * 4 + 1]!;\n const nz = planes[s * 4 + 2]!;\n const vx = ny * uz - nz * uy;\n const vy = nz * ux - nx * uz;\n const vz = nx * uy - ny * ux;\n\n frame[0] = ux;\n frame[1] = uy;\n frame[2] = uz;\n frame[3] = vx;\n frame[4] = vy;\n frame[5] = vz;\n\n // Corners 0–2 are s, 3–5 are t, as (x, y) in the plane.\n for (let i = 0; i < 6; i += 1) {\n const at = i < 3 ? S + i * 3 : T + (i - 3) * 3;\n const px = corners[at]!;\n const py = corners[at + 1]!;\n const pz = corners[at + 2]!;\n\n flat[i * 2] = px * ux + py * uy + pz * uz;\n flat[i * 2 + 1] = px * vx + py * vy + pz * vz;\n }\n\n for (let edge = 0; edge < 6; edge += 1) {\n const from = edge;\n const to = edge < 3 ? (edge + 1) % 3 : 3 + ((edge + 1) % 3);\n const ex = flat[to * 2]! - flat[from * 2]!;\n const ey = flat[to * 2 + 1]! - flat[from * 2 + 1]!;\n const edgeLength = Math.hypot(ex, ey) || 1;\n const axisX = -ey / edgeLength;\n const axisY = ex / edgeLength;\n let p0 = Infinity;\n let p1 = -Infinity;\n let q0 = Infinity;\n let q1 = -Infinity;\n\n for (let i = 0; i < 6; i += 1) {\n const value = flat[i * 2]! * axisX + flat[i * 2 + 1]! * axisY;\n\n if (i < 3) {\n p0 = Math.min(p0, value);\n p1 = Math.max(p1, value);\n } else {\n q0 = Math.min(q0, value);\n q1 = Math.max(q1, value);\n }\n }\n\n if (Math.min(p1, q1) - Math.max(p0, q0) < margin) {\n return false;\n }\n }\n\n return true;\n };\n\n /**\n * The overlap `overlaps` just found, as area and centroid in s's plane: t's triangle clipped to\n * s's (Sutherland–Hodgman), then the shoelace formula.\n */\n const clipped = () => {\n const turn =\n (flat[2]! - flat[0]!) * (flat[5]! - flat[1]!) - (flat[4]! - flat[0]!) * (flat[3]! - flat[1]!);\n // s's corners counter-clockwise.\n const order = turn >= 0 ? [0, 1, 2] : [0, 2, 1];\n let polygon = flat.slice(6, 12);\n\n for (let edge = 0; edge < 3 && polygon.length >= 6; edge += 1) {\n const ax = flat[order[edge]! * 2]!;\n const ay = flat[order[edge]! * 2 + 1]!;\n const ex = flat[order[(edge + 1) % 3]! * 2]! - ax;\n const ey = flat[order[(edge + 1) % 3]! * 2 + 1]! - ay;\n const next: number[] = [];\n const n = polygon.length / 2;\n\n for (let i = 0; i < n; i += 1) {\n const px = polygon[i * 2]!;\n const py = polygon[i * 2 + 1]!;\n const qx = polygon[((i + 1) % n) * 2]!;\n const qy = polygon[((i + 1) % n) * 2 + 1]!;\n const dp = ex * (py - ay) - ey * (px - ax);\n const dq = ex * (qy - ay) - ey * (qx - ax);\n\n if (dp >= 0) {\n next.push(px, py);\n }\n\n if (dp >= 0 !== dq >= 0) {\n const k = dp / (dp - dq);\n\n next.push(px + (qx - px) * k, py + (qy - py) * k);\n }\n }\n\n polygon = next;\n }\n\n let twice = 0;\n let cx = 0;\n let cy = 0;\n const n = polygon.length / 2;\n\n for (let i = 0; i < n; i += 1) {\n const x0 = polygon[i * 2]!;\n const y0 = polygon[i * 2 + 1]!;\n const x1 = polygon[((i + 1) % n) * 2]!;\n const y1 = polygon[((i + 1) % n) * 2 + 1]!;\n const cross = x0 * y1 - x1 * y0;\n\n twice += cross;\n cx += (x0 + x1) * cross;\n cy += (y0 + y1) * cross;\n }\n\n return Math.abs(twice) < 1e-12\n ? { area: 0, x: 0, y: 0 }\n : { area: Math.abs(twice) / 2, x: cx / (3 * twice), y: cy / (3 * twice) };\n };\n\n // Every corner of t within `gap` of s's plane, measured where t stands.\n const onPlane = (s: number, t: number) => {\n const nx = planes[s * 4]!;\n const ny = planes[s * 4 + 1]!;\n const nz = planes[s * 4 + 2]!;\n const d = planes[s * 4 + 3]!;\n\n for (let c = 0; c < 3; c += 1) {\n const at = t * 9 + c * 3;\n\n if (Math.abs(nx * corners[at]! + ny * corners[at + 1]! + nz * corners[at + 2]! - d) > gap) {\n return false;\n }\n }\n\n return true;\n };\n\n type Pair = {\n a: number;\n b: number;\n triangles: number;\n area: number;\n /** Area-weighted sums of the overlap's centre. */\n centre: [number, number, number];\n planes: Map<string, number>;\n };\n\n const pairs = new Map<string, Pair>();\n\n // Every neighbouring bucket, so near-equal planes that round apart still meet.\n for (let s = 0; s < owner.length; s += 1) {\n const [kx, ky, kz, kd] = keys[s]!;\n\n for (let i = -1; i <= 1; i += 1) {\n for (let j = -1; j <= 1; j += 1) {\n for (let k = -1; k <= 1; k += 1) {\n for (let l = -1; l <= 1; l += 1) {\n for (const t of buckets.get(keyOf(kx + i, ky + j, kz + k, kd + l)) ?? []) {\n const a = owner[s]!;\n const b = owner[t]!;\n\n if (\n t <= s ||\n (a === b && !self) ||\n !canFight(parts[a]!, parts[b]!) ||\n planes[s * 4]! * planes[t * 4]! +\n planes[s * 4 + 1]! * planes[t * 4 + 1]! +\n planes[s * 4 + 2]! * planes[t * 4 + 2]! <\n PARALLEL ||\n !onPlane(s, t) ||\n !onPlane(t, s) ||\n !overlaps(s, t)\n ) {\n continue;\n }\n\n const key = a < b ? `${a}:${b}` : `${b}:${a}`;\n const pair = pairs.get(key) ?? {\n a: Math.min(a, b),\n b: Math.max(a, b),\n triangles: 0,\n area: 0,\n centre: [0, 0, 0],\n planes: new Map<string, number>(),\n };\n const nx = planes[s * 4]!;\n const ny = planes[s * 4 + 1]!;\n const nz = planes[s * 4 + 2]!;\n const d = planes[s * 4 + 3]!;\n const { area, x, y } = clipped();\n const plane = planeLabel(nx, ny, nz, d);\n\n pair.triangles += 1;\n pair.area += area;\n pair.centre[0] += area * (x * frame[0]! + y * frame[3]! + d * nx);\n pair.centre[1] += area * (x * frame[1]! + y * frame[4]! + d * ny);\n pair.centre[2] += area * (x * frame[2]! + y * frame[5]! + d * nz);\n pair.planes.set(plane, (pair.planes.get(plane) ?? 0) + area);\n pairs.set(key, pair);\n }\n }\n }\n }\n }\n }\n\n // Fold identical pairs (pooled copies of one prop, all at one spot) into one row each.\n const rows = new Map<string, ZFightingRow & { pairs: (readonly [Mesh, Mesh])[] }>();\n\n for (const pair of [...pairs.values()].sort((x, y) => y.triangles - x.triangles)) {\n const meshes = [parts[pair.a]!.mesh, parts[pair.b]!.mesh] as const;\n const a = labelOf(parts[pair.a]!);\n const b = labelOf(parts[pair.b]!);\n const planeLabels = [...pair.planes.entries()]\n .sort((x, y) => y[1] - x[1])\n .map(([plane]) => plane);\n const key = JSON.stringify([a, b, pair.triangles, planeLabels]);\n const folded = rows.get(key);\n\n if (folded) {\n folded.count += 1;\n folded.pairs.push(meshes);\n continue;\n }\n\n const weight = pair.area > 0 ? pair.area : 1;\n const row = withHidden(\n withHidden(\n {\n a,\n b,\n triangles: pair.triangles,\n planes: planeLabels,\n overlap: {\n area: round(pair.area, 4),\n centre: pair.centre.map((sum) => round(sum / weight)) as [number, number, number],\n },\n count: 1,\n },\n \"meshes\",\n meshes,\n ),\n \"pairs\",\n [meshes],\n );\n\n rows.set(key, row);\n }\n\n return [...rows.values()];\n};\n","import type { FrameBudget, PartBudget } from \"./define-budgets.ts\";\n/**\n * One budgets file per app: what a frame may cost, and what each part of the scene may. The\n * matchers in `@zkmake/three-audit/vitest`, the `three-audit --budgets` CLI and three-meter's HUD\n * all read it, so the numbers live in one place.\n */\nimport { type AnyObject3D, type Skip } from \"./scene.ts\";\nimport { countDraws, countTriangles, listMeshes } from \"./triangles.ts\";\n\nexport { defineBudgets, parseBudgets } from \"./define-budgets.ts\";\nexport type { Budgets, BudgetsInput, FrameBudget, PartBudget } from \"./define-budgets.ts\";\n\nexport type BudgetMetric = \"triangles\" | \"draws\" | \"meshes\";\n\nexport type BudgetCheck = {\n metric: BudgetMetric;\n actual: number;\n limit: number;\n over: boolean;\n};\n\n/**\n * An object measured against a part budget (or the frame's `triangles` and `calls`), one row per\n * limit the budget sets. Nothing over is a pass.\n */\nexport const checkBudget = (\n root: AnyObject3D,\n budget: PartBudget | FrameBudget,\n options: { skip?: Skip | undefined } = {},\n): BudgetCheck[] => {\n const part = budget as PartBudget & FrameBudget;\n const draws = part.draws ?? part.calls;\n const rows: BudgetCheck[] = [];\n const add = (metric: BudgetMetric, limit: number | undefined, measure: () => number) => {\n if (limit !== undefined) {\n const actual = measure();\n\n rows.push({ metric, actual, limit, over: actual > limit });\n }\n };\n\n add(\"triangles\", part.triangles, () => countTriangles(root, options));\n add(\"draws\", draws, () => countDraws(root, options));\n add(\"meshes\", part.meshes, () => listMeshes(root, options).length);\n\n return rows;\n};\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { i as PartBudget, r as FrameBudget } from "./define-budgets-DH6-ZyZy.js";
1
2
  import { BatchedMesh, BufferGeometry, Mesh, Object3D } from "three";
2
3
  /**
3
4
  * Any three.js object, from whichever copy of three's types the app has. The checks take this, so
@@ -159,49 +160,6 @@ type ZFightingRow = {
159
160
  };
160
161
  /** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */
161
162
  declare const findZFighting: (scene: AnyObject3D, options?: ZFightingOptions) => ZFightingRow[];
162
- /** What one frame may cost, all parts together. */
163
- type FrameBudget = {
164
- /** The frame rate to hold: three-meter's `targetFps`. */
165
- fps?: number | undefined;
166
- /** Draw calls a frame makes. */
167
- calls?: number | undefined;
168
- triangles?: number | undefined;
169
- /** Texture memory in MB. No reader checks it yet; kept for a perf bench. */
170
- textureMB?: number | undefined;
171
- };
172
- /** What one part (a model, a prop, a kart) may cost. */
173
- type PartBudget = {
174
- triangles?: number | undefined;
175
- /** Draw calls one main pass makes for it: see `countDraws`. */
176
- draws?: number | undefined;
177
- /** Visible meshes in it. */
178
- meshes?: number | undefined;
179
- };
180
- type BudgetsInput<Part extends string = string> = {
181
- frame: FrameBudget;
182
- parts?: Record<Part, PartBudget>;
183
- };
184
- /** `defineBudgets`'s result: frozen, every part named in its type. */
185
- type Budgets<Part extends string = string> = {
186
- readonly frame: Readonly<FrameBudget>;
187
- readonly parts: { readonly [Name in Part]: Readonly<PartBudget>; };
188
- };
189
- /**
190
- * Checks budgets read from anywhere (a JSON file, a module) and returns them frozen. Throws a
191
- * `TypeError` naming the first bad key.
192
- */
193
- declare const parseBudgets: (value: unknown) => Budgets;
194
- /**
195
- * The app's budgets, typed and frozen: one file every reader imports.
196
- *
197
- * export default defineBudgets({
198
- * frame: { fps: 60, calls: 150, triangles: 250_000 },
199
- * parts: { kart: { triangles: 7_500, draws: 12 } },
200
- * });
201
- *
202
- * `budgets.parts.kart` is typed, and a part not listed is a type error.
203
- */
204
- declare const defineBudgets: <const Part extends string = never>(budgets: BudgetsInput<Part>) => Budgets<Part>;
205
163
  type BudgetMetric = "triangles" | "draws" | "meshes";
206
164
  type BudgetCheck = {
207
165
  metric: BudgetMetric;
@@ -216,5 +174,5 @@ type BudgetCheck = {
216
174
  declare const checkBudget: (root: AnyObject3D, budget: PartBudget | FrameBudget, options?: {
217
175
  skip?: Skip | undefined;
218
176
  }) => BudgetCheck[];
219
- export { triangleCount as A, BadGeometryRow as C, AnyObject3D as D, findEmptyMeshes as E, Bakes as O, BadGeometryOptions as S, findBadGeometry as T, countDraws as _, FrameBudget as a, listMeshes as b, defineBudgets as c, ZFightingRow as d, findZFighting as f, TriangleOptions as g, MeshRow as h, BudgetsInput as i, Skip as k, parseBudgets as l, GeometryRow as m, BudgetMetric as n, PartBudget as o, CensusOptions as p, Budgets as r, checkBudget as s, BudgetCheck as t, ZFightingOptions as u, countTriangles as v, EmptyMeshRow as w, triangles as x, geometryCensus as y };
220
- //# sourceMappingURL=budgets-Dl-AOd9T.d.ts.map
177
+ export { Skip as C, Bakes as S, BadGeometryRow as _, ZFightingRow as a, findEmptyMeshes as b, GeometryRow as c, countDraws as d, countTriangles as f, BadGeometryOptions as g, triangles as h, ZFightingOptions as i, MeshRow as l, listMeshes as m, BudgetMetric as n, findZFighting as o, geometryCensus as p, checkBudget as r, CensusOptions as s, BudgetCheck as t, TriangleOptions as u, EmptyMeshRow as v, triangleCount as w, AnyObject3D as x, findBadGeometry as y };
178
+ //# sourceMappingURL=budgets-DWEObe-R.d.ts.map
@@ -0,0 +1,2 @@
1
+ import { a as defineBudgets, i as PartBudget, n as BudgetsInput, o as parseBudgets, r as FrameBudget, t as Budgets } from "./define-budgets-DH6-ZyZy.js";
2
+ export { Budgets, BudgetsInput, FrameBudget, PartBudget, defineBudgets, parseBudgets };
@@ -0,0 +1 @@
1
+ export { t as defineBudgets, n as parseBudgets } from "./define-budgets-77olu5Xb.js";
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { r as parseBudgets, t as checkBudget } from "./budgets-DQggDhEu.js";
3
- import { t as auditScene } from "./audit-scene-DHTFd-zs.js";
2
+ import { t as checkBudget } from "./budgets-BtXN-bkX.js";
3
+ import { t as auditScene } from "./audit-scene-BjB8raDp.js";
4
+ import { n as parseBudgets } from "./define-budgets-77olu5Xb.js";
4
5
  import { t as loadModel } from "./load-D4bV0-2t.js";
5
6
  import { a as meshLines, i as emptyMeshLines, n as budgetLines, o as plural, r as count, s as zFightingLines, t as badGeometryLines } from "./report-lines-Myjl7bcq.js";
6
7
  import { basename, extname, resolve } from "node:path";
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli/main.ts","../src/cli.ts"],"sourcesContent":["/**\n * `three-audit model.glb [more.gltf …]`: load each model in Node and check it for z-fighting and\n * NaN geometry, and with `--budgets` against the app's budgets. Exits 1 when a check fails, so it\n * can gate CI; `--json` for machines.\n */\n/* oxlint-disable no-console -- a CLI's output is the console */\nimport { readFileSync } from \"node:fs\";\nimport { basename, extname, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nimport { type AuditReport, auditScene } from \"../audit-scene.ts\";\nimport { type BudgetCheck, type Budgets, checkBudget, parseBudgets } from \"../budgets.ts\";\nimport { loadModel } from \"../node/load.ts\";\nimport {\n badGeometryLines,\n budgetLines,\n count,\n emptyMeshLines,\n meshLines,\n plural,\n zFightingLines,\n} from \"../report-lines.ts\";\nimport type { AnyObject3D, Skip } from \"../scene.ts\";\n\nconst HELP = `Usage: three-audit <model.glb|model.gltf>… [options]\n\nChecks glTF models for z-fighting (faces in one plane, facing the same way, overlapping) and\ngeometry that renders NaN (non-finite positions or normals, zero-length normals), and with\n--budgets, that each fits its budget. Exits 1 when any model fails a check, 2 when a model or the\nbudgets can't be read.\n\nOptions:\n --gap <m> How close two planes count as one, in metres (default 0.004)\n --self Also check for z-fighting within each mesh (merged geometry)\n --skip <regex> Leave out objects whose name matches, and everything under them (decals)\n --budgets <file> The app's budgets: JSON, or a module whose default export is them\n (defineBudgets). Each model is held to the part named like its file\n (kart.glb: parts.kart) and to the frame's triangles and calls\n --part <name> Hold every model to this part's budget instead\n --top <n> List the n costliest meshes (default 5, 0 for none)\n --json Print the reports as JSON\n --no-fail Exit 0 even when a check fails\n -h, --help Show this help\n -v, --version Show the version\n\nDraco and Meshopt models need draco3dgltf / meshoptimizer installed alongside.`;\n\n/** This package's version: its package.json is the nearest one up from here (`src/cli` or `dist`). */\nconst version = () => {\n for (let up = \"../\"; up.length <= 12; up += \"../\") {\n try {\n const pkg = JSON.parse(\n readFileSync(new URL(`${up}package.json`, import.meta.url), \"utf8\"),\n ) as {\n name?: string;\n version: string;\n };\n\n if (pkg.name === \"@zkmake/three-audit\") {\n return pkg.version;\n }\n } catch {\n // Not here; one more up.\n }\n }\n\n return \"unknown\";\n};\n\ntype Budgeted = {\n /** The part whose budget applied, `null` for the frame's alone. */\n part: string | null;\n checks: BudgetCheck[];\n};\n\ntype Result = { file: string; report?: AuditReport; budget?: Budgeted; error?: string };\n\nconst printReport = (file: string, report: AuditReport, top: number, budget?: Budgeted) => {\n const lines = [\n `${basename(file)} ${count(report.triangles)} triangles, ${count(report.draws)} draws`,\n ];\n\n if (report.zFighting.length === 0) {\n lines.push(\" ✓ no z-fighting\");\n } else {\n lines.push(\n ` ✗ ${report.zFighting.length} z-fighting ${plural(report.zFighting.length, \"pair\")}`,\n ...zFightingLines(report.zFighting),\n );\n }\n\n if (report.badGeometry.length === 0) {\n lines.push(\" ✓ no NaN geometry\");\n } else {\n lines.push(\n ` ✗ ${report.badGeometry.length} ${plural(report.badGeometry.length, \"mesh\", \"meshes\")} with NaN geometry`,\n ...badGeometryLines(report.badGeometry),\n );\n }\n\n if (report.emptyMeshes.length > 0) {\n lines.push(\n ` ! ${report.emptyMeshes.length} empty ${plural(report.emptyMeshes.length, \"mesh\", \"meshes\")} (no vertices)`,\n ...emptyMeshLines(report.emptyMeshes),\n );\n }\n\n if (budget) {\n const name = budget.part === null ? \"the frame budget\" : `budget \"${budget.part}\"`;\n\n if (budget.checks.length === 0) {\n lines.push(\n ` ! no budget for ${basename(file)}${budget.part === null ? \": name a part with --part\" : ` in part \"${budget.part}\"`}`,\n );\n } else {\n lines.push(\n budget.checks.some((check) => check.over) ? ` ✗ over ${name}` : ` ✓ within ${name}`,\n ...budgetLines(budget.checks),\n );\n }\n }\n\n if (top > 0 && report.meshes.length > 0) {\n lines.push(\" most triangles:\", ...meshLines(report.meshes));\n }\n\n return lines.join(\"\\n\");\n};\n\n/** A budgets file: JSON, or a module whose default export (or the module itself) is the budgets. */\nconst readBudgets = async (file: string) => {\n const path = resolve(file);\n\n if (extname(path) === \".json\") {\n return parseBudgets(JSON.parse(readFileSync(path, \"utf8\")));\n }\n\n const module = (await import(pathToFileURL(path).href)) as { default?: unknown };\n\n return parseBudgets(module.default ?? module);\n};\n\n/**\n * The model against its part's budget (`--part`, else the part named like the file) and the\n * frame's triangles and calls, which no one model may pass either.\n */\nconst budgetOf = (\n scene: AnyObject3D,\n file: string,\n budgets: Budgets,\n part: string | undefined,\n skip: Skip | undefined,\n) => {\n const stem = basename(file, extname(file));\n const name = part ?? (Object.hasOwn(budgets.parts, stem) ? stem : null);\n const own = name === null ? [] : checkBudget(scene, budgets.parts[name]!, { skip });\n const frame = checkBudget(\n scene,\n { triangles: budgets.frame.triangles, calls: budgets.frame.calls },\n { skip },\n );\n // A part's limit is the tighter one; the frame's still applies to what the part leaves unset.\n const checks = [\n ...own,\n ...frame.filter((check) => !own.some((mine) => mine.metric === check.metric)),\n ];\n\n return { part: name, checks };\n};\n\nexport const main = async (argv: string[]): Promise<number> => {\n let parsed;\n\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n budgets: { type: \"string\" },\n part: { type: \"string\" },\n gap: { type: \"string\" },\n self: { type: \"boolean\", default: false },\n skip: { type: \"string\" },\n top: { type: \"string\" },\n json: { type: \"boolean\", default: false },\n \"no-fail\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n console.error(`three-audit: ${(error as Error).message}\\n\\n${HELP}`);\n\n return 2;\n }\n\n const { values, positionals } = parsed;\n\n if (values.help) {\n console.log(HELP);\n\n return 0;\n }\n\n if (values.version) {\n console.log(version());\n\n return 0;\n }\n\n if (positionals.length === 0) {\n console.error(HELP);\n\n return 2;\n }\n\n const gap = values.gap === undefined ? undefined : Number(values.gap);\n const top = values.top === undefined ? 5 : Number(values.top);\n\n if ((gap !== undefined && !(gap > 0)) || !(top >= 0)) {\n console.error(\"three-audit: --gap must be a positive number and --top zero or more\");\n\n return 2;\n }\n\n if (values.part !== undefined && values.budgets === undefined) {\n console.error(\"three-audit: --part needs --budgets\");\n\n return 2;\n }\n\n let budgets: Budgets | undefined;\n\n if (values.budgets !== undefined) {\n try {\n budgets = await readBudgets(values.budgets);\n } catch (error) {\n console.error(`three-audit: couldn't read ${values.budgets}: ${(error as Error).message}`);\n\n return 2;\n }\n\n if (values.part !== undefined && !Object.hasOwn(budgets.parts, values.part)) {\n const known = Object.keys(budgets.parts);\n\n console.error(\n `three-audit: no part \"${values.part}\" in ${values.budgets}${known.length > 0 ? ` (it has ${known.join(\", \")})` : \"\"}`,\n );\n\n return 2;\n }\n }\n\n const skipPattern = values.skip === undefined ? undefined : new RegExp(values.skip);\n const skip: Skip | undefined = skipPattern\n ? (object) => skipPattern.test(object.name)\n : undefined;\n const results: Result[] = [];\n\n for (const file of positionals) {\n try {\n const scene = await loadModel(file);\n const report = auditScene(scene, {\n top,\n ...(gap === undefined ? {} : { gap }),\n ...(values.self ? { self: true } : {}),\n ...(skip ? { skip } : {}),\n });\n\n results.push({\n file,\n report,\n ...(budgets ? { budget: budgetOf(scene, file, budgets, values.part, skip) } : {}),\n });\n } catch (error) {\n results.push({ file, error: (error as Error).message });\n }\n }\n\n if (values.json) {\n console.log(JSON.stringify(results, null, 2));\n } else {\n console.log(\n results\n .map(({ file, report, budget, error }) =>\n report\n ? printReport(file, report, top, budget)\n : `${basename(file)}\\n ✗ couldn't read: ${error}`,\n )\n .join(\"\\n\\n\"),\n );\n }\n\n if (results.some((result) => result.error !== undefined)) {\n return 2;\n }\n\n const failed = results.some(\n ({ report, budget }) =>\n report!.zFighting.length > 0 ||\n report!.badGeometry.length > 0 ||\n budget?.checks.some((check) => check.over) === true,\n );\n\n return failed && !values[\"no-fail\"] ? 1 : 0;\n};\n","#!/usr/bin/env node\n/** The `three-audit` bin. The work is in `cli/main.ts`, which the tests import. */\nimport { main } from \"./cli/main.ts\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli/main.ts","../src/cli.ts"],"sourcesContent":["/**\n * `three-audit model.glb [more.gltf …]`: load each model in Node and check it for z-fighting and\n * NaN geometry, and with `--budgets` against the app's budgets. Exits 1 when a check fails, so it\n * can gate CI; `--json` for machines.\n */\n/* oxlint-disable no-console -- a CLI's output is the console */\nimport { readFileSync } from \"node:fs\";\nimport { basename, extname, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport { parseArgs } from \"node:util\";\n\nimport { type AuditReport, auditScene } from \"../audit-scene.ts\";\nimport { type BudgetCheck, type Budgets, checkBudget, parseBudgets } from \"../budgets.ts\";\nimport { loadModel } from \"../node/load.ts\";\nimport {\n badGeometryLines,\n budgetLines,\n count,\n emptyMeshLines,\n meshLines,\n plural,\n zFightingLines,\n} from \"../report-lines.ts\";\nimport type { AnyObject3D, Skip } from \"../scene.ts\";\n\nconst HELP = `Usage: three-audit <model.glb|model.gltf>… [options]\n\nChecks glTF models for z-fighting (faces in one plane, facing the same way, overlapping) and\ngeometry that renders NaN (non-finite positions or normals, zero-length normals), and with\n--budgets, that each fits its budget. Exits 1 when any model fails a check, 2 when a model or the\nbudgets can't be read.\n\nOptions:\n --gap <m> How close two planes count as one, in metres (default 0.004)\n --self Also check for z-fighting within each mesh (merged geometry)\n --skip <regex> Leave out objects whose name matches, and everything under them (decals)\n --budgets <file> The app's budgets: JSON, or a module whose default export is them\n (defineBudgets). Each model is held to the part named like its file\n (kart.glb: parts.kart) and to the frame's triangles and calls\n --part <name> Hold every model to this part's budget instead\n --top <n> List the n costliest meshes (default 5, 0 for none)\n --json Print the reports as JSON\n --no-fail Exit 0 even when a check fails\n -h, --help Show this help\n -v, --version Show the version\n\nDraco and Meshopt models need draco3dgltf / meshoptimizer installed alongside.`;\n\n/** This package's version: its package.json is the nearest one up from here (`src/cli` or `dist`). */\nconst version = () => {\n for (let up = \"../\"; up.length <= 12; up += \"../\") {\n try {\n const pkg = JSON.parse(\n readFileSync(new URL(`${up}package.json`, import.meta.url), \"utf8\"),\n ) as {\n name?: string;\n version: string;\n };\n\n if (pkg.name === \"@zkmake/three-audit\") {\n return pkg.version;\n }\n } catch {\n // Not here; one more up.\n }\n }\n\n return \"unknown\";\n};\n\ntype Budgeted = {\n /** The part whose budget applied, `null` for the frame's alone. */\n part: string | null;\n checks: BudgetCheck[];\n};\n\ntype Result = { file: string; report?: AuditReport; budget?: Budgeted; error?: string };\n\nconst printReport = (file: string, report: AuditReport, top: number, budget?: Budgeted) => {\n const lines = [\n `${basename(file)} ${count(report.triangles)} triangles, ${count(report.draws)} draws`,\n ];\n\n if (report.zFighting.length === 0) {\n lines.push(\" ✓ no z-fighting\");\n } else {\n lines.push(\n ` ✗ ${report.zFighting.length} z-fighting ${plural(report.zFighting.length, \"pair\")}`,\n ...zFightingLines(report.zFighting),\n );\n }\n\n if (report.badGeometry.length === 0) {\n lines.push(\" ✓ no NaN geometry\");\n } else {\n lines.push(\n ` ✗ ${report.badGeometry.length} ${plural(report.badGeometry.length, \"mesh\", \"meshes\")} with NaN geometry`,\n ...badGeometryLines(report.badGeometry),\n );\n }\n\n if (report.emptyMeshes.length > 0) {\n lines.push(\n ` ! ${report.emptyMeshes.length} empty ${plural(report.emptyMeshes.length, \"mesh\", \"meshes\")} (no vertices)`,\n ...emptyMeshLines(report.emptyMeshes),\n );\n }\n\n if (budget) {\n const name = budget.part === null ? \"the frame budget\" : `budget \"${budget.part}\"`;\n\n if (budget.checks.length === 0) {\n lines.push(\n ` ! no budget for ${basename(file)}${budget.part === null ? \": name a part with --part\" : ` in part \"${budget.part}\"`}`,\n );\n } else {\n lines.push(\n budget.checks.some((check) => check.over) ? ` ✗ over ${name}` : ` ✓ within ${name}`,\n ...budgetLines(budget.checks),\n );\n }\n }\n\n if (top > 0 && report.meshes.length > 0) {\n lines.push(\" most triangles:\", ...meshLines(report.meshes));\n }\n\n return lines.join(\"\\n\");\n};\n\n/** A budgets file: JSON, or a module whose default export (or the module itself) is the budgets. */\nconst readBudgets = async (file: string) => {\n const path = resolve(file);\n\n if (extname(path) === \".json\") {\n return parseBudgets(JSON.parse(readFileSync(path, \"utf8\")));\n }\n\n const module = (await import(pathToFileURL(path).href)) as { default?: unknown };\n\n return parseBudgets(module.default ?? module);\n};\n\n/**\n * The model against its part's budget (`--part`, else the part named like the file) and the\n * frame's triangles and calls, which no one model may pass either.\n */\nconst budgetOf = (\n scene: AnyObject3D,\n file: string,\n budgets: Budgets,\n part: string | undefined,\n skip: Skip | undefined,\n) => {\n const stem = basename(file, extname(file));\n const name = part ?? (Object.hasOwn(budgets.parts, stem) ? stem : null);\n const own = name === null ? [] : checkBudget(scene, budgets.parts[name]!, { skip });\n const frame = checkBudget(\n scene,\n { triangles: budgets.frame.triangles, calls: budgets.frame.calls },\n { skip },\n );\n // A part's limit is the tighter one; the frame's still applies to what the part leaves unset.\n const checks = [\n ...own,\n ...frame.filter((check) => !own.some((mine) => mine.metric === check.metric)),\n ];\n\n return { part: name, checks };\n};\n\nexport const main = async (argv: string[]): Promise<number> => {\n let parsed;\n\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n budgets: { type: \"string\" },\n part: { type: \"string\" },\n gap: { type: \"string\" },\n self: { type: \"boolean\", default: false },\n skip: { type: \"string\" },\n top: { type: \"string\" },\n json: { type: \"boolean\", default: false },\n \"no-fail\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n console.error(`three-audit: ${(error as Error).message}\\n\\n${HELP}`);\n\n return 2;\n }\n\n const { values, positionals } = parsed;\n\n if (values.help) {\n console.log(HELP);\n\n return 0;\n }\n\n if (values.version) {\n console.log(version());\n\n return 0;\n }\n\n if (positionals.length === 0) {\n console.error(HELP);\n\n return 2;\n }\n\n const gap = values.gap === undefined ? undefined : Number(values.gap);\n const top = values.top === undefined ? 5 : Number(values.top);\n\n if ((gap !== undefined && !(gap > 0)) || !(top >= 0)) {\n console.error(\"three-audit: --gap must be a positive number and --top zero or more\");\n\n return 2;\n }\n\n if (values.part !== undefined && values.budgets === undefined) {\n console.error(\"three-audit: --part needs --budgets\");\n\n return 2;\n }\n\n let budgets: Budgets | undefined;\n\n if (values.budgets !== undefined) {\n try {\n budgets = await readBudgets(values.budgets);\n } catch (error) {\n console.error(`three-audit: couldn't read ${values.budgets}: ${(error as Error).message}`);\n\n return 2;\n }\n\n if (values.part !== undefined && !Object.hasOwn(budgets.parts, values.part)) {\n const known = Object.keys(budgets.parts);\n\n console.error(\n `three-audit: no part \"${values.part}\" in ${values.budgets}${known.length > 0 ? ` (it has ${known.join(\", \")})` : \"\"}`,\n );\n\n return 2;\n }\n }\n\n const skipPattern = values.skip === undefined ? undefined : new RegExp(values.skip);\n const skip: Skip | undefined = skipPattern\n ? (object) => skipPattern.test(object.name)\n : undefined;\n const results: Result[] = [];\n\n for (const file of positionals) {\n try {\n const scene = await loadModel(file);\n const report = auditScene(scene, {\n top,\n ...(gap === undefined ? {} : { gap }),\n ...(values.self ? { self: true } : {}),\n ...(skip ? { skip } : {}),\n });\n\n results.push({\n file,\n report,\n ...(budgets ? { budget: budgetOf(scene, file, budgets, values.part, skip) } : {}),\n });\n } catch (error) {\n results.push({ file, error: (error as Error).message });\n }\n }\n\n if (values.json) {\n console.log(JSON.stringify(results, null, 2));\n } else {\n console.log(\n results\n .map(({ file, report, budget, error }) =>\n report\n ? printReport(file, report, top, budget)\n : `${basename(file)}\\n ✗ couldn't read: ${error}`,\n )\n .join(\"\\n\\n\"),\n );\n }\n\n if (results.some((result) => result.error !== undefined)) {\n return 2;\n }\n\n const failed = results.some(\n ({ report, budget }) =>\n report!.zFighting.length > 0 ||\n report!.badGeometry.length > 0 ||\n budget?.checks.some((check) => check.over) === true,\n );\n\n return failed && !values[\"no-fail\"] ? 1 : 0;\n};\n","#!/usr/bin/env node\n/** The `three-audit` bin. The work is in `cli/main.ts`, which the tests import. */\nimport { main } from \"./cli/main.ts\";\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ // src/define-budgets.ts
2
+ const FRAME_KEYS = [
3
+ "fps",
4
+ "calls",
5
+ "triangles",
6
+ "textureMB"
7
+ ];
8
+ const PART_KEYS = [
9
+ "triangles",
10
+ "draws",
11
+ "meshes"
12
+ ];
13
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
+ /** A copy of `value` with only `keys`, each a number ≥ 0, frozen. Throws naming `where`. */
15
+ const limits = (value, keys, where) => {
16
+ if (!isRecord(value)) throw new TypeError(`three-audit: budgets ${where} must be an object`);
17
+ const out = {};
18
+ for (const [key, limit] of Object.entries(value)) {
19
+ if (!keys.includes(key)) throw new TypeError(`three-audit: budgets ${where} has "${key}"; expected ${keys.map((name) => `"${name}"`).join(", ")}`);
20
+ if (limit === void 0) continue;
21
+ if (typeof limit !== "number" || !(limit >= 0) || !Number.isFinite(limit)) throw new TypeError(`three-audit: budgets ${where}.${key} must be a number ≥ 0`);
22
+ out[key] = limit;
23
+ }
24
+ return Object.freeze(out);
25
+ };
26
+ /**
27
+ * Checks budgets read from anywhere (a JSON file, a module) and returns them frozen. Throws a
28
+ * `TypeError` naming the first bad key.
29
+ */
30
+ const parseBudgets = (value) => {
31
+ if (!isRecord(value)) throw new TypeError("three-audit: budgets must be an object with `frame` and `parts`");
32
+ const parts = {};
33
+ if (value.parts !== void 0) {
34
+ if (!isRecord(value.parts)) throw new TypeError("three-audit: budgets parts must be an object of part budgets");
35
+ for (const [name, part] of Object.entries(value.parts)) parts[name] = limits(part, PART_KEYS, `parts.${name}`);
36
+ }
37
+ return Object.freeze({
38
+ frame: limits(value.frame ?? {}, FRAME_KEYS, "frame"),
39
+ parts: Object.freeze(parts)
40
+ });
41
+ };
42
+ /**
43
+ * The app's budgets, typed and frozen: one file every reader imports.
44
+ *
45
+ * export default defineBudgets({
46
+ * frame: { fps: 60, calls: 150, triangles: 250_000 },
47
+ * parts: { kart: { triangles: 7_500, draws: 12 } },
48
+ * });
49
+ *
50
+ * `budgets.parts.kart` is typed, and a part not listed is a type error.
51
+ */
52
+ const defineBudgets = (budgets) => parseBudgets(budgets);
53
+ export { parseBudgets as n, defineBudgets as t };
54
+
55
+ //# sourceMappingURL=define-budgets-77olu5Xb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-budgets-77olu5Xb.js","names":[],"sources":["../src/define-budgets.ts"],"sourcesContent":["/**\n * `@zkmake/three-audit/budgets`: the app's budgets file and nothing else, so an app's own code can\n * import it (three-meter's HUD reads it) without the checks coming along: no imports, its own\n * chunk. Measuring against it is `checkBudget` in the main entry.\n */\n\n/** What one frame may cost, all parts together. */\nexport type FrameBudget = {\n /** The frame rate to hold: three-meter's `targetFps`. */\n fps?: number | undefined;\n /** Draw calls a frame makes. */\n calls?: number | undefined;\n triangles?: number | undefined;\n /** Texture memory in MB. No reader checks it yet; kept for a perf bench. */\n textureMB?: number | undefined;\n};\n\n/** What one part (a model, a prop, a kart) may cost. */\nexport type PartBudget = {\n triangles?: number | undefined;\n /** Draw calls one main pass makes for it: see `countDraws`. */\n draws?: number | undefined;\n /** Visible meshes in it. */\n meshes?: number | undefined;\n};\n\nexport type BudgetsInput<Part extends string = string> = {\n frame: FrameBudget;\n parts?: Record<Part, PartBudget>;\n};\n\n/** `defineBudgets`'s result: frozen, every part named in its type. */\nexport type Budgets<Part extends string = string> = {\n readonly frame: Readonly<FrameBudget>;\n readonly parts: { readonly [Name in Part]: Readonly<PartBudget> };\n};\n\nconst FRAME_KEYS = [\"fps\", \"calls\", \"triangles\", \"textureMB\"] as const;\nconst PART_KEYS = [\"triangles\", \"draws\", \"meshes\"] as const;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** A copy of `value` with only `keys`, each a number ≥ 0, frozen. Throws naming `where`. */\nconst limits = <Key extends string>(value: unknown, keys: readonly Key[], where: string) => {\n if (!isRecord(value)) {\n throw new TypeError(`three-audit: budgets ${where} must be an object`);\n }\n\n const out: Partial<Record<Key, number>> = {};\n\n for (const [key, limit] of Object.entries(value)) {\n if (!keys.includes(key as Key)) {\n throw new TypeError(\n `three-audit: budgets ${where} has \"${key}\"; expected ${keys.map((name) => `\"${name}\"`).join(\", \")}`,\n );\n }\n\n if (limit === undefined) {\n continue;\n }\n\n if (typeof limit !== \"number\" || !(limit >= 0) || !Number.isFinite(limit)) {\n throw new TypeError(`three-audit: budgets ${where}.${key} must be a number ≥ 0`);\n }\n\n out[key as Key] = limit;\n }\n\n return Object.freeze(out);\n};\n\n/**\n * Checks budgets read from anywhere (a JSON file, a module) and returns them frozen. Throws a\n * `TypeError` naming the first bad key.\n */\nexport const parseBudgets = (value: unknown): Budgets => {\n if (!isRecord(value)) {\n throw new TypeError(\"three-audit: budgets must be an object with `frame` and `parts`\");\n }\n\n const parts: Record<string, Readonly<PartBudget>> = {};\n\n if (value.parts !== undefined) {\n if (!isRecord(value.parts)) {\n throw new TypeError(\"three-audit: budgets parts must be an object of part budgets\");\n }\n\n for (const [name, part] of Object.entries(value.parts)) {\n parts[name] = limits(part, PART_KEYS, `parts.${name}`);\n }\n }\n\n return Object.freeze({\n frame: limits(value.frame ?? {}, FRAME_KEYS, \"frame\"),\n parts: Object.freeze(parts),\n });\n};\n\n/**\n * The app's budgets, typed and frozen: one file every reader imports.\n *\n * export default defineBudgets({\n * frame: { fps: 60, calls: 150, triangles: 250_000 },\n * parts: { kart: { triangles: 7_500, draws: 12 } },\n * });\n *\n * `budgets.parts.kart` is typed, and a part not listed is a type error.\n */\nexport const defineBudgets = <const Part extends string = never>(\n budgets: BudgetsInput<Part>,\n): Budgets<Part> => parseBudgets(budgets) as Budgets<Part>;\n"],"mappings":";AAqCA,MAAM,aAAa;CAAC;CAAO;CAAS;CAAa;AAAW;AAC5D,MAAM,YAAY;CAAC;CAAa;CAAS;AAAQ;AAEjD,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;;AAGrE,MAAM,UAA8B,OAAgB,MAAsB,UAAkB;CAC1F,IAAI,CAAC,SAAS,KAAK,GACjB,MAAM,IAAI,UAAU,wBAAwB,MAAM,mBAAmB;CAGvE,MAAM,MAAoC,CAAC;CAE3C,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,GAAG;EAChD,IAAI,CAAC,KAAK,SAAS,GAAU,GAC3B,MAAM,IAAI,UACR,wBAAwB,MAAM,QAAQ,IAAI,cAAc,KAAK,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,GACnG;EAGF,IAAI,UAAU,KAAA,GACZ;EAGF,IAAI,OAAO,UAAU,YAAY,EAAE,SAAS,MAAM,CAAC,OAAO,SAAS,KAAK,GACtE,MAAM,IAAI,UAAU,wBAAwB,MAAM,GAAG,IAAI,sBAAsB;EAGjF,IAAI,OAAc;CACpB;CAEA,OAAO,OAAO,OAAO,GAAG;AAC1B;;;;;AAMA,MAAa,gBAAgB,UAA4B;CACvD,IAAI,CAAC,SAAS,KAAK,GACjB,MAAM,IAAI,UAAU,iEAAiE;CAGvF,MAAM,QAA8C,CAAC;CAErD,IAAI,MAAM,UAAU,KAAA,GAAW;EAC7B,IAAI,CAAC,SAAS,MAAM,KAAK,GACvB,MAAM,IAAI,UAAU,8DAA8D;EAGpF,KAAK,MAAM,CAAC,MAAM,SAAS,OAAO,QAAQ,MAAM,KAAK,GACnD,MAAM,QAAQ,OAAO,MAAM,WAAW,SAAS,MAAM;CAEzD;CAEA,OAAO,OAAO,OAAO;EACnB,OAAO,OAAO,MAAM,SAAS,CAAC,GAAG,YAAY,OAAO;EACpD,OAAO,OAAO,OAAO,KAAK;CAC5B,CAAC;AACH;;;;;;;;;;;AAYA,MAAa,iBACX,YACkB,aAAa,OAAO"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `@zkmake/three-audit/budgets`: the app's budgets file and nothing else, so an app's own code can
3
+ * import it (three-meter's HUD reads it) without the checks coming along: no imports, its own
4
+ * chunk. Measuring against it is `checkBudget` in the main entry.
5
+ */
6
+ /** What one frame may cost, all parts together. */
7
+ type FrameBudget = {
8
+ /** The frame rate to hold: three-meter's `targetFps`. */
9
+ fps?: number | undefined;
10
+ /** Draw calls a frame makes. */
11
+ calls?: number | undefined;
12
+ triangles?: number | undefined;
13
+ /** Texture memory in MB. No reader checks it yet; kept for a perf bench. */
14
+ textureMB?: number | undefined;
15
+ };
16
+ /** What one part (a model, a prop, a kart) may cost. */
17
+ type PartBudget = {
18
+ triangles?: number | undefined;
19
+ /** Draw calls one main pass makes for it: see `countDraws`. */
20
+ draws?: number | undefined;
21
+ /** Visible meshes in it. */
22
+ meshes?: number | undefined;
23
+ };
24
+ type BudgetsInput<Part extends string = string> = {
25
+ frame: FrameBudget;
26
+ parts?: Record<Part, PartBudget>;
27
+ };
28
+ /** `defineBudgets`'s result: frozen, every part named in its type. */
29
+ type Budgets<Part extends string = string> = {
30
+ readonly frame: Readonly<FrameBudget>;
31
+ readonly parts: { readonly [Name in Part]: Readonly<PartBudget>; };
32
+ };
33
+ /**
34
+ * Checks budgets read from anywhere (a JSON file, a module) and returns them frozen. Throws a
35
+ * `TypeError` naming the first bad key.
36
+ */
37
+ declare const parseBudgets: (value: unknown) => Budgets;
38
+ /**
39
+ * The app's budgets, typed and frozen: one file every reader imports.
40
+ *
41
+ * export default defineBudgets({
42
+ * frame: { fps: 60, calls: 150, triangles: 250_000 },
43
+ * parts: { kart: { triangles: 7_500, draws: 12 } },
44
+ * });
45
+ *
46
+ * `budgets.parts.kart` is typed, and a part not listed is a type error.
47
+ */
48
+ declare const defineBudgets: <const Part extends string = never>(budgets: BudgetsInput<Part>) => Budgets<Part>;
49
+ export { defineBudgets as a, PartBudget as i, BudgetsInput as n, parseBudgets as o, FrameBudget as r, Budgets as t };
50
+ //# sourceMappingURL=define-budgets-DH6-ZyZy.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
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";
1
+ import { a as defineBudgets, i as PartBudget, n as BudgetsInput, o as parseBudgets, r as FrameBudget, t as Budgets } from "./define-budgets-DH6-ZyZy.js";
2
+ import { C as Skip, S as Bakes, _ as BadGeometryRow, a as ZFightingRow, b as findEmptyMeshes, c as GeometryRow, d as countDraws, f as countTriangles, g as BadGeometryOptions, h as triangles, i as ZFightingOptions, l as MeshRow, m as listMeshes, n as BudgetMetric, o as findZFighting, p as geometryCensus, r as checkBudget, s as CensusOptions, t as BudgetCheck, u as TriangleOptions, v as EmptyMeshRow, w as triangleCount, x as AnyObject3D, y as findBadGeometry } from "./budgets-DWEObe-R.js";
2
3
  import { BatchedMesh } from "three";
3
4
  type AuditSceneOptions = Pick<ZFightingOptions, "bakes" | "gap" | "self" | "tagged"> & {
4
5
  skip?: Skip | undefined;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
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";
1
+ import { _ as pathOf, a as geometryCensus, c as findBadGeometry, d as batchedTriangles, f as isBatched, h as meshLabel, i as countTriangles, l as findEmptyMeshes, m as isMesh, n as findZFighting, o as listMeshes, p as isInstanced, r as countDraws, s as triangles, t as checkBudget, u as asObject3D, v as triangleCount, y as walk } from "./budgets-BtXN-bkX.js";
2
+ import { n as summarizeScene, t as auditScene } from "./audit-scene-BjB8raDp.js";
3
+ import { n as parseBudgets, t as defineBudgets } from "./define-budgets-77olu5Xb.js";
3
4
  // src/patch.ts
4
5
  /** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */
5
6
  const patch = (target, key, value) => {