@draco-rust/decoder 0.1.1 → 0.1.2

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,123 @@
1
+ // What three.js's GLTFLoader and DRACOLoader users call, over the decoder pool.
2
+ // three.js is not imported: the caller hands in the classes, so the package
3
+ // works with whichever copy of three the application bundles.
4
+ import { createDecoderPool } from './pool.js';
5
+
6
+ const SRGB = 'srgb';
7
+ const LINEAR_SRGB = 'srgb-linear';
8
+
9
+ // DRACOLoader's defaults for a `.drc` file, read by attribute type.
10
+ const defaultAttributes = {
11
+ position: 'POSITION',
12
+ normal: 'NORMAL',
13
+ color: 'COLOR',
14
+ uv: 'TEX_COORD',
15
+ };
16
+
17
+ export function createDracoLoader(three, options = {}) {
18
+ const { BufferGeometry, BufferAttribute, InterleavedBuffer, InterleavedBufferAttribute, Color, ColorManagement } =
19
+ three;
20
+ if (typeof BufferGeometry !== 'function' || typeof BufferAttribute !== 'function') {
21
+ throw new TypeError("createDracoLoader needs three.js's BufferGeometry and BufferAttribute");
22
+ }
23
+ const pool = options.pool ?? createDecoderPool(options);
24
+ // The buffer goes to the worker by transfer, which leaves nothing here to
25
+ // decode a second time; GLTFLoader asks again for a primitive it shares, and
26
+ // gets the first answer, as DRACOLoader gives it.
27
+ const tasks = new WeakMap();
28
+
29
+ // Vertex colours a `.drc` file stores in sRGB become linear, as DRACOLoader
30
+ // makes them; without Color and ColorManagement they are left as stored.
31
+ function toWorkingColorSpace(attribute) {
32
+ const convert = ColorManagement?.colorSpaceToWorking ?? ColorManagement?.toWorkingColorSpace;
33
+ if (!Color || !convert) return;
34
+ const color = new Color();
35
+ for (let i = 0; i < attribute.count; i += 1) {
36
+ color.fromBufferAttribute(attribute, i);
37
+ convert.call(ColorManagement, color, SRGB);
38
+ attribute.setXYZ(i, color.r, color.g, color.b);
39
+ }
40
+ }
41
+
42
+ // An item that is not a multiple of four bytes is padded to one, and the
43
+ // attribute interleaved over the padding, as DRACOLoader does since r181
44
+ // for glTF's data alignment rule -- when the classes for it were handed in.
45
+ function attributeOf(array, itemSize) {
46
+ const bytes = array.BYTES_PER_ELEMENT;
47
+ const stride = Math.ceil((itemSize * bytes) / 4) * (4 / bytes);
48
+ if (stride === itemSize || !InterleavedBuffer || !InterleavedBufferAttribute) {
49
+ return new BufferAttribute(array, itemSize);
50
+ }
51
+ const count = array.length / itemSize;
52
+ const padded = new array.constructor(count * stride);
53
+ for (let i = 0; i < count; i += 1) {
54
+ padded.set(array.subarray(i * itemSize, (i + 1) * itemSize), i * stride);
55
+ }
56
+ return new InterleavedBufferAttribute(new InterleavedBuffer(padded, stride), itemSize, 0);
57
+ }
58
+
59
+ async function decode(buffer, attributeIDs, attributeTypes, vertexColorSpace) {
60
+ const byId = !!attributeIDs;
61
+ const requests = Object.entries(attributeIDs ?? defaultAttributes).map(([name, id]) => ({
62
+ name,
63
+ ...(byId ? { id } : { semantic: id }),
64
+ type: attributeTypes?.[name] ?? 'Float32Array',
65
+ }));
66
+ const result = await pool.decode(buffer, requests, { transfer: buffer instanceof ArrayBuffer });
67
+ if (!result.success) throw new Error(result.error);
68
+
69
+ const geometry = new BufferGeometry();
70
+ if (result.index) geometry.setIndex(new BufferAttribute(result.index, 1));
71
+ for (const { name, array, itemSize } of result.attributes) {
72
+ const attribute = attributeOf(array, itemSize);
73
+ if (name === 'color') {
74
+ if (vertexColorSpace === SRGB) toWorkingColorSpace(attribute);
75
+ attribute.normalized = !(array instanceof Float32Array);
76
+ }
77
+ geometry.setAttribute(name, attribute);
78
+ }
79
+ return geometry;
80
+ }
81
+
82
+ function decodeGeometry(buffer, { attributeIDs, attributeTypes, vertexColorSpace = LINEAR_SRGB } = {}) {
83
+ const key = JSON.stringify([attributeIDs ?? null, attributeTypes ?? null, vertexColorSpace]);
84
+ const cached = buffer instanceof ArrayBuffer ? tasks.get(buffer) : undefined;
85
+ if (cached?.key === key) return cached.promise;
86
+ if (cached && buffer.byteLength === 0) {
87
+ return Promise.reject(new Error('this buffer was already decoded with other settings and handed to a worker'));
88
+ }
89
+ const promise = decode(buffer, attributeIDs, attributeTypes, vertexColorSpace);
90
+ if (buffer instanceof ArrayBuffer) tasks.set(buffer, { key, promise });
91
+ return promise;
92
+ }
93
+
94
+ const loader = {
95
+ pool,
96
+ decodeGeometry,
97
+ /** DRACOLoader's entry for GLTFLoader's `KHR_draco_mesh_compression`. */
98
+ decodeDracoFile(buffer, callback, attributeIDs, attributeTypes, vertexColorSpace = LINEAR_SRGB, onError = () => {}) {
99
+ return decodeGeometry(buffer, { attributeIDs, attributeTypes, vertexColorSpace }).then(callback).catch(onError);
100
+ },
101
+ /** A `.drc` file's contents, as DRACOLoader's `parse`. */
102
+ parse(buffer, onLoad, onError = () => {}) {
103
+ loader.decodeDracoFile(buffer, onLoad, null, null, SRGB, onError);
104
+ },
105
+ parseAsync(buffer) {
106
+ return decodeGeometry(buffer, { vertexColorSpace: SRGB });
107
+ },
108
+ preload() {
109
+ // A failure shows again on the first decode, which is where it is handled.
110
+ pool.preload().catch(() => {});
111
+ return loader;
112
+ },
113
+ setWorkerLimit(workers) {
114
+ pool.setWorkerLimit(workers);
115
+ return loader;
116
+ },
117
+ dispose() {
118
+ pool.dispose();
119
+ return loader;
120
+ },
121
+ };
122
+ return loader;
123
+ }
@@ -0,0 +1,28 @@
1
+ // A decoder worker. The pool sends it the compiled module once, then streams
2
+ // to decode; it answers each with its result, handing the arrays back by
3
+ // transfer. Runs as a Web Worker and as a Node worker thread.
4
+ import init, { decode_draco } from './index.js';
5
+
6
+ const web = typeof self !== 'undefined' && typeof self.postMessage === 'function';
7
+ const port = web ? self : process.getBuiltinModule('node:worker_threads').parentPort;
8
+ const listen = (handler) =>
9
+ web ? self.addEventListener('message', (event) => handler(event.data)) : port.on('message', handler);
10
+
11
+ let ready = null;
12
+
13
+ listen(async (message) => {
14
+ if (message.type === 'init') {
15
+ ready = init({ module_or_path: message.module });
16
+ return;
17
+ }
18
+ const { id, data, attributes } = message;
19
+ try {
20
+ await ready;
21
+ const result = decode_draco(data, attributes);
22
+ const transfer = result.attributes.map((attribute) => attribute.array.buffer);
23
+ if (result.index) transfer.push(result.index.buffer);
24
+ port.postMessage({ id, result }, transfer);
25
+ } catch (error) {
26
+ port.postMessage({ id, error: String(error?.message ?? error) });
27
+ }
28
+ });
package/pool.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ import type { DracoAttributeRequest, DracoDecodeResult } from './index.js';
2
+
3
+ export interface DecoderPoolOptions {
4
+ /**
5
+ * How many workers to run at most; they start as work arrives. Defaults to
6
+ * one less than the cores, between one and four. `0` decodes on the calling
7
+ * thread, as does an environment without workers.
8
+ */
9
+ workers?: number;
10
+ /**
11
+ * The entry's wasm, when it is not next to this module where the pool looks
12
+ * for it: a URL, a `Response`, the bytes, or a compiled module.
13
+ */
14
+ wasm?: string | URL | Response | BufferSource | WebAssembly.Module;
15
+ }
16
+
17
+ export interface DecodeOptions {
18
+ /**
19
+ * Hand the stream's buffer to the worker instead of copying it, leaving it
20
+ * detached here. For a buffer nothing else reads afterwards.
21
+ */
22
+ transfer?: boolean;
23
+ }
24
+
25
+ export interface DecoderPool {
26
+ /**
27
+ * `decode_draco` on a worker. A stream that does not decode resolves with
28
+ * `success: false`, as `decode_draco` returns it; a worker that fails rejects
29
+ * the tasks it held.
30
+ */
31
+ decode(
32
+ data: ArrayBuffer | ArrayBufferView,
33
+ attributes?: DracoAttributeRequest[],
34
+ options?: DecodeOptions,
35
+ ): Promise<DracoDecodeResult>;
36
+ /** Compiles the wasm and starts one worker ahead of the first decode. */
37
+ preload(): Promise<void>;
38
+ /** Changes the worker limit; idle workers past it stop now, busy ones when done. */
39
+ setWorkerLimit(workers: number): void;
40
+ readonly workerLimit: number;
41
+ /** Stops every worker and rejects what they held. */
42
+ dispose(): void;
43
+ }
44
+
45
+ export function createDecoderPool(options?: DecoderPoolOptions): DecoderPool;
package/pool.js ADDED
@@ -0,0 +1,193 @@
1
+ // Decoding off the calling thread: a pool of workers, each running this entry's
2
+ // own module. The wasm is compiled once here and handed to every worker, the
3
+ // stream goes over by transfer and the arrays come back the same way, so a
4
+ // decode copies nothing it does not have to.
5
+ import init, { decode_draco } from './index.js';
6
+
7
+ /** Leaves a core for the page, and stops at four, past which a page rarely has
8
+ * that many streams in flight at once. */
9
+ const defaultWorkers = () =>
10
+ Math.max(1, Math.min(4, (globalThis.navigator?.hardwareConcurrency ?? 4) - 1));
11
+
12
+ const builtin = (name) =>
13
+ typeof process !== 'undefined' && typeof process.getBuiltinModule === 'function'
14
+ ? process.getBuiltinModule(name)
15
+ : undefined;
16
+
17
+ async function compile(wasm) {
18
+ if (wasm instanceof WebAssembly.Module) return wasm;
19
+ let source = wasm ?? new URL('./index_bg.wasm', import.meta.url);
20
+ if (typeof source === 'string' || source instanceof URL) {
21
+ const url = new URL(source, import.meta.url);
22
+ // Node's fetch does not read files.
23
+ const fs = url.protocol === 'file:' ? builtin('node:fs/promises') : undefined;
24
+ if (fs) return WebAssembly.compile(await fs.readFile(url));
25
+ source = await fetch(url);
26
+ }
27
+ if (typeof Response !== 'undefined' && source instanceof Response) {
28
+ if (!source.ok) throw new Error(`failed to fetch ${source.url}: ${source.status}`);
29
+ if (source.headers.get('content-type') === 'application/wasm') {
30
+ return WebAssembly.compileStreaming(source);
31
+ }
32
+ source = await source.arrayBuffer();
33
+ }
34
+ return WebAssembly.compile(source);
35
+ }
36
+
37
+ /** What the pool needs of a worker, the same over a Web Worker and a Node
38
+ * worker thread. */
39
+ function spawn(blobUrl) {
40
+ if (typeof Worker === 'function') {
41
+ const url = new URL('./worker.js', import.meta.url);
42
+ // A bundler recognises this exact form and emits the worker as a chunk.
43
+ const worker =
44
+ typeof location === 'undefined' || url.origin === location.origin
45
+ ? new Worker(new URL('./worker.js', import.meta.url), { type: 'module' })
46
+ : new Worker(blobUrl(url), { type: 'module' });
47
+ return {
48
+ post: (message, transfer) => worker.postMessage(message, transfer),
49
+ listen: (onMessage, onError) => {
50
+ worker.addEventListener('message', (event) => onMessage(event.data));
51
+ worker.addEventListener('error', (event) => {
52
+ event.preventDefault();
53
+ onError(new Error(event.message || 'the decoder worker failed'));
54
+ });
55
+ worker.addEventListener('messageerror', () => onError(new Error('a decode result could not be read')));
56
+ },
57
+ busy: () => {},
58
+ terminate: () => worker.terminate(),
59
+ };
60
+ }
61
+ const threads = builtin('node:worker_threads');
62
+ if (!threads) return null;
63
+ const worker = new threads.Worker(new URL('./worker.js', import.meta.url));
64
+ // An idle worker does not keep Node running; one with work does.
65
+ worker.unref();
66
+ return {
67
+ post: (message, transfer) => worker.postMessage(message, transfer),
68
+ listen: (onMessage, onError) => {
69
+ worker.on('message', onMessage);
70
+ worker.on('error', onError);
71
+ worker.on('exit', (code) => onError(new Error(`the decoder worker exited with code ${code}`)));
72
+ },
73
+ busy: (busy) => (busy ? worker.ref() : worker.unref()),
74
+ terminate: () => worker.terminate(),
75
+ };
76
+ }
77
+
78
+ const workersAvailable = () =>
79
+ typeof Worker === 'function' || builtin('node:worker_threads') !== undefined;
80
+
81
+ export function createDecoderPool(options = {}) {
82
+ let limit = options.workers ?? defaultWorkers();
83
+ let compiled = null;
84
+ let local = null;
85
+ let blob = null;
86
+ let disposed = false;
87
+ let nextId = 0;
88
+ const slots = [];
89
+
90
+ const load = () => (compiled ??= compile(options.wasm));
91
+ // A worker script must share the page's origin. From a CDN, a worker made
92
+ // here imports it instead, which a module worker may do across origins.
93
+ const blobUrl = (url) =>
94
+ (blob ??= URL.createObjectURL(
95
+ new Blob([`import ${JSON.stringify(url.href)};`], { type: 'text/javascript' }),
96
+ ));
97
+
98
+ function retire(slot, error) {
99
+ const at = slots.indexOf(slot);
100
+ if (at >= 0) slots.splice(at, 1);
101
+ slot.worker.terminate();
102
+ for (const { reject } of slot.pending.values()) reject(error);
103
+ slot.pending.clear();
104
+ }
105
+
106
+ function open(module) {
107
+ const worker = spawn(blobUrl);
108
+ if (!worker) return null;
109
+ const slot = { worker, pending: new Map() };
110
+ worker.listen(
111
+ (message) => {
112
+ const task = slot.pending.get(message.id);
113
+ if (!task) return;
114
+ slot.pending.delete(message.id);
115
+ if (slot.pending.size === 0) {
116
+ worker.busy(false);
117
+ if (slots.indexOf(slot) >= limit) retire(slot);
118
+ }
119
+ if ('error' in message) task.reject(new Error(message.error));
120
+ else task.resolve(message.result);
121
+ },
122
+ // A worker that fails takes only its own tasks down; the next decode
123
+ // starts another.
124
+ (error) => {
125
+ if (slots.includes(slot)) retire(slot, error);
126
+ },
127
+ );
128
+ worker.post({ type: 'init', module });
129
+ slots.push(slot);
130
+ return slot;
131
+ }
132
+
133
+ /** The least loaded worker, or a new one while every worker is busy and
134
+ * the limit allows another. */
135
+ function pick(module) {
136
+ let best = null;
137
+ for (const slot of slots.slice(0, limit)) {
138
+ if (!best || slot.pending.size < best.pending.size) best = slot;
139
+ }
140
+ if ((!best || best.pending.size > 0) && slots.length < limit) return open(module) ?? best;
141
+ return best;
142
+ }
143
+
144
+ const onThisThread = () =>
145
+ (local ??= load().then((module) => init({ module_or_path: module })));
146
+
147
+ async function decode(data, attributes, { transfer = false } = {}) {
148
+ if (disposed) throw new Error('the decoder pool was disposed');
149
+ const bytes = ArrayBuffer.isView(data)
150
+ ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
151
+ : new Uint8Array(data);
152
+ if (limit > 0 && workersAvailable()) {
153
+ const module = await load();
154
+ if (disposed) throw new Error('the decoder pool was disposed');
155
+ const slot = pick(module);
156
+ if (slot) {
157
+ const id = nextId++;
158
+ const sent = transfer ? bytes : bytes.slice();
159
+ return new Promise((resolve, reject) => {
160
+ slot.pending.set(id, { resolve, reject });
161
+ slot.worker.busy(true);
162
+ slot.worker.post({ id, data: sent, attributes }, [sent.buffer]);
163
+ });
164
+ }
165
+ }
166
+ await onThisThread();
167
+ return decode_draco(bytes, attributes);
168
+ }
169
+
170
+ return {
171
+ decode,
172
+ async preload() {
173
+ if (limit > 0 && workersAvailable()) {
174
+ const module = await load();
175
+ if (!disposed && slots.length === 0) open(module);
176
+ } else {
177
+ await onThisThread();
178
+ }
179
+ },
180
+ setWorkerLimit(workers) {
181
+ limit = workers;
182
+ for (const slot of slots.slice(limit)) if (slot.pending.size === 0) retire(slot);
183
+ },
184
+ get workerLimit() {
185
+ return limit;
186
+ },
187
+ dispose() {
188
+ disposed = true;
189
+ for (const slot of [...slots]) retire(slot, new Error('the decoder pool was disposed'));
190
+ if (blob) URL.revokeObjectURL(blob);
191
+ },
192
+ };
193
+ }
package/three.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { DracoArrayType } from './index.js';
2
+ import type { DecoderPool, DecoderPoolOptions } from './pool.js';
3
+
4
+ /**
5
+ * The three.js classes the loader builds with, passed in so the package uses
6
+ * the application's own copy of three: `import * as THREE from "three"` will
7
+ * do. With `InterleavedBuffer` and `InterleavedBufferAttribute`, an attribute
8
+ * whose items are not a multiple of four bytes is padded to one, as DRACOLoader
9
+ * pads it since r181; with `Color` and `ColorManagement`, a `.drc` file's sRGB
10
+ * vertex colours are made linear as DRACOLoader makes them.
11
+ */
12
+ export interface ThreeClasses<Geometry> {
13
+ BufferGeometry: new () => Geometry;
14
+ BufferAttribute: new (array: any, itemSize: number, normalized?: boolean) => any;
15
+ InterleavedBuffer?: new (array: any, stride: number) => any;
16
+ InterleavedBufferAttribute?: new (buffer: any, itemSize: number, offset: number) => any;
17
+ Color?: new () => any;
18
+ /** `colorSpaceToWorking` since three r177, `toWorkingColorSpace` before. */
19
+ ColorManagement?:
20
+ | { colorSpaceToWorking(color: any, colorSpace: string): any }
21
+ | { toWorkingColorSpace(color: any, colorSpace: string): any };
22
+ }
23
+
24
+ export interface DracoLoaderOptions extends DecoderPoolOptions {
25
+ /** A pool to share with other loaders, instead of one of the loader's own. */
26
+ pool?: DecoderPool;
27
+ }
28
+
29
+ export interface DecodeGeometryOptions {
30
+ /** three.js attribute name to Draco unique id, as glTF names them. Without
31
+ * it, `position`, `normal`, `color` and `uv` are read by type. */
32
+ attributeIDs?: Record<string, number>;
33
+ /** three.js attribute name to the typed array it comes back in. */
34
+ attributeTypes?: Record<string, DracoArrayType>;
35
+ /** `"srgb"` makes vertex colours linear; the default leaves them as stored. */
36
+ vertexColorSpace?: string;
37
+ }
38
+
39
+ /** The part of three.js's DRACOLoader that GLTFLoader and `.drc` users call. */
40
+ export interface DracoLoader<Geometry> {
41
+ readonly pool: DecoderPool;
42
+ decodeGeometry(buffer: ArrayBuffer | ArrayBufferView, options?: DecodeGeometryOptions): Promise<Geometry>;
43
+ decodeDracoFile(
44
+ buffer: ArrayBuffer,
45
+ callback: (geometry: Geometry) => void,
46
+ attributeIDs?: Record<string, number> | null,
47
+ attributeTypes?: Record<string, DracoArrayType> | null,
48
+ vertexColorSpace?: string,
49
+ onError?: (error: unknown) => void,
50
+ ): Promise<void>;
51
+ parse(buffer: ArrayBuffer, onLoad: (geometry: Geometry) => void, onError?: (error: unknown) => void): void;
52
+ parseAsync(buffer: ArrayBuffer | ArrayBufferView): Promise<Geometry>;
53
+ preload(): this;
54
+ setWorkerLimit(workers: number): this;
55
+ dispose(): this;
56
+ }
57
+
58
+ /**
59
+ * A stand-in for three.js's DRACOLoader on the decoder pool:
60
+ * `gltfLoader.setDRACOLoader(createDracoLoader(THREE))`.
61
+ */
62
+ export function createDracoLoader<Geometry>(
63
+ three: ThreeClasses<Geometry>,
64
+ options?: DracoLoaderOptions,
65
+ ): DracoLoader<Geometry>;
package/three.js ADDED
@@ -0,0 +1,123 @@
1
+ // What three.js's GLTFLoader and DRACOLoader users call, over the decoder pool.
2
+ // three.js is not imported: the caller hands in the classes, so the package
3
+ // works with whichever copy of three the application bundles.
4
+ import { createDecoderPool } from './pool.js';
5
+
6
+ const SRGB = 'srgb';
7
+ const LINEAR_SRGB = 'srgb-linear';
8
+
9
+ // DRACOLoader's defaults for a `.drc` file, read by attribute type.
10
+ const defaultAttributes = {
11
+ position: 'POSITION',
12
+ normal: 'NORMAL',
13
+ color: 'COLOR',
14
+ uv: 'TEX_COORD',
15
+ };
16
+
17
+ export function createDracoLoader(three, options = {}) {
18
+ const { BufferGeometry, BufferAttribute, InterleavedBuffer, InterleavedBufferAttribute, Color, ColorManagement } =
19
+ three;
20
+ if (typeof BufferGeometry !== 'function' || typeof BufferAttribute !== 'function') {
21
+ throw new TypeError("createDracoLoader needs three.js's BufferGeometry and BufferAttribute");
22
+ }
23
+ const pool = options.pool ?? createDecoderPool(options);
24
+ // The buffer goes to the worker by transfer, which leaves nothing here to
25
+ // decode a second time; GLTFLoader asks again for a primitive it shares, and
26
+ // gets the first answer, as DRACOLoader gives it.
27
+ const tasks = new WeakMap();
28
+
29
+ // Vertex colours a `.drc` file stores in sRGB become linear, as DRACOLoader
30
+ // makes them; without Color and ColorManagement they are left as stored.
31
+ function toWorkingColorSpace(attribute) {
32
+ const convert = ColorManagement?.colorSpaceToWorking ?? ColorManagement?.toWorkingColorSpace;
33
+ if (!Color || !convert) return;
34
+ const color = new Color();
35
+ for (let i = 0; i < attribute.count; i += 1) {
36
+ color.fromBufferAttribute(attribute, i);
37
+ convert.call(ColorManagement, color, SRGB);
38
+ attribute.setXYZ(i, color.r, color.g, color.b);
39
+ }
40
+ }
41
+
42
+ // An item that is not a multiple of four bytes is padded to one, and the
43
+ // attribute interleaved over the padding, as DRACOLoader does since r181
44
+ // for glTF's data alignment rule -- when the classes for it were handed in.
45
+ function attributeOf(array, itemSize) {
46
+ const bytes = array.BYTES_PER_ELEMENT;
47
+ const stride = Math.ceil((itemSize * bytes) / 4) * (4 / bytes);
48
+ if (stride === itemSize || !InterleavedBuffer || !InterleavedBufferAttribute) {
49
+ return new BufferAttribute(array, itemSize);
50
+ }
51
+ const count = array.length / itemSize;
52
+ const padded = new array.constructor(count * stride);
53
+ for (let i = 0; i < count; i += 1) {
54
+ padded.set(array.subarray(i * itemSize, (i + 1) * itemSize), i * stride);
55
+ }
56
+ return new InterleavedBufferAttribute(new InterleavedBuffer(padded, stride), itemSize, 0);
57
+ }
58
+
59
+ async function decode(buffer, attributeIDs, attributeTypes, vertexColorSpace) {
60
+ const byId = !!attributeIDs;
61
+ const requests = Object.entries(attributeIDs ?? defaultAttributes).map(([name, id]) => ({
62
+ name,
63
+ ...(byId ? { id } : { semantic: id }),
64
+ type: attributeTypes?.[name] ?? 'Float32Array',
65
+ }));
66
+ const result = await pool.decode(buffer, requests, { transfer: buffer instanceof ArrayBuffer });
67
+ if (!result.success) throw new Error(result.error);
68
+
69
+ const geometry = new BufferGeometry();
70
+ if (result.index) geometry.setIndex(new BufferAttribute(result.index, 1));
71
+ for (const { name, array, itemSize } of result.attributes) {
72
+ const attribute = attributeOf(array, itemSize);
73
+ if (name === 'color') {
74
+ if (vertexColorSpace === SRGB) toWorkingColorSpace(attribute);
75
+ attribute.normalized = !(array instanceof Float32Array);
76
+ }
77
+ geometry.setAttribute(name, attribute);
78
+ }
79
+ return geometry;
80
+ }
81
+
82
+ function decodeGeometry(buffer, { attributeIDs, attributeTypes, vertexColorSpace = LINEAR_SRGB } = {}) {
83
+ const key = JSON.stringify([attributeIDs ?? null, attributeTypes ?? null, vertexColorSpace]);
84
+ const cached = buffer instanceof ArrayBuffer ? tasks.get(buffer) : undefined;
85
+ if (cached?.key === key) return cached.promise;
86
+ if (cached && buffer.byteLength === 0) {
87
+ return Promise.reject(new Error('this buffer was already decoded with other settings and handed to a worker'));
88
+ }
89
+ const promise = decode(buffer, attributeIDs, attributeTypes, vertexColorSpace);
90
+ if (buffer instanceof ArrayBuffer) tasks.set(buffer, { key, promise });
91
+ return promise;
92
+ }
93
+
94
+ const loader = {
95
+ pool,
96
+ decodeGeometry,
97
+ /** DRACOLoader's entry for GLTFLoader's `KHR_draco_mesh_compression`. */
98
+ decodeDracoFile(buffer, callback, attributeIDs, attributeTypes, vertexColorSpace = LINEAR_SRGB, onError = () => {}) {
99
+ return decodeGeometry(buffer, { attributeIDs, attributeTypes, vertexColorSpace }).then(callback).catch(onError);
100
+ },
101
+ /** A `.drc` file's contents, as DRACOLoader's `parse`. */
102
+ parse(buffer, onLoad, onError = () => {}) {
103
+ loader.decodeDracoFile(buffer, onLoad, null, null, SRGB, onError);
104
+ },
105
+ parseAsync(buffer) {
106
+ return decodeGeometry(buffer, { vertexColorSpace: SRGB });
107
+ },
108
+ preload() {
109
+ // A failure shows again on the first decode, which is where it is handled.
110
+ pool.preload().catch(() => {});
111
+ return loader;
112
+ },
113
+ setWorkerLimit(workers) {
114
+ pool.setWorkerLimit(workers);
115
+ return loader;
116
+ },
117
+ dispose() {
118
+ pool.dispose();
119
+ return loader;
120
+ },
121
+ };
122
+ return loader;
123
+ }
package/worker.js ADDED
@@ -0,0 +1,28 @@
1
+ // A decoder worker. The pool sends it the compiled module once, then streams
2
+ // to decode; it answers each with its result, handing the arrays back by
3
+ // transfer. Runs as a Web Worker and as a Node worker thread.
4
+ import init, { decode_draco } from './index.js';
5
+
6
+ const web = typeof self !== 'undefined' && typeof self.postMessage === 'function';
7
+ const port = web ? self : process.getBuiltinModule('node:worker_threads').parentPort;
8
+ const listen = (handler) =>
9
+ web ? self.addEventListener('message', (event) => handler(event.data)) : port.on('message', handler);
10
+
11
+ let ready = null;
12
+
13
+ listen(async (message) => {
14
+ if (message.type === 'init') {
15
+ ready = init({ module_or_path: message.module });
16
+ return;
17
+ }
18
+ const { id, data, attributes } = message;
19
+ try {
20
+ await ready;
21
+ const result = decode_draco(data, attributes);
22
+ const transfer = result.attributes.map((attribute) => attribute.array.buffer);
23
+ if (result.index) transfer.push(result.index.buffer);
24
+ port.postMessage({ id, result }, transfer);
25
+ } catch (error) {
26
+ port.postMessage({ id, error: String(error?.message ?? error) });
27
+ }
28
+ });