@draco-rust/decoder 0.1.1 → 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.
package/legacy/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
+ }
@@ -0,0 +1,19 @@
1
+ import type { LoadingManager } from 'three';
2
+ import { DRACOLoader as ThreeDRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
3
+ import type { DecoderPool, DecoderPoolOptions } from './pool.js';
4
+
5
+ export interface DRACOLoaderOptions extends DecoderPoolOptions {
6
+ /** A pool to share with other loaders, instead of one of the loader's own. */
7
+ pool?: DecoderPool;
8
+ }
9
+
10
+ /**
11
+ * three.js's `DRACOLoader`, decoding on this entry's worker pool instead of
12
+ * the Draco decoder three fetches from `setDecoderPath`. It is three's class,
13
+ * so it goes wherever one is expected:
14
+ * `gltfLoader.setDRACOLoader(new DRACOLoader())`.
15
+ */
16
+ export class DRACOLoader extends ThreeDRACOLoader {
17
+ constructor(manager?: LoadingManager, options?: DRACOLoaderOptions);
18
+ readonly pool: DecoderPool;
19
+ }
@@ -0,0 +1,82 @@
1
+ // three.js's DRACOLoader, decoding on this entry's worker pool. Everything a
2
+ // caller sees -- load, parse, decodeDracoFile, GLTFLoader's use of it, the
3
+ // geometry it builds -- is three's own class; only the decode is replaced, so
4
+ // the result follows whichever three the application has.
5
+ import { REVISION } from 'three';
6
+ import { DRACOLoader as ThreeDRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
7
+ import { createDecoderPool } from './pool.js';
8
+
9
+ // From r181 DRACOLoader pads an attribute whose items are not a multiple of
10
+ // four bytes and builds it interleaved, from the stride its worker reports.
11
+ const PADS = Number.parseInt(REVISION, 10) >= 181;
12
+
13
+ function padded(array, itemSize) {
14
+ const bytes = array.BYTES_PER_ELEMENT;
15
+ const stride = (Math.ceil((itemSize * bytes) / 4) * 4) / bytes;
16
+ if (!PADS || stride === itemSize) return { array, stride: itemSize };
17
+ const count = array.length / itemSize;
18
+ const out = new array.constructor(count * stride);
19
+ for (let i = 0; i < count; i += 1) {
20
+ out.set(array.subarray(i * itemSize, (i + 1) * itemSize), i * stride);
21
+ }
22
+ return { array: out, stride };
23
+ }
24
+
25
+ export class DRACOLoader extends ThreeDRACOLoader {
26
+ #tasks = new WeakMap();
27
+
28
+ constructor(manager, options = {}) {
29
+ super(manager);
30
+ this.pool = options.pool ?? createDecoderPool(options);
31
+ }
32
+
33
+ // The buffer goes to a worker by transfer, which leaves nothing here to
34
+ // decode again; a second request for it gets the first answer, as three's
35
+ // own task cache gives it.
36
+ decodeGeometry(buffer, taskConfig) {
37
+ const key = JSON.stringify(taskConfig);
38
+ const cached = this.#tasks.get(buffer);
39
+ if (cached?.key === key) return cached.promise;
40
+ if (cached && buffer.byteLength === 0) {
41
+ return Promise.reject(new Error('THREE.DRACOLoader: Unable to re-decode a buffer with different settings. Buffer has already been transferred.'));
42
+ }
43
+ const { attributeIDs, attributeTypes, useUniqueIDs, vertexColorSpace } = taskConfig;
44
+ const requests = Object.keys(attributeIDs).map((name) => ({
45
+ name,
46
+ ...(useUniqueIDs ? { id: attributeIDs[name] } : { semantic: attributeIDs[name] }),
47
+ type: attributeTypes[name],
48
+ }));
49
+ const promise = this.pool.decode(buffer, requests, { transfer: buffer instanceof ArrayBuffer }).then((result) => {
50
+ if (!result.success) throw new Error(`THREE.DRACOLoader: Decoding failed: ${result.error}`);
51
+ return this._createGeometry({
52
+ index: result.index ? { array: result.index, itemSize: 1 } : null,
53
+ attributes: result.attributes.map(({ name, array, itemSize }) => ({
54
+ name,
55
+ itemSize,
56
+ ...padded(array, itemSize),
57
+ ...(name === 'color' ? { vertexColorSpace } : {}),
58
+ })),
59
+ });
60
+ });
61
+ if (buffer instanceof ArrayBuffer) this.#tasks.set(buffer, { key, promise });
62
+ return promise;
63
+ }
64
+
65
+ setWorkerLimit(workerLimit) {
66
+ super.setWorkerLimit(workerLimit);
67
+ this.pool.setWorkerLimit(workerLimit);
68
+ return this;
69
+ }
70
+
71
+ preload() {
72
+ // A failure shows again on the first decode, which is where it is handled.
73
+ this.pool.preload().catch(() => {});
74
+ return this;
75
+ }
76
+
77
+ dispose() {
78
+ super.dispose();
79
+ this.pool.dispose();
80
+ return this;
81
+ }
82
+ }
@@ -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/mesh/index.d.ts CHANGED
@@ -1,6 +1,76 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
 
4
+ /** The name of a typed array `decode_draco` can convert an attribute to. */
5
+ export type DracoArrayType =
6
+ | "Int8Array" | "Uint8Array" | "Int16Array" | "Uint16Array"
7
+ | "Int32Array" | "Uint32Array" | "Float32Array";
8
+
9
+ /** A Draco attribute type, as `semantic` names it. */
10
+ export type DracoSemantic = "POSITION" | "NORMAL" | "COLOR" | "TEX_COORD" | "GENERIC";
11
+
12
+ /**
13
+ * One attribute to decode: by `id`, its unique id, or by `semantic`, the first
14
+ * attribute of that type. `name` is handed back unchanged.
15
+ */
16
+ export interface DracoAttributeRequest {
17
+ name?: string;
18
+ id?: number;
19
+ semantic?: DracoSemantic;
20
+ type?: DracoArrayType;
21
+ }
22
+
23
+ /** The typed arrays an attribute can come back in. */
24
+ export type DracoArray =
25
+ | Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array
26
+ | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
27
+
28
+ export interface DracoAttribute {
29
+ /** The request's `name`, or else the name the stream's metadata gives it. */
30
+ name?: string;
31
+ uniqueId: number;
32
+ semantic: DracoSemantic;
33
+ array: DracoArray;
34
+ itemSize: number;
35
+ normalized: boolean;
36
+ }
37
+
38
+ export interface DracoDecodeResult {
39
+ success: boolean;
40
+ error?: string;
41
+ warnings: string[];
42
+ geometry?: "mesh" | "point_cloud";
43
+ /** Triangle corners for a mesh, `null` for a point cloud. */
44
+ index: Uint32Array | null;
45
+ attributes: DracoAttribute[];
46
+ }
47
+
48
+
49
+
50
+ /**
51
+ * Decodes a Draco stream and returns its attributes as typed arrays.
52
+ *
53
+ * `attributes` is `undefined` for every attribute in its own component type,
54
+ * or an array of requests `{ name, id, semantic, type }`:
55
+ *
56
+ * - `id`, a unique id, finds that attribute, as glTF's
57
+ * `KHR_draco_mesh_compression` names them; a missing one is an error.
58
+ * - `semantic` (`"POSITION"`, `"NORMAL"`, `"COLOR"`, `"TEX_COORD"` or
59
+ * `"GENERIC"`) finds the first attribute of that type instead, as a `.drc`
60
+ * file is read; a missing one is left out.
61
+ * - `type`, a typed array's name such as `"Float32Array"` or
62
+ * `"Uint16Array"`, converts the values the way upstream's
63
+ * `GetAttributeDataArrayForAllPoints` does; without it the values keep
64
+ * their own type. A value the type cannot hold is an error.
65
+ *
66
+ * The result is `{ success, error, warnings, geometry, index, attributes }`,
67
+ * where `geometry` is `"mesh"` or `"point_cloud"` as the stream's header says,
68
+ * `index` a `Uint32Array` of triangle corners for a mesh and `null` for a
69
+ * point cloud, and each attribute `{ name, uniqueId, semantic, array,
70
+ * itemSize, normalized }`.
71
+ */
72
+ export function decode_draco(data: Uint8Array, attributes: DracoAttributeRequest[] | undefined): DracoDecodeResult;
73
+
4
74
  /**
5
75
  * Install the panic hook, so a panic reads as a message rather than a trap.
6
76
  */
@@ -25,14 +95,16 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
25
95
 
26
96
  export interface InitOutput {
27
97
  readonly memory: WebAssembly.Memory;
98
+ readonly decode_draco: (a: number, b: number, c: number) => number;
28
99
  readonly init: () => void;
29
100
  readonly parse_drc_bytes: (a: number, b: number) => number;
30
101
  readonly supported_extensions: (a: number) => void;
31
102
  readonly version: (a: number) => void;
32
- readonly __wbindgen_export: (a: number) => void;
33
- readonly __wbindgen_export2: (a: number, b: number) => number;
103
+ readonly __wbindgen_export: (a: number, b: number) => number;
104
+ readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
105
+ readonly __wbindgen_export3: (a: number) => void;
34
106
  readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
35
- readonly __wbindgen_export3: (a: number, b: number, c: number) => void;
107
+ readonly __wbindgen_export4: (a: number, b: number, c: number) => void;
36
108
  readonly __wbindgen_start: () => void;
37
109
  }
38
110