@zkmake/three-audit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -0,0 +1,2 @@
1
+ import { n as parseModel, r as createNodeDracoLoader, t as loadModel } from "./load-nqfCU3-k.js";
2
+ export { createNodeDracoLoader, loadModel, parseModel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkmake/three-audit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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
  }