@zkmake/three-audit 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/audit-scene-D5ZaENGZ.js +425 -0
- package/dist/audit-scene-D5ZaENGZ.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +144 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +1 -411
- package/dist/index.js.map +1 -1
- package/dist/load-nqfCU3-k.js +216 -0
- package/dist/load-nqfCU3-k.js.map +1 -0
- package/dist/node.d.ts +15 -0
- package/dist/node.js +2 -0
- package/package.json +18 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/scene.ts","../src/bad-geometry.ts","../src/triangles.ts","../src/z-fighting.ts","../src/audit-scene.ts","../src/patch.ts","../src/black-frames.ts","../src/draw-ledger.ts","../src/console-helpers.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/** Leave an object and everything under it out of a check. */\nexport type Skip = (object: Object3D) => boolean;\n\nexport type WalkOptions = {\n skip?: Skip | undefined;\n /** Prune hidden objects and their subtrees, as the renderer does. */\n visibleOnly?: boolean;\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\n while (stack.length > 0) {\n const object = stack.pop()!;\n\n if ((options.visibleOnly === true && !object.visible) || options.skip?.(object) === true) {\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\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/** 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 (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 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 triangles += Math.floor(elements / 3);\n instances += 1;\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\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\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, Object3D } from \"three\";\n\nimport { isMesh, meshName, type Skip, tagOf, walk, withHidden } 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};\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: Object3D,\n options: BadGeometryOptions = {},\n): BadGeometryRow[] => {\n const tagKey = options.tagKey ?? \"studioObject\";\n const rows: BadGeometryRow[] = [];\n\n walk(root, { skip: options.skip }, (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","/**\n * Where a triangle budget goes: the total one pass draws, the meshes drawing it, and the\n * geometries behind them.\n */\nimport type { Mesh, Object3D } from \"three\";\n\nimport {\n batchedTriangles,\n isBatched,\n isInstanced,\n isMesh,\n meshName,\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 name: string;\n /** Triangles in one copy (a batch's average). */\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 /** `geometry.name`, or its type and the start of its uuid. */\n geometry: string;\n /** Copies drawn: every mesh using it, instances counted. */\n uses: number;\n triangles: number;\n total: number;\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: Object3D, options: TriangleOptions = {}) => {\n let total = 0;\n\n walk(root, { skip: options.skip, visibleOnly: true }, (object) => {\n if (isMesh(object)) {\n total += cost(object).triangles;\n }\n });\n\n return total;\n};\n\n/** Every visible mesh, most triangles drawn first. */\nexport const listMeshes = (root: Object3D, options: TriangleOptions = {}): MeshRow[] => {\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: meshName(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. Batches are left out: their shared buffer is not one\n * geometry; `listMeshes` counts them.\n */\nexport const geometryCensus = (root: Object3D, options: TriangleOptions = {}): GeometryRow[] => {\n const rows = new Map<string, GeometryRow>();\n\n walk(root, { skip: options.skip }, (object) => {\n if (!isMesh(object) || isBatched(object) || !object.geometry.attributes.position) {\n return;\n }\n\n const geometry = object.geometry;\n const key = geometry.name || `${geometry.type}:${geometry.uuid.slice(0, 6)}`;\n const triangles = triangleCount(geometry);\n const uses = isInstanced(object) ? object.count : 1;\n const row = rows.get(key) ?? { geometry: key, uses: 0, triangles, total: 0 };\n\n row.uses += uses;\n row.total += triangles * uses;\n rows.set(key, row);\n });\n\n return [...rows.values()].sort((a, b) => b.total - a.total);\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; overlap in the shared plane is a separating-axis test.\n */\nimport type { Mesh, Object3D } from \"three\";\n\nimport {\n hasTag,\n isBatched,\n isInstanced,\n isMesh,\n materialName,\n meshName,\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\nexport type ZFightingRow = {\n /** `tag/mesh [material] @ (x, y, z)`, the centre in world space. */\n a: string;\n b: string;\n /** Overlapping triangle pairs between the two. */\n triangles: number;\n /** Not enumerable: kept out of JSON and `console.table`. */\n readonly meshes: 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 = { mesh: Mesh; tag: string | null; min: number[]; max: number[] };\n\nconst round = (value: number) => Math.round(value * 100) / 100;\n\nconst labelOf = (part: Part) => {\n const centre = part.min.map((low, axis) => round((low + part.max[axis]!) / 2));\n const tag = part.tag === null ? \"\" : `${part.tag}/`;\n\n return `${tag}${meshName(part.mesh)} [${materialName(part.mesh)}] @ (${centre.join(\", \")})`;\n};\n\n/** Pairs of meshes that z-fight, most overlapping triangles first. `[]` is a pass. */\nexport const findZFighting = (root: Object3D, options: ZFightingOptions = {}): ZFightingRow[] => {\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(root, { skip: options.skip }, (object) => {\n if (\n !isMesh(object) ||\n isInstanced(object) ||\n isBatched(object) ||\n !object.geometry.attributes.position\n ) {\n return;\n }\n\n const tag = tagOf(object, tagKey);\n\n if (tagged && tag === null) {\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({ mesh: object, tag, min, max });\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 // 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\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 // 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 const pairs = new Map<string, { a: number; b: number; triangles: number }>();\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 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 Math.abs(planes[s * 4 + 3]! - planes[t * 4 + 3]!) > gap ||\n !overlaps(s, t)\n ) {\n continue;\n }\n\n const key = a < b ? `${a}:${b}` : `${b}:${a}`;\n const pair = pairs.get(key) ?? { a: Math.min(a, b), b: Math.max(a, b), triangles: 0 };\n\n pair.triangles += 1;\n pairs.set(key, pair);\n }\n }\n }\n }\n }\n }\n\n return [...pairs.values()]\n .sort((x, y) => y.triangles - x.triangles)\n .map(({ a, b, triangles }) =>\n withHidden({ a: labelOf(parts[a]!), b: labelOf(parts[b]!), triangles }, \"meshes\", [\n parts[a]!.mesh,\n parts[b]!.mesh,\n ] as const),\n );\n};\n","import type { Object3D } from \"three\";\n\nimport { type BadGeometryRow, findBadGeometry } from \"./bad-geometry.ts\";\nimport type { Skip } from \"./scene.ts\";\nimport { countTriangles, listMeshes, type MeshRow } from \"./triangles.ts\";\nimport { findZFighting, type ZFightingOptions, type ZFightingRow } from \"./z-fighting.ts\";\n\nexport type AuditSceneOptions = Pick<ZFightingOptions, \"gap\" | \"self\" | \"tagged\"> & {\n skip?: Skip | undefined;\n tagKey?: string;\n /** How many of the costliest meshes to list. */\n top?: number;\n};\n\nexport type AuditReport = {\n triangles: number;\n zFighting: ZFightingRow[];\n badGeometry: BadGeometryRow[];\n /** The costliest meshes, most triangles first. */\n meshes: MeshRow[];\n};\n\n/**\n * Every check at once, JSON-safe: what a test asserts on or a headless browser prints. A clean\n * scene has `zFighting` and `badGeometry` both `[]`.\n */\nexport const auditScene = (root: Object3D, options: AuditSceneOptions = {}): AuditReport => {\n const { skip, tagKey } = options;\n\n return {\n triangles: countTriangles(root, { skip }),\n zFighting: findZFighting(root, options),\n badGeometry: findBadGeometry(root, tagKey === undefined ? { skip } : { skip, tagKey }),\n meshes: listMeshes(root, { skip }).slice(0, options.top ?? 10),\n };\n};\n","/** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */\nexport const patch = <Target extends object, Key extends keyof Target>(\n target: Target,\n key: Key,\n value: Target[Key],\n) => {\n const own = Object.hasOwn(target, key);\n const original = target[key];\n\n target[key] = value;\n\n return () => {\n if (own) {\n target[key] = original;\n } else {\n delete target[key];\n }\n };\n};\n","/**\n * Black-frame probe: after each frame, samples a grid of the drawing buffer and counts frames\n * where several samples are pure black. Catches the flash a NaN pixel makes once bloom spreads it,\n * which lasts one frame in twenty and never shows in a screenshot.\n *\n * WebGL only (`readPixels`). Each sample stalls the GPU pipeline, so watch, read, stop.\n */\nimport type { Camera, Object3D } from \"three\";\n\nimport { patch } from \"./patch.ts\";\n\n/** The `WebGLRenderer` methods the probe uses. */\nexport type ProbeRenderer = {\n render(scene: Object3D, camera: Camera): void;\n getContext(): unknown;\n getRenderTarget(): unknown;\n};\n\nexport type BlackFrameOptions = {\n /** Samples per side: 3 reads a 3 × 3 grid at the quarter lines. */\n grid?: number;\n /** Black samples that make a frame black. */\n threshold?: number;\n};\n\nexport type BlackFrameWatch = {\n /** Frames sampled since the start or the last `reset()`. */\n readonly frames: number;\n /** Which of those frames (1-based) were black. */\n readonly blackFrames: readonly number[];\n reset: () => void;\n /** Restore the renderer and stop sampling. */\n stop: () => void;\n};\n\ntype ReadPixels = Pick<\n WebGLRenderingContext,\n \"RGBA\" | \"UNSIGNED_BYTE\" | \"drawingBufferHeight\" | \"drawingBufferWidth\" | \"readPixels\"\n>;\n\n/**\n * Sample every frame the app draws to the screen. Several screen renders in one frame (an overlay\n * redrawn after the composer) count once, as the last of them: the image the display shows.\n * Browser only: frames are told apart by `requestAnimationFrame`.\n */\nexport const watchBlackFrames = (\n renderer: ProbeRenderer,\n options: BlackFrameOptions = {},\n): BlackFrameWatch => {\n const context = renderer.getContext() as Partial<ReadPixels> | null;\n\n if (typeof context?.readPixels !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs a WebGLRenderer\");\n }\n\n if (typeof requestAnimationFrame !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs requestAnimationFrame\");\n }\n\n const gl = context as ReadPixels;\n const grid = options.grid ?? 3;\n const threshold = options.threshold ?? 3;\n const pixel = new Uint8Array(4);\n const render = renderer.render;\n let frames = 0;\n let black: number[] = [];\n let pending: boolean | null = null;\n\n const isBlack = () => {\n let count = 0;\n\n for (let i = 1; i <= grid; i += 1) {\n for (let j = 1; j <= grid; j += 1) {\n const x = Math.floor((gl.drawingBufferWidth * i) / (grid + 1));\n const y = Math.floor((gl.drawingBufferHeight * j) / (grid + 1));\n\n gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);\n\n if (pixel[0]! < 4 && pixel[1]! < 4 && pixel[2]! < 4 && pixel[3]! > 250) {\n count += 1;\n }\n }\n }\n\n return count >= threshold;\n };\n\n // Commit the frame's last screen sample once per animation frame.\n let raf = 0;\n const commit = () => {\n if (pending !== null) {\n frames += 1;\n\n if (pending) {\n black.push(frames);\n }\n\n pending = null;\n }\n\n raf = requestAnimationFrame(commit);\n };\n\n raf = requestAnimationFrame(commit);\n\n const restore = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n render.call(this, scene, camera);\n\n // Read before the browser presents (and, without preserveDrawingBuffer, clears) the buffer.\n if (renderer.getRenderTarget() === null) {\n pending = isBlack();\n }\n });\n\n return {\n get frames() {\n return frames;\n },\n get blackFrames() {\n return black;\n },\n reset: () => {\n frames = 0;\n black = [];\n pending = null;\n },\n stop: () => {\n cancelAnimationFrame(raf);\n restore();\n },\n };\n};\n","/**\n * A ledger of every draw call over one frame: which object drew, in which pass. Exact counts from\n * the renderer itself, where `renderer.info` gives one total (and resets it every `render()`).\n *\n * WebGL only: it wraps `WebGLRenderer.renderBufferDirect`, which every draw goes through, and\n * `render()`, to tell passes apart. Draws with a depth or distance material are the shadow maps.\n */\nimport type { BufferGeometry, Camera, Material, Object3D, Scene } from \"three\";\n\nimport { patch } from \"./patch.ts\";\n\n/** The two `WebGLRenderer` methods the ledger wraps. */\nexport type LedgerRenderer = {\n render(scene: Object3D, camera: Camera): void;\n renderBufferDirect(\n camera: Camera,\n scene: Scene | null,\n geometry: BufferGeometry,\n material: Material,\n object: Object3D,\n group: unknown,\n ): void;\n};\n\nexport type DrawLedgerOptions = {\n /**\n * The group an object name files under. Default: up to the first `:` or `#`, so a bake's\n * `wagon:paint` and a strip's `grass#3` group as `wagon` and `grass`.\n */\n groupOf?: (name: string) => string;\n};\n\nexport type LedgerRow = {\n name: string;\n total: number;\n /** Draws per pass name. */\n passes: Record<string, number>;\n};\n\nexport type DrawLedger = {\n /**\n * Pass names in the order they first drew: `shadow`, then each `render()` call as its scene's\n * name, or `render 1`, `render 2`… when unnamed (a post-processing chain is several).\n */\n passes: string[];\n /** By group, most draws first. */\n groups: LedgerRow[];\n /** By object name, most draws first. */\n objects: LedgerRow[];\n draws: number;\n};\n\nexport type DrawLedgerRecording = {\n /** Restore the renderer and return what was drawn since `beginDrawLedger`. */\n end: () => DrawLedger;\n};\n\nconst defaultGroupOf = (name: string) => name.split(/[:#]/)[0] || name;\n\nconst nameOf = (object: Object3D, material: Material) =>\n object.name || `${object.type}:${material.name || material.type}`;\n\nconst tally = (rows: Map<string, LedgerRow>, name: string, pass: string) => {\n const row = rows.get(name) ?? { name, total: 0, passes: {} };\n\n row.total += 1;\n row.passes[pass] = (row.passes[pass] ?? 0) + 1;\n rows.set(name, row);\n};\n\nconst sorted = (rows: Map<string, LedgerRow>) =>\n [...rows.values()].sort((a, b) => b.total - a.total);\n\n/** Start counting every draw. Call `end()` after the frame to restore the renderer and read it. */\nexport const beginDrawLedger = (\n renderer: LedgerRenderer,\n options: DrawLedgerOptions = {},\n): DrawLedgerRecording => {\n if (typeof renderer.renderBufferDirect !== \"function\") {\n throw new TypeError(\"three-audit: the draw ledger needs a WebGLRenderer\");\n }\n\n const groupOf = options.groupOf ?? defaultGroupOf;\n const render = renderer.render;\n const renderBufferDirect = renderer.renderBufferDirect;\n const passes: string[] = [];\n const objects = new Map<string, LedgerRow>();\n const groups = new Map<string, LedgerRow>();\n let renders = 0;\n let current = \"direct\";\n let draws = 0;\n\n const restoreRender = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n const outer = current;\n\n renders += 1;\n current = scene.name || `render ${renders}`;\n\n try {\n render.call(this, scene, camera);\n } finally {\n current = outer;\n }\n });\n\n const restoreDraw = patch(\n renderer,\n \"renderBufferDirect\",\n function (this: unknown, camera, scene, geometry, material, object, group) {\n const shadow =\n (material as { isMeshDepthMaterial?: boolean }).isMeshDepthMaterial === true ||\n (material as { isMeshDistanceMaterial?: boolean }).isMeshDistanceMaterial === true;\n const pass = shadow ? \"shadow\" : current;\n const name = nameOf(object, material);\n\n if (!passes.includes(pass)) {\n passes.push(pass);\n }\n\n draws += 1;\n tally(objects, name, pass);\n tally(groups, groupOf(name), pass);\n renderBufferDirect.call(this, camera, scene, geometry, material, object, group);\n },\n );\n\n return {\n end: () => {\n restoreDraw();\n restoreRender();\n\n return { passes, groups: sorted(groups), objects: sorted(objects), draws };\n },\n };\n};\n\n/**\n * Record exactly one frame of an app that renders from `requestAnimationFrame` (three's\n * `setAnimationLoop`, React Three Fiber): from one animation frame to the next. A demand-driven\n * loop that renders nothing that frame gives an empty ledger. Browser only.\n */\nexport const recordDrawLedger = (renderer: LedgerRenderer, options: DrawLedgerOptions = {}) =>\n new Promise<DrawLedger>((resolve, reject) => {\n if (typeof requestAnimationFrame !== \"function\") {\n reject(new Error(\"three-audit: recordDrawLedger needs requestAnimationFrame\"));\n\n return;\n }\n\n requestAnimationFrame(() => {\n try {\n const recording = beginDrawLedger(renderer, options);\n\n requestAnimationFrame(() => resolve(recording.end()));\n } catch (error) {\n reject(error);\n }\n });\n });\n\n/** Rows shaped for `console.table`: one column per pass, then the total. */\nexport const ledgerTable = (rows: LedgerRow[], passes: string[]) =>\n rows.map((row) => ({\n name: row.name,\n ...Object.fromEntries(passes.map((pass) => [pass, row.passes[pass] ?? 0])),\n total: row.total,\n }));\n\n/** The ledger as two console tables: by group, and every object collapsed beneath. */\nexport const printDrawLedger = (ledger: DrawLedger) => {\n /* oxlint-disable no-console -- printing is this function's job */\n console.group(`Draw ledger: ${ledger.draws} draws in one frame`);\n console.table(ledgerTable(ledger.groups, ledger.passes));\n console.groupCollapsed(\"Every object\");\n console.table(ledgerTable(ledger.objects, ledger.passes));\n console.groupEnd();\n console.groupEnd();\n /* oxlint-enable no-console */\n};\n","/**\n * The checks as short console globals, for a dev page or a headless browser's `eval`:\n * `tris()`, `meshes()`, `census()`, `audit()`, `zfight()`, `report()`, and with a renderer\n * `ledger()` and `blackFrames()`.\n */\nimport type { Object3D } from \"three\";\n\nimport { auditScene } from \"./audit-scene.ts\";\nimport { findBadGeometry } from \"./bad-geometry.ts\";\nimport { type ProbeRenderer, watchBlackFrames } from \"./black-frames.ts\";\nimport { type LedgerRenderer, printDrawLedger, recordDrawLedger } from \"./draw-ledger.ts\";\nimport type { Skip } from \"./scene.ts\";\nimport { countTriangles, geometryCensus, listMeshes } from \"./triangles.ts\";\nimport { findZFighting } from \"./z-fighting.ts\";\n\nexport type AuditHelpersOptions = {\n /** A `WebGLRenderer`, for `ledger()` and `blackFrames()`. */\n renderer?: (LedgerRenderer & ProbeRenderer) | undefined;\n skip?: Skip | undefined;\n tagKey?: string;\n};\n\nconst NAMES = [\n \"auditRoot\",\n \"tris\",\n \"meshes\",\n \"census\",\n \"audit\",\n \"zfight\",\n \"report\",\n \"ledger\",\n \"blackFrames\",\n] as const;\n\nconst needsRenderer = (name: string) => () => {\n throw new Error(`three-audit: ${name}() needs installAuditHelpers(scene, { renderer })`);\n};\n\n/**\n * Put the helpers on `globalThis`. Returns a function that removes them.\n *\n * vanilla: installAuditHelpers(scene, { renderer })\n * R3F: const { scene, gl } = useThree();\n * useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);\n */\nexport const installAuditHelpers = (root: Object3D, options: AuditHelpersOptions = {}) => {\n const { renderer, skip } = options;\n const tagKey = options.tagKey ?? \"studioObject\";\n\n const helpers: Record<(typeof NAMES)[number], unknown> = {\n auditRoot: root,\n tris: () => countTriangles(root, { skip }),\n meshes: () => listMeshes(root, { skip }),\n census: () => geometryCensus(root, { skip }),\n audit: () => findBadGeometry(root, { skip, tagKey }),\n zfight: (gap?: number, self?: boolean) =>\n findZFighting(root, {\n skip,\n tagKey,\n ...(gap === undefined ? {} : { gap }),\n ...(self === undefined ? {} : { self }),\n }),\n report: () => auditScene(root, { skip, tagKey }),\n ledger: renderer\n ? async () => {\n const ledger = await recordDrawLedger(renderer);\n\n printDrawLedger(ledger);\n\n return ledger;\n }\n : needsRenderer(\"ledger\"),\n blackFrames: renderer\n ? (seconds = 10) =>\n new Promise<{ frames: number; blackFrames: readonly number[] }>((resolve) => {\n const watch = watchBlackFrames(renderer);\n\n setTimeout(() => {\n watch.stop();\n resolve({ frames: watch.frames, blackFrames: watch.blackFrames });\n }, seconds * 1000);\n })\n : needsRenderer(\"blackFrames\"),\n };\n\n Object.assign(globalThis, helpers);\n\n return () => {\n const scope = globalThis as Record<string, unknown>;\n\n for (const name of NAMES) {\n if (scope[name] === helpers[name]) {\n delete scope[name];\n }\n }\n };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/patch.ts","../src/black-frames.ts","../src/draw-ledger.ts","../src/console-helpers.ts"],"sourcesContent":["/** Swap `key` on `target` for `value`; the returned function puts back exactly what was there. */\nexport const patch = <Target extends object, Key extends keyof Target>(\n target: Target,\n key: Key,\n value: Target[Key],\n) => {\n const own = Object.hasOwn(target, key);\n const original = target[key];\n\n target[key] = value;\n\n return () => {\n if (own) {\n target[key] = original;\n } else {\n delete target[key];\n }\n };\n};\n","/**\n * Black-frame probe: after each frame, samples a grid of the drawing buffer and counts frames\n * where several samples are pure black. Catches the flash a NaN pixel makes once bloom spreads it,\n * which lasts one frame in twenty and never shows in a screenshot.\n *\n * WebGL only (`readPixels`). Each sample stalls the GPU pipeline, so watch, read, stop.\n */\nimport type { Camera, Object3D } from \"three\";\n\nimport { patch } from \"./patch.ts\";\n\n/** The `WebGLRenderer` methods the probe uses. */\nexport type ProbeRenderer = {\n render(scene: Object3D, camera: Camera): void;\n getContext(): unknown;\n getRenderTarget(): unknown;\n};\n\nexport type BlackFrameOptions = {\n /** Samples per side: 3 reads a 3 × 3 grid at the quarter lines. */\n grid?: number;\n /** Black samples that make a frame black. */\n threshold?: number;\n};\n\nexport type BlackFrameWatch = {\n /** Frames sampled since the start or the last `reset()`. */\n readonly frames: number;\n /** Which of those frames (1-based) were black. */\n readonly blackFrames: readonly number[];\n reset: () => void;\n /** Restore the renderer and stop sampling. */\n stop: () => void;\n};\n\ntype ReadPixels = Pick<\n WebGLRenderingContext,\n \"RGBA\" | \"UNSIGNED_BYTE\" | \"drawingBufferHeight\" | \"drawingBufferWidth\" | \"readPixels\"\n>;\n\n/**\n * Sample every frame the app draws to the screen. Several screen renders in one frame (an overlay\n * redrawn after the composer) count once, as the last of them: the image the display shows.\n * Browser only: frames are told apart by `requestAnimationFrame`.\n */\nexport const watchBlackFrames = (\n renderer: ProbeRenderer,\n options: BlackFrameOptions = {},\n): BlackFrameWatch => {\n const context = renderer.getContext() as Partial<ReadPixels> | null;\n\n if (typeof context?.readPixels !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs a WebGLRenderer\");\n }\n\n if (typeof requestAnimationFrame !== \"function\") {\n throw new TypeError(\"three-audit: the black-frame probe needs requestAnimationFrame\");\n }\n\n const gl = context as ReadPixels;\n const grid = options.grid ?? 3;\n const threshold = options.threshold ?? 3;\n const pixel = new Uint8Array(4);\n const render = renderer.render;\n let frames = 0;\n let black: number[] = [];\n let pending: boolean | null = null;\n\n const isBlack = () => {\n let count = 0;\n\n for (let i = 1; i <= grid; i += 1) {\n for (let j = 1; j <= grid; j += 1) {\n const x = Math.floor((gl.drawingBufferWidth * i) / (grid + 1));\n const y = Math.floor((gl.drawingBufferHeight * j) / (grid + 1));\n\n gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);\n\n if (pixel[0]! < 4 && pixel[1]! < 4 && pixel[2]! < 4 && pixel[3]! > 250) {\n count += 1;\n }\n }\n }\n\n return count >= threshold;\n };\n\n // Commit the frame's last screen sample once per animation frame.\n let raf = 0;\n const commit = () => {\n if (pending !== null) {\n frames += 1;\n\n if (pending) {\n black.push(frames);\n }\n\n pending = null;\n }\n\n raf = requestAnimationFrame(commit);\n };\n\n raf = requestAnimationFrame(commit);\n\n const restore = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n render.call(this, scene, camera);\n\n // Read before the browser presents (and, without preserveDrawingBuffer, clears) the buffer.\n if (renderer.getRenderTarget() === null) {\n pending = isBlack();\n }\n });\n\n return {\n get frames() {\n return frames;\n },\n get blackFrames() {\n return black;\n },\n reset: () => {\n frames = 0;\n black = [];\n pending = null;\n },\n stop: () => {\n cancelAnimationFrame(raf);\n restore();\n },\n };\n};\n","/**\n * A ledger of every draw call over one frame: which object drew, in which pass. Exact counts from\n * the renderer itself, where `renderer.info` gives one total (and resets it every `render()`).\n *\n * WebGL only: it wraps `WebGLRenderer.renderBufferDirect`, which every draw goes through, and\n * `render()`, to tell passes apart. Draws with a depth or distance material are the shadow maps.\n */\nimport type { BufferGeometry, Camera, Material, Object3D, Scene } from \"three\";\n\nimport { patch } from \"./patch.ts\";\n\n/** The two `WebGLRenderer` methods the ledger wraps. */\nexport type LedgerRenderer = {\n render(scene: Object3D, camera: Camera): void;\n renderBufferDirect(\n camera: Camera,\n scene: Scene | null,\n geometry: BufferGeometry,\n material: Material,\n object: Object3D,\n group: unknown,\n ): void;\n};\n\nexport type DrawLedgerOptions = {\n /**\n * The group an object name files under. Default: up to the first `:` or `#`, so a bake's\n * `wagon:paint` and a strip's `grass#3` group as `wagon` and `grass`.\n */\n groupOf?: (name: string) => string;\n};\n\nexport type LedgerRow = {\n name: string;\n total: number;\n /** Draws per pass name. */\n passes: Record<string, number>;\n};\n\nexport type DrawLedger = {\n /**\n * Pass names in the order they first drew: `shadow`, then each `render()` call as its scene's\n * name, or `render 1`, `render 2`… when unnamed (a post-processing chain is several).\n */\n passes: string[];\n /** By group, most draws first. */\n groups: LedgerRow[];\n /** By object name, most draws first. */\n objects: LedgerRow[];\n draws: number;\n};\n\nexport type DrawLedgerRecording = {\n /** Restore the renderer and return what was drawn since `beginDrawLedger`. */\n end: () => DrawLedger;\n};\n\nconst defaultGroupOf = (name: string) => name.split(/[:#]/)[0] || name;\n\nconst nameOf = (object: Object3D, material: Material) =>\n object.name || `${object.type}:${material.name || material.type}`;\n\nconst tally = (rows: Map<string, LedgerRow>, name: string, pass: string) => {\n const row = rows.get(name) ?? { name, total: 0, passes: {} };\n\n row.total += 1;\n row.passes[pass] = (row.passes[pass] ?? 0) + 1;\n rows.set(name, row);\n};\n\nconst sorted = (rows: Map<string, LedgerRow>) =>\n [...rows.values()].sort((a, b) => b.total - a.total);\n\n/** Start counting every draw. Call `end()` after the frame to restore the renderer and read it. */\nexport const beginDrawLedger = (\n renderer: LedgerRenderer,\n options: DrawLedgerOptions = {},\n): DrawLedgerRecording => {\n if (typeof renderer.renderBufferDirect !== \"function\") {\n throw new TypeError(\"three-audit: the draw ledger needs a WebGLRenderer\");\n }\n\n const groupOf = options.groupOf ?? defaultGroupOf;\n const render = renderer.render;\n const renderBufferDirect = renderer.renderBufferDirect;\n const passes: string[] = [];\n const objects = new Map<string, LedgerRow>();\n const groups = new Map<string, LedgerRow>();\n let renders = 0;\n let current = \"direct\";\n let draws = 0;\n\n const restoreRender = patch(renderer, \"render\", function (this: unknown, scene, camera) {\n const outer = current;\n\n renders += 1;\n current = scene.name || `render ${renders}`;\n\n try {\n render.call(this, scene, camera);\n } finally {\n current = outer;\n }\n });\n\n const restoreDraw = patch(\n renderer,\n \"renderBufferDirect\",\n function (this: unknown, camera, scene, geometry, material, object, group) {\n const shadow =\n (material as { isMeshDepthMaterial?: boolean }).isMeshDepthMaterial === true ||\n (material as { isMeshDistanceMaterial?: boolean }).isMeshDistanceMaterial === true;\n const pass = shadow ? \"shadow\" : current;\n const name = nameOf(object, material);\n\n if (!passes.includes(pass)) {\n passes.push(pass);\n }\n\n draws += 1;\n tally(objects, name, pass);\n tally(groups, groupOf(name), pass);\n renderBufferDirect.call(this, camera, scene, geometry, material, object, group);\n },\n );\n\n return {\n end: () => {\n restoreDraw();\n restoreRender();\n\n return { passes, groups: sorted(groups), objects: sorted(objects), draws };\n },\n };\n};\n\n/**\n * Record exactly one frame of an app that renders from `requestAnimationFrame` (three's\n * `setAnimationLoop`, React Three Fiber): from one animation frame to the next. A demand-driven\n * loop that renders nothing that frame gives an empty ledger. Browser only.\n */\nexport const recordDrawLedger = (renderer: LedgerRenderer, options: DrawLedgerOptions = {}) =>\n new Promise<DrawLedger>((resolve, reject) => {\n if (typeof requestAnimationFrame !== \"function\") {\n reject(new Error(\"three-audit: recordDrawLedger needs requestAnimationFrame\"));\n\n return;\n }\n\n requestAnimationFrame(() => {\n try {\n const recording = beginDrawLedger(renderer, options);\n\n requestAnimationFrame(() => resolve(recording.end()));\n } catch (error) {\n reject(error);\n }\n });\n });\n\n/** Rows shaped for `console.table`: one column per pass, then the total. */\nexport const ledgerTable = (rows: LedgerRow[], passes: string[]) =>\n rows.map((row) => ({\n name: row.name,\n ...Object.fromEntries(passes.map((pass) => [pass, row.passes[pass] ?? 0])),\n total: row.total,\n }));\n\n/** The ledger as two console tables: by group, and every object collapsed beneath. */\nexport const printDrawLedger = (ledger: DrawLedger) => {\n /* oxlint-disable no-console -- printing is this function's job */\n console.group(`Draw ledger: ${ledger.draws} draws in one frame`);\n console.table(ledgerTable(ledger.groups, ledger.passes));\n console.groupCollapsed(\"Every object\");\n console.table(ledgerTable(ledger.objects, ledger.passes));\n console.groupEnd();\n console.groupEnd();\n /* oxlint-enable no-console */\n};\n","/**\n * The checks as short console globals, for a dev page or a headless browser's `eval`:\n * `tris()`, `meshes()`, `census()`, `audit()`, `zfight()`, `report()`, and with a renderer\n * `ledger()` and `blackFrames()`.\n */\nimport type { Object3D } from \"three\";\n\nimport { auditScene } from \"./audit-scene.ts\";\nimport { findBadGeometry } from \"./bad-geometry.ts\";\nimport { type ProbeRenderer, watchBlackFrames } from \"./black-frames.ts\";\nimport { type LedgerRenderer, printDrawLedger, recordDrawLedger } from \"./draw-ledger.ts\";\nimport type { Skip } from \"./scene.ts\";\nimport { countTriangles, geometryCensus, listMeshes } from \"./triangles.ts\";\nimport { findZFighting } from \"./z-fighting.ts\";\n\nexport type AuditHelpersOptions = {\n /** A `WebGLRenderer`, for `ledger()` and `blackFrames()`. */\n renderer?: (LedgerRenderer & ProbeRenderer) | undefined;\n skip?: Skip | undefined;\n tagKey?: string;\n};\n\nconst NAMES = [\n \"auditRoot\",\n \"tris\",\n \"meshes\",\n \"census\",\n \"audit\",\n \"zfight\",\n \"report\",\n \"ledger\",\n \"blackFrames\",\n] as const;\n\nconst needsRenderer = (name: string) => () => {\n throw new Error(`three-audit: ${name}() needs installAuditHelpers(scene, { renderer })`);\n};\n\n/**\n * Put the helpers on `globalThis`. Returns a function that removes them.\n *\n * vanilla: installAuditHelpers(scene, { renderer })\n * R3F: const { scene, gl } = useThree();\n * useLayoutEffect(() => installAuditHelpers(scene, { renderer: gl }), [scene, gl]);\n */\nexport const installAuditHelpers = (root: Object3D, options: AuditHelpersOptions = {}) => {\n const { renderer, skip } = options;\n const tagKey = options.tagKey ?? \"studioObject\";\n\n const helpers: Record<(typeof NAMES)[number], unknown> = {\n auditRoot: root,\n tris: () => countTriangles(root, { skip }),\n meshes: () => listMeshes(root, { skip }),\n census: () => geometryCensus(root, { skip }),\n audit: () => findBadGeometry(root, { skip, tagKey }),\n zfight: (gap?: number, self?: boolean) =>\n findZFighting(root, {\n skip,\n tagKey,\n ...(gap === undefined ? {} : { gap }),\n ...(self === undefined ? {} : { self }),\n }),\n report: () => auditScene(root, { skip, tagKey }),\n ledger: renderer\n ? async () => {\n const ledger = await recordDrawLedger(renderer);\n\n printDrawLedger(ledger);\n\n return ledger;\n }\n : needsRenderer(\"ledger\"),\n blackFrames: renderer\n ? (seconds = 10) =>\n new Promise<{ frames: number; blackFrames: readonly number[] }>((resolve) => {\n const watch = watchBlackFrames(renderer);\n\n setTimeout(() => {\n watch.stop();\n resolve({ frames: watch.frames, blackFrames: watch.blackFrames });\n }, seconds * 1000);\n })\n : needsRenderer(\"blackFrames\"),\n };\n\n Object.assign(globalThis, helpers);\n\n return () => {\n const scope = globalThis as Record<string, unknown>;\n\n for (const name of NAMES) {\n if (scope[name] === helpers[name]) {\n delete scope[name];\n }\n }\n };\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
4
|
+
import { BufferAttribute, BufferGeometry, InterleavedBuffer, InterleavedBufferAttribute } from "three";
|
|
5
|
+
//#region src/node/draco.ts
|
|
6
|
+
/**
|
|
7
|
+
* Draco for `GLTFLoader` in Node. three's `DRACOLoader` decodes in Web Workers, which Node lacks;
|
|
8
|
+
* this is its decode run in-process on `draco3dgltf` (Google's Node build of the decoder),
|
|
9
|
+
* shaped like the two methods `GLTFLoader` calls on it.
|
|
10
|
+
*/
|
|
11
|
+
const DATA_TYPES = {
|
|
12
|
+
Float32Array: "DT_FLOAT32",
|
|
13
|
+
Int8Array: "DT_INT8",
|
|
14
|
+
Int16Array: "DT_INT16",
|
|
15
|
+
Int32Array: "DT_INT32",
|
|
16
|
+
Uint8Array: "DT_UINT8",
|
|
17
|
+
Uint16Array: "DT_UINT16",
|
|
18
|
+
Uint32Array: "DT_UINT32"
|
|
19
|
+
};
|
|
20
|
+
const decodeAttribute = (draco, decoder, mesh, attribute, ArrayType) => {
|
|
21
|
+
const count = mesh.num_points();
|
|
22
|
+
const itemSize = attribute.num_components();
|
|
23
|
+
const srcBytes = itemSize * ArrayType.BYTES_PER_ELEMENT;
|
|
24
|
+
const stride = Math.ceil(srcBytes / 4) * 4 / ArrayType.BYTES_PER_ELEMENT;
|
|
25
|
+
const byteLength = count * srcBytes;
|
|
26
|
+
const pointer = draco._malloc(byteLength);
|
|
27
|
+
decoder.GetAttributeDataArrayForAllPoints(mesh, attribute, draco[DATA_TYPES[ArrayType.name]], byteLength, pointer);
|
|
28
|
+
const source = new ArrayType(draco.HEAPF32.buffer, pointer, byteLength / ArrayType.BYTES_PER_ELEMENT);
|
|
29
|
+
let array;
|
|
30
|
+
if (stride === itemSize) array = source.slice();
|
|
31
|
+
else {
|
|
32
|
+
array = new ArrayType(count * stride);
|
|
33
|
+
for (let i = 0; i < count; i += 1) for (let k = 0; k < itemSize; k += 1) array[i * stride + k] = source[i * itemSize + k];
|
|
34
|
+
}
|
|
35
|
+
draco._free(pointer);
|
|
36
|
+
return stride === itemSize ? new BufferAttribute(array, itemSize) : new InterleavedBufferAttribute(new InterleavedBuffer(array, stride), itemSize, 0);
|
|
37
|
+
};
|
|
38
|
+
const decode = (draco, buffer, attributeIds, attributeTypes) => {
|
|
39
|
+
const decoder = new draco.Decoder();
|
|
40
|
+
const array = new Int8Array(buffer);
|
|
41
|
+
if (decoder.GetEncodedGeometryType(array) !== draco.TRIANGULAR_MESH) {
|
|
42
|
+
draco.destroy(decoder);
|
|
43
|
+
throw new Error("three-audit: Draco point clouds aren't supported");
|
|
44
|
+
}
|
|
45
|
+
const mesh = new draco.Mesh();
|
|
46
|
+
const status = decoder.DecodeArrayToMesh(array, array.byteLength, mesh);
|
|
47
|
+
if (!status.ok() || mesh.ptr === 0) {
|
|
48
|
+
draco.destroy(mesh);
|
|
49
|
+
draco.destroy(decoder);
|
|
50
|
+
throw new Error(`three-audit: Draco decoding failed: ${status.error_msg()}`);
|
|
51
|
+
}
|
|
52
|
+
const geometry = new BufferGeometry();
|
|
53
|
+
for (const [name, id] of Object.entries(attributeIds)) {
|
|
54
|
+
const ArrayType = globalThis[attributeTypes[name] ?? "Float32Array"];
|
|
55
|
+
geometry.setAttribute(name, decodeAttribute(draco, decoder, mesh, decoder.GetAttributeByUniqueId(mesh, id), ArrayType));
|
|
56
|
+
}
|
|
57
|
+
const indices = mesh.num_faces() * 3;
|
|
58
|
+
const pointer = draco._malloc(indices * 4);
|
|
59
|
+
decoder.GetTrianglesUInt32Array(mesh, indices * 4, pointer);
|
|
60
|
+
geometry.setIndex(new BufferAttribute(new Uint32Array(draco.HEAPF32.buffer, pointer, indices).slice(), 1));
|
|
61
|
+
draco._free(pointer);
|
|
62
|
+
draco.destroy(mesh);
|
|
63
|
+
draco.destroy(decoder);
|
|
64
|
+
return geometry;
|
|
65
|
+
};
|
|
66
|
+
/** Stands in for `DRACOLoader` in `gltfLoader.setDRACOLoader(...)`. Needs `draco3dgltf`. */
|
|
67
|
+
const createNodeDracoLoader = async () => {
|
|
68
|
+
let module;
|
|
69
|
+
try {
|
|
70
|
+
module = (await import("draco3dgltf")).default;
|
|
71
|
+
} catch {
|
|
72
|
+
throw new Error("three-audit: this model is Draco-compressed; install draco3dgltf to read it (npm i -D draco3dgltf)");
|
|
73
|
+
}
|
|
74
|
+
const draco = await module.createDecoderModule({});
|
|
75
|
+
return {
|
|
76
|
+
preload() {
|
|
77
|
+
return this;
|
|
78
|
+
},
|
|
79
|
+
decodeDracoFile(buffer, callback, attributeIds, attributeTypes, _colorSpace, onError = () => {}) {
|
|
80
|
+
try {
|
|
81
|
+
callback(decode(draco, buffer, attributeIds, attributeTypes));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
onError(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/node/load.ts
|
|
90
|
+
/**
|
|
91
|
+
* Load a glTF model in Node for the checks: `.glb`, or `.gltf` with its buffers beside it.
|
|
92
|
+
*
|
|
93
|
+
* Textures are stripped before parsing: no check reads them, and decoding images needs a DOM.
|
|
94
|
+
* Draco (`draco3dgltf`) and Meshopt (`meshoptimizer`) compressed geometry decode when those
|
|
95
|
+
* packages are installed.
|
|
96
|
+
*/
|
|
97
|
+
const GLB_MAGIC = 1179937895;
|
|
98
|
+
const JSON_CHUNK = 1313821514;
|
|
99
|
+
const BIN_CHUNK = 5130562;
|
|
100
|
+
const TEXTURE_EXTENSIONS = [
|
|
101
|
+
"KHR_texture_basisu",
|
|
102
|
+
"KHR_texture_transform",
|
|
103
|
+
"EXT_texture_webp",
|
|
104
|
+
"EXT_texture_avif"
|
|
105
|
+
];
|
|
106
|
+
/** Every `…Texture` reference in a material, extensions included. */
|
|
107
|
+
const dropTextureRefs = (value) => {
|
|
108
|
+
if (value === null || typeof value !== "object") return;
|
|
109
|
+
for (const [key, child] of Object.entries(value)) if (key.endsWith("Texture") && child !== null && typeof child === "object" && "index" in child) delete value[key];
|
|
110
|
+
else dropTextureRefs(child);
|
|
111
|
+
};
|
|
112
|
+
const stripTextures = (json) => {
|
|
113
|
+
delete json.images;
|
|
114
|
+
delete json.textures;
|
|
115
|
+
delete json.samplers;
|
|
116
|
+
dropTextureRefs(json.materials);
|
|
117
|
+
for (const key of ["extensionsUsed", "extensionsRequired"]) if (Array.isArray(json[key])) json[key] = json[key].filter((name) => !TEXTURE_EXTENSIONS.includes(name));
|
|
118
|
+
};
|
|
119
|
+
const uses = (json, extension) => Array.isArray(json.extensionsUsed) && json.extensionsUsed.includes(extension);
|
|
120
|
+
/** Split a GLB into its JSON and binary chunks. */
|
|
121
|
+
const readGlb = (bytes) => {
|
|
122
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
123
|
+
let json = null;
|
|
124
|
+
let bin = null;
|
|
125
|
+
for (let offset = 12; offset < bytes.byteLength;) {
|
|
126
|
+
const length = view.getUint32(offset, true);
|
|
127
|
+
const type = view.getUint32(offset + 4, true);
|
|
128
|
+
const chunk = bytes.subarray(offset + 8, offset + 8 + length);
|
|
129
|
+
if (type === JSON_CHUNK) json = JSON.parse(new TextDecoder().decode(chunk));
|
|
130
|
+
else if (type === BIN_CHUNK) bin = chunk;
|
|
131
|
+
offset += 8 + length;
|
|
132
|
+
}
|
|
133
|
+
if (!json) throw new Error("three-audit: GLB has no JSON chunk");
|
|
134
|
+
return {
|
|
135
|
+
json,
|
|
136
|
+
bin
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
/** A GLB from JSON and an optional binary chunk, each padded to four bytes. */
|
|
140
|
+
const writeGlb = (json, bin) => {
|
|
141
|
+
const text = new TextEncoder().encode(JSON.stringify(json));
|
|
142
|
+
const jsonLength = Math.ceil(text.byteLength / 4) * 4;
|
|
143
|
+
const binLength = bin ? Math.ceil(bin.byteLength / 4) * 4 : 0;
|
|
144
|
+
const total = 20 + jsonLength + (bin ? 8 + binLength : 0);
|
|
145
|
+
const out = new Uint8Array(total);
|
|
146
|
+
const view = new DataView(out.buffer);
|
|
147
|
+
view.setUint32(0, GLB_MAGIC, true);
|
|
148
|
+
view.setUint32(4, 2, true);
|
|
149
|
+
view.setUint32(8, total, true);
|
|
150
|
+
view.setUint32(12, jsonLength, true);
|
|
151
|
+
view.setUint32(16, JSON_CHUNK, true);
|
|
152
|
+
out.fill(32, 20, 20 + jsonLength);
|
|
153
|
+
out.set(text, 20);
|
|
154
|
+
if (bin) {
|
|
155
|
+
view.setUint32(20 + jsonLength, binLength, true);
|
|
156
|
+
view.setUint32(24 + jsonLength, BIN_CHUNK, true);
|
|
157
|
+
out.set(bin, 28 + jsonLength);
|
|
158
|
+
}
|
|
159
|
+
return out.buffer;
|
|
160
|
+
};
|
|
161
|
+
/** A buffer's bytes: the GLB's binary chunk, a data URI, or a file beside the model. */
|
|
162
|
+
const bufferBytes = async (buffer, index, bin, directory) => {
|
|
163
|
+
if (buffer.uri === void 0) {
|
|
164
|
+
if (index !== 0 || !bin) throw new Error("three-audit: a buffer without a uri must be the GLB's binary chunk");
|
|
165
|
+
return bin;
|
|
166
|
+
}
|
|
167
|
+
if (buffer.uri.startsWith("data:")) return new Uint8Array(Buffer.from(buffer.uri.slice(buffer.uri.indexOf(",") + 1), "base64"));
|
|
168
|
+
return new Uint8Array(await readFile(resolve(directory, decodeURIComponent(buffer.uri))));
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Every buffer folded into one binary chunk, the buffer views re-pointed into it. The result is a
|
|
172
|
+
* self-contained GLB, so the loader never fetches: three's `FileLoader` needs browser globals.
|
|
173
|
+
*/
|
|
174
|
+
const packBuffers = async (json, bin, directory) => {
|
|
175
|
+
const buffers = json.buffers ?? [];
|
|
176
|
+
const parts = await Promise.all(buffers.map((buffer, i) => bufferBytes(buffer, i, bin, directory)));
|
|
177
|
+
const offsets = [];
|
|
178
|
+
let total = 0;
|
|
179
|
+
for (const part of parts) {
|
|
180
|
+
offsets.push(total);
|
|
181
|
+
total += Math.ceil(part.byteLength / 4) * 4;
|
|
182
|
+
}
|
|
183
|
+
const packed = new Uint8Array(total);
|
|
184
|
+
parts.forEach((part, i) => packed.set(part, offsets[i]));
|
|
185
|
+
for (const view of json.bufferViews ?? []) {
|
|
186
|
+
view.byteOffset = (view.byteOffset ?? 0) + offsets[view.buffer];
|
|
187
|
+
view.buffer = 0;
|
|
188
|
+
}
|
|
189
|
+
json.buffers = parts.length > 0 ? [{ byteLength: total }] : [];
|
|
190
|
+
return parts.length > 0 ? packed : null;
|
|
191
|
+
};
|
|
192
|
+
/** Parse glTF bytes (GLB or JSON) into a three scene, textures stripped. */
|
|
193
|
+
const parseModel = async (bytes, directory = ".") => {
|
|
194
|
+
const { json, bin } = bytes.byteLength >= 12 && new DataView(bytes.buffer, bytes.byteOffset).getUint32(0, true) === GLB_MAGIC ? readGlb(bytes) : {
|
|
195
|
+
json: JSON.parse(new TextDecoder().decode(bytes)),
|
|
196
|
+
bin: null
|
|
197
|
+
};
|
|
198
|
+
stripTextures(json);
|
|
199
|
+
const data = writeGlb(json, await packBuffers(json, bin, directory));
|
|
200
|
+
const loader = new GLTFLoader();
|
|
201
|
+
if (uses(json, "KHR_draco_mesh_compression")) loader.setDRACOLoader(await createNodeDracoLoader());
|
|
202
|
+
if (uses(json, "EXT_meshopt_compression") || uses(json, "KHR_meshopt_compression")) try {
|
|
203
|
+
const { MeshoptDecoder } = await import("meshoptimizer");
|
|
204
|
+
await MeshoptDecoder.ready;
|
|
205
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
206
|
+
} catch {
|
|
207
|
+
throw new Error("three-audit: this model is Meshopt-compressed; install meshoptimizer to read it (npm i -D meshoptimizer)");
|
|
208
|
+
}
|
|
209
|
+
return (await loader.parseAsync(data, "")).scene;
|
|
210
|
+
};
|
|
211
|
+
/** Read and parse a `.glb` or `.gltf` file into a three scene, ready for the checks. */
|
|
212
|
+
const loadModel = async (path) => parseModel(await readFile(path), dirname(path));
|
|
213
|
+
//#endregion
|
|
214
|
+
export { parseModel as n, createNodeDracoLoader as r, loadModel as t };
|
|
215
|
+
|
|
216
|
+
//# sourceMappingURL=load-nqfCU3-k.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-nqfCU3-k.js","names":[],"sources":["../src/node/draco.ts","../src/node/load.ts"],"sourcesContent":["/**\n * Draco for `GLTFLoader` in Node. three's `DRACOLoader` decodes in Web Workers, which Node lacks;\n * this is its decode run in-process on `draco3dgltf` (Google's Node build of the decoder),\n * shaped like the two methods `GLTFLoader` calls on it.\n */\nimport {\n BufferAttribute,\n BufferGeometry,\n InterleavedBuffer,\n InterleavedBufferAttribute,\n} from \"three\";\n\ntype TypedArray =\n | Float32Array\n | Int8Array\n | Int16Array\n | Int32Array\n | Uint8Array\n | Uint16Array\n | Uint32Array;\ntype TypedArrayConstructor = {\n new (length: number): TypedArray;\n new (buffer: ArrayBufferLike, offset: number, length: number): TypedArray;\n BYTES_PER_ELEMENT: number;\n};\n\n/* The slice of the emscripten module this uses. */\ntype DracoAttribute = { num_components(): number };\ntype DracoMesh = { num_faces(): number; num_points(): number; ptr: number };\ntype DracoDecoder = {\n GetEncodedGeometryType(array: Int8Array): number;\n DecodeArrayToMesh(\n array: Int8Array,\n length: number,\n mesh: DracoMesh,\n ): { ok(): boolean; error_msg(): string };\n GetAttributeByUniqueId(mesh: DracoMesh, id: number): DracoAttribute;\n GetAttributeDataArrayForAllPoints(\n mesh: DracoMesh,\n attribute: DracoAttribute,\n type: number,\n byteLength: number,\n pointer: number,\n ): boolean;\n GetTrianglesUInt32Array(mesh: DracoMesh, byteLength: number, pointer: number): boolean;\n};\ntype DracoModule = {\n Decoder: new () => DracoDecoder;\n Mesh: new () => DracoMesh;\n TRIANGULAR_MESH: number;\n HEAPF32: Float32Array;\n _malloc(bytes: number): number;\n _free(pointer: number): void;\n destroy(object: unknown): void;\n [type: `DT_${string}`]: number;\n};\n\nconst DATA_TYPES: Record<string, string> = {\n Float32Array: \"DT_FLOAT32\",\n Int8Array: \"DT_INT8\",\n Int16Array: \"DT_INT16\",\n Int32Array: \"DT_INT32\",\n Uint8Array: \"DT_UINT8\",\n Uint16Array: \"DT_UINT16\",\n Uint32Array: \"DT_UINT32\",\n};\n\nconst decodeAttribute = (\n draco: DracoModule,\n decoder: DracoDecoder,\n mesh: DracoMesh,\n attribute: DracoAttribute,\n ArrayType: TypedArrayConstructor,\n) => {\n const count = mesh.num_points();\n const itemSize = attribute.num_components();\n // glTF aligns every vertex to 4 bytes; a narrower one is read interleaved.\n const srcBytes = itemSize * ArrayType.BYTES_PER_ELEMENT;\n const stride = (Math.ceil(srcBytes / 4) * 4) / ArrayType.BYTES_PER_ELEMENT;\n const byteLength = count * srcBytes;\n const pointer = draco._malloc(byteLength);\n\n decoder.GetAttributeDataArrayForAllPoints(\n mesh,\n attribute,\n draco[DATA_TYPES[ArrayType.name] as `DT_${string}`]!,\n byteLength,\n pointer,\n );\n\n const source = new ArrayType(\n draco.HEAPF32.buffer,\n pointer,\n byteLength / ArrayType.BYTES_PER_ELEMENT,\n );\n let array: TypedArray;\n\n if (stride === itemSize) {\n array = source.slice();\n } else {\n array = new ArrayType(count * stride);\n\n for (let i = 0; i < count; i += 1) {\n for (let k = 0; k < itemSize; k += 1) {\n array[i * stride + k] = source[i * itemSize + k]!;\n }\n }\n }\n\n draco._free(pointer);\n\n return stride === itemSize\n ? new BufferAttribute(array, itemSize)\n : new InterleavedBufferAttribute(new InterleavedBuffer(array, stride), itemSize, 0);\n};\n\nconst decode = (\n draco: DracoModule,\n buffer: ArrayBuffer,\n attributeIds: Record<string, number>,\n attributeTypes: Record<string, string>,\n) => {\n const decoder = new draco.Decoder();\n const array = new Int8Array(buffer);\n\n if (decoder.GetEncodedGeometryType(array) !== draco.TRIANGULAR_MESH) {\n draco.destroy(decoder);\n throw new Error(\"three-audit: Draco point clouds aren't supported\");\n }\n\n const mesh = new draco.Mesh();\n const status = decoder.DecodeArrayToMesh(array, array.byteLength, mesh);\n\n if (!status.ok() || mesh.ptr === 0) {\n draco.destroy(mesh);\n draco.destroy(decoder);\n throw new Error(`three-audit: Draco decoding failed: ${status.error_msg()}`);\n }\n\n const geometry = new BufferGeometry();\n\n for (const [name, id] of Object.entries(attributeIds)) {\n const ArrayType = (globalThis as unknown as Record<string, TypedArrayConstructor>)[\n attributeTypes[name] ?? \"Float32Array\"\n ]!;\n\n geometry.setAttribute(\n name,\n decodeAttribute(draco, decoder, mesh, decoder.GetAttributeByUniqueId(mesh, id), ArrayType),\n );\n }\n\n const indices = mesh.num_faces() * 3;\n const pointer = draco._malloc(indices * 4);\n\n decoder.GetTrianglesUInt32Array(mesh, indices * 4, pointer);\n geometry.setIndex(\n new BufferAttribute(new Uint32Array(draco.HEAPF32.buffer, pointer, indices).slice(), 1),\n );\n draco._free(pointer);\n draco.destroy(mesh);\n draco.destroy(decoder);\n\n return geometry;\n};\n\n/** Stands in for `DRACOLoader` in `gltfLoader.setDRACOLoader(...)`. Needs `draco3dgltf`. */\nexport const createNodeDracoLoader = async () => {\n let module: { createDecoderModule(options: object): Promise<DracoModule> };\n\n try {\n module = ((await import(\"draco3dgltf\")) as { default: typeof module }).default;\n } catch {\n throw new Error(\n \"three-audit: this model is Draco-compressed; install draco3dgltf to read it (npm i -D draco3dgltf)\",\n );\n }\n\n const draco = await module.createDecoderModule({});\n\n return {\n preload() {\n return this;\n },\n decodeDracoFile(\n buffer: ArrayBuffer,\n callback: (geometry: BufferGeometry) => void,\n attributeIds: Record<string, number>,\n attributeTypes: Record<string, string>,\n _colorSpace?: string,\n onError: (error: unknown) => void = () => {},\n ) {\n try {\n callback(decode(draco, buffer, attributeIds, attributeTypes));\n } catch (error) {\n onError(error);\n }\n },\n };\n};\n","/**\n * Load a glTF model in Node for the checks: `.glb`, or `.gltf` with its buffers beside it.\n *\n * Textures are stripped before parsing: no check reads them, and decoding images needs a DOM.\n * Draco (`draco3dgltf`) and Meshopt (`meshoptimizer`) compressed geometry decode when those\n * packages are installed.\n */\nimport { readFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport type { Group } from \"three\";\nimport { GLTFLoader } from \"three/addons/loaders/GLTFLoader.js\";\n\nimport { createNodeDracoLoader } from \"./draco.ts\";\n\ntype Json = Record<string, unknown>;\n\nconst GLB_MAGIC = 0x46546c67;\nconst JSON_CHUNK = 0x4e4f534a;\nconst BIN_CHUNK = 0x004e4942;\nconst TEXTURE_EXTENSIONS = [\n \"KHR_texture_basisu\",\n \"KHR_texture_transform\",\n \"EXT_texture_webp\",\n \"EXT_texture_avif\",\n];\n\n/** Every `…Texture` reference in a material, extensions included. */\nconst dropTextureRefs = (value: unknown) => {\n if (value === null || typeof value !== \"object\") {\n return;\n }\n\n for (const [key, child] of Object.entries(value)) {\n if (\n key.endsWith(\"Texture\") &&\n child !== null &&\n typeof child === \"object\" &&\n \"index\" in child\n ) {\n delete (value as Json)[key];\n } else {\n dropTextureRefs(child);\n }\n }\n};\n\nconst stripTextures = (json: Json) => {\n delete json.images;\n delete json.textures;\n delete json.samplers;\n dropTextureRefs(json.materials);\n\n for (const key of [\"extensionsUsed\", \"extensionsRequired\"]) {\n if (Array.isArray(json[key])) {\n json[key] = (json[key] as string[]).filter((name) => !TEXTURE_EXTENSIONS.includes(name));\n }\n }\n};\n\nconst uses = (json: Json, extension: string) =>\n Array.isArray(json.extensionsUsed) && (json.extensionsUsed as string[]).includes(extension);\n\n/** Split a GLB into its JSON and binary chunks. */\nconst readGlb = (bytes: Uint8Array) => {\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n let json: Json | null = null;\n let bin: Uint8Array | null = null;\n\n for (let offset = 12; offset < bytes.byteLength;) {\n const length = view.getUint32(offset, true);\n const type = view.getUint32(offset + 4, true);\n const chunk = bytes.subarray(offset + 8, offset + 8 + length);\n\n if (type === JSON_CHUNK) {\n json = JSON.parse(new TextDecoder().decode(chunk)) as Json;\n } else if (type === BIN_CHUNK) {\n bin = chunk;\n }\n\n offset += 8 + length;\n }\n\n if (!json) {\n throw new Error(\"three-audit: GLB has no JSON chunk\");\n }\n\n return { json, bin };\n};\n\n/** A GLB from JSON and an optional binary chunk, each padded to four bytes. */\nconst writeGlb = (json: Json, bin: Uint8Array | null) => {\n const text = new TextEncoder().encode(JSON.stringify(json));\n const jsonLength = Math.ceil(text.byteLength / 4) * 4;\n const binLength = bin ? Math.ceil(bin.byteLength / 4) * 4 : 0;\n const total = 12 + 8 + jsonLength + (bin ? 8 + binLength : 0);\n const out = new Uint8Array(total);\n const view = new DataView(out.buffer);\n\n view.setUint32(0, GLB_MAGIC, true);\n view.setUint32(4, 2, true);\n view.setUint32(8, total, true);\n view.setUint32(12, jsonLength, true);\n view.setUint32(16, JSON_CHUNK, true);\n out.fill(0x20, 20, 20 + jsonLength);\n out.set(text, 20);\n\n if (bin) {\n view.setUint32(20 + jsonLength, binLength, true);\n view.setUint32(24 + jsonLength, BIN_CHUNK, true);\n out.set(bin, 28 + jsonLength);\n }\n\n return out.buffer;\n};\n\n/** A buffer's bytes: the GLB's binary chunk, a data URI, or a file beside the model. */\nconst bufferBytes = async (\n buffer: { uri?: string },\n index: number,\n bin: Uint8Array | null,\n directory: string,\n) => {\n if (buffer.uri === undefined) {\n if (index !== 0 || !bin) {\n throw new Error(\"three-audit: a buffer without a uri must be the GLB's binary chunk\");\n }\n\n return bin;\n }\n\n if (buffer.uri.startsWith(\"data:\")) {\n return new Uint8Array(Buffer.from(buffer.uri.slice(buffer.uri.indexOf(\",\") + 1), \"base64\"));\n }\n\n return new Uint8Array(await readFile(resolve(directory, decodeURIComponent(buffer.uri))));\n};\n\n/**\n * Every buffer folded into one binary chunk, the buffer views re-pointed into it. The result is a\n * self-contained GLB, so the loader never fetches: three's `FileLoader` needs browser globals.\n */\nconst packBuffers = async (json: Json, bin: Uint8Array | null, directory: string) => {\n const buffers = (json.buffers ?? []) as { uri?: string }[];\n const parts = await Promise.all(\n buffers.map((buffer, i) => bufferBytes(buffer, i, bin, directory)),\n );\n const offsets: number[] = [];\n let total = 0;\n\n for (const part of parts) {\n offsets.push(total);\n total += Math.ceil(part.byteLength / 4) * 4;\n }\n\n const packed = new Uint8Array(total);\n\n parts.forEach((part, i) => packed.set(part, offsets[i]));\n\n for (const view of (json.bufferViews ?? []) as { buffer: number; byteOffset?: number }[]) {\n view.byteOffset = (view.byteOffset ?? 0) + offsets[view.buffer]!;\n view.buffer = 0;\n }\n\n json.buffers = parts.length > 0 ? [{ byteLength: total }] : [];\n\n return parts.length > 0 ? packed : null;\n};\n\n/** Parse glTF bytes (GLB or JSON) into a three scene, textures stripped. */\nexport const parseModel = async (bytes: Uint8Array, directory = \".\"): Promise<Group> => {\n const isGlb =\n bytes.byteLength >= 12 &&\n new DataView(bytes.buffer, bytes.byteOffset).getUint32(0, true) === GLB_MAGIC;\n const { json, bin } = isGlb\n ? readGlb(bytes)\n : { json: JSON.parse(new TextDecoder().decode(bytes)) as Json, bin: null };\n\n stripTextures(json);\n\n const data = writeGlb(json, await packBuffers(json, bin, directory));\n const loader = new GLTFLoader();\n\n if (uses(json, \"KHR_draco_mesh_compression\")) {\n loader.setDRACOLoader((await createNodeDracoLoader()) as never);\n }\n\n if (uses(json, \"EXT_meshopt_compression\") || uses(json, \"KHR_meshopt_compression\")) {\n try {\n const { MeshoptDecoder } = await import(\"meshoptimizer\");\n\n await MeshoptDecoder.ready;\n loader.setMeshoptDecoder(MeshoptDecoder);\n } catch {\n throw new Error(\n \"three-audit: this model is Meshopt-compressed; install meshoptimizer to read it (npm i -D meshoptimizer)\",\n );\n }\n }\n\n const gltf = await loader.parseAsync(data, \"\");\n\n return gltf.scene;\n};\n\n/** Read and parse a `.glb` or `.gltf` file into a three scene, ready for the checks. */\nexport const loadModel = async (path: string): Promise<Group> =>\n parseModel(await readFile(path), dirname(path));\n"],"mappings":"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"}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BufferGeometry, Group } from "three";
|
|
2
|
+
//#region src/node/load.d.ts
|
|
3
|
+
/** Parse glTF bytes (GLB or JSON) into a three scene, textures stripped. */
|
|
4
|
+
export declare const parseModel: (bytes: Uint8Array, directory?: string) => Promise<Group>;
|
|
5
|
+
/** Read and parse a `.glb` or `.gltf` file into a three scene, ready for the checks. */
|
|
6
|
+
export declare const loadModel: (path: string) => Promise<Group>;
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/node/draco.d.ts
|
|
9
|
+
/** Stands in for `DRACOLoader` in `gltfLoader.setDRACOLoader(...)`. Needs `draco3dgltf`. */
|
|
10
|
+
export declare const createNodeDracoLoader: () => Promise<{
|
|
11
|
+
preload(): /*elided*/ any;
|
|
12
|
+
decodeDracoFile(buffer: ArrayBuffer, callback: (geometry: BufferGeometry) => void, attributeIds: Record<string, number>, attributeTypes: Record<string, string>, _colorSpace?: string, onError?: (error: unknown) => void): void;
|
|
13
|
+
}>;
|
|
14
|
+
//#endregion
|
|
15
|
+
//# sourceMappingURL=node.d.ts.map
|
package/dist/node.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zkmake/three-audit",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Scene checks for three.js: triangle counts, cost census, NaN normals, z-fighting, black frames and a one-frame draw-call ledger. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"audit",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"url": "git+https://github.com/zkmake/three-kit.git",
|
|
23
23
|
"directory": "packages/three-audit"
|
|
24
24
|
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"three-audit": "./dist/cli.js"
|
|
27
|
+
},
|
|
25
28
|
"files": [
|
|
26
29
|
"dist",
|
|
27
30
|
"README.md",
|
|
@@ -31,6 +34,7 @@
|
|
|
31
34
|
"sideEffects": false,
|
|
32
35
|
"exports": {
|
|
33
36
|
".": "./dist/index.js",
|
|
37
|
+
"./node": "./dist/node.js",
|
|
34
38
|
"./package.json": "./package.json"
|
|
35
39
|
},
|
|
36
40
|
"publishConfig": {
|
|
@@ -46,7 +50,10 @@
|
|
|
46
50
|
},
|
|
47
51
|
"devDependencies": {
|
|
48
52
|
"@arethetypeswrong/core": "catalog:",
|
|
53
|
+
"@types/node": "catalog:",
|
|
49
54
|
"@types/three": "catalog:",
|
|
55
|
+
"draco3dgltf": "catalog:",
|
|
56
|
+
"meshoptimizer": "catalog:",
|
|
50
57
|
"publint": "catalog:",
|
|
51
58
|
"three": "catalog:",
|
|
52
59
|
"tsdown": "catalog:",
|
|
@@ -54,8 +61,18 @@
|
|
|
54
61
|
"vitest": "catalog:"
|
|
55
62
|
},
|
|
56
63
|
"peerDependencies": {
|
|
64
|
+
"draco3dgltf": ">=1.5",
|
|
65
|
+
"meshoptimizer": ">=0.21",
|
|
57
66
|
"three": ">=0.160"
|
|
58
67
|
},
|
|
68
|
+
"peerDependenciesMeta": {
|
|
69
|
+
"draco3dgltf": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"meshoptimizer": {
|
|
73
|
+
"optional": true
|
|
74
|
+
}
|
|
75
|
+
},
|
|
59
76
|
"engines": {
|
|
60
77
|
"node": ">=20.19"
|
|
61
78
|
}
|