@draco-rust/decoder 0.1.2 → 0.3.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.
Files changed (50) hide show
  1. package/README.md +27 -21
  2. package/index.d.ts +21 -25
  3. package/index.js +12 -7
  4. package/index_bg.wasm +0 -0
  5. package/index_bg.wasm.d.ts +1 -1
  6. package/legacy/index.d.ts +21 -25
  7. package/legacy/index.js +12 -7
  8. package/legacy/index_bg.wasm +0 -0
  9. package/legacy/index_bg.wasm.d.ts +1 -1
  10. package/legacy/pool-core.d.ts +44 -0
  11. package/legacy/pool-core.js +250 -0
  12. package/legacy/pool.d.ts +6 -29
  13. package/legacy/pool.js +15 -184
  14. package/legacy/tasks.js +2 -0
  15. package/legacy/three.d.ts +11 -57
  16. package/legacy/three.js +66 -107
  17. package/legacy/worker.js +24 -10
  18. package/mesh/index.d.ts +21 -25
  19. package/mesh/index.js +12 -7
  20. package/mesh/index_bg.wasm +0 -0
  21. package/mesh/index_bg.wasm.d.ts +1 -1
  22. package/mesh/pool-core.d.ts +44 -0
  23. package/mesh/pool-core.js +250 -0
  24. package/mesh/pool.d.ts +6 -29
  25. package/mesh/pool.js +15 -184
  26. package/mesh/tasks.js +2 -0
  27. package/mesh/three.d.ts +11 -57
  28. package/mesh/three.js +66 -107
  29. package/mesh/worker.js +24 -10
  30. package/package.json +10 -2
  31. package/point-cloud/index.d.ts +21 -25
  32. package/point-cloud/index.js +12 -7
  33. package/point-cloud/index_bg.wasm +0 -0
  34. package/point-cloud/index_bg.wasm.d.ts +1 -1
  35. package/point-cloud/pool-core.d.ts +44 -0
  36. package/point-cloud/pool-core.js +250 -0
  37. package/point-cloud/pool.d.ts +6 -29
  38. package/point-cloud/pool.js +15 -184
  39. package/point-cloud/tasks.js +2 -0
  40. package/point-cloud/three.d.ts +11 -57
  41. package/point-cloud/three.js +66 -107
  42. package/point-cloud/worker.js +24 -10
  43. package/pool-core.d.ts +44 -0
  44. package/pool-core.js +250 -0
  45. package/pool.d.ts +6 -29
  46. package/pool.js +15 -184
  47. package/tasks.js +2 -0
  48. package/three.d.ts +11 -57
  49. package/three.js +66 -107
  50. package/worker.js +24 -10
package/mesh/three.js CHANGED
@@ -1,123 +1,82 @@
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.
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';
4
7
  import { createDecoderPool } from './pool.js';
5
8
 
6
- const SRGB = 'srgb';
7
- const LINEAR_SRGB = 'srgb-linear';
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;
8
12
 
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");
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);
22
21
  }
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();
22
+ return { array: out, stride };
23
+ }
28
24
 
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
- }
25
+ export class DRACOLoader extends ThreeDRACOLoader {
26
+ #tasks = new WeakMap();
41
27
 
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);
28
+ constructor(manager, options = {}) {
29
+ super(manager);
30
+ this.pool = options.pool ?? createDecoderPool(options);
57
31
  }
58
32
 
59
- async function decode(buffer, attributeIDs, attributeTypes, vertexColorSpace) {
60
- const byId = !!attributeIDs;
61
- const requests = Object.entries(attributeIDs ?? defaultAttributes).map(([name, id]) => ({
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) => ({
62
45
  name,
63
- ...(byId ? { id } : { semantic: id }),
64
- type: attributeTypes?.[name] ?? 'Float32Array',
46
+ ...(useUniqueIDs ? { id: attributeIDs[name] } : { semantic: attributeIDs[name] }),
47
+ type: attributeTypes[name],
65
48
  }));
66
- const result = await pool.decode(buffer, requests, { transfer: buffer instanceof ArrayBuffer });
67
- if (!result.success) throw new Error(result.error);
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
+ }
68
64
 
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;
65
+ setWorkerLimit(workerLimit) {
66
+ super.setWorkerLimit(workerLimit);
67
+ this.pool.setWorkerLimit(workerLimit);
68
+ return this;
80
69
  }
81
70
 
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;
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;
92
75
  }
93
76
 
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;
77
+ dispose() {
78
+ super.dispose();
79
+ this.pool.dispose();
80
+ return this;
81
+ }
123
82
  }
package/mesh/worker.js CHANGED
@@ -1,7 +1,9 @@
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';
1
+ // A pool worker. The pool sends it the compiled module once, then tasks by
2
+ // name from this entry's `tasks.js`; it answers each with its result, handing
3
+ // every array in it back by transfer. Runs as a Web Worker and as a Node
4
+ // worker thread.
5
+ import init from './index.js';
6
+ import * as tasks from './tasks.js';
5
7
 
6
8
  const web = typeof self !== 'undefined' && typeof self.postMessage === 'function';
7
9
  const port = web ? self : process.getBuiltinModule('node:worker_threads').parentPort;
@@ -10,18 +12,30 @@ const listen = (handler) =>
10
12
 
11
13
  let ready = null;
12
14
 
15
+ /** Every buffer in a result, except the module's own memory, which stays. */
16
+ function buffersOf(value, memory, found = new Set()) {
17
+ if (ArrayBuffer.isView(value)) {
18
+ if (value.buffer instanceof ArrayBuffer && value.buffer !== memory) found.add(value.buffer);
19
+ } else if (value instanceof ArrayBuffer) {
20
+ if (value !== memory) found.add(value);
21
+ } else if (Array.isArray(value)) {
22
+ for (const item of value) buffersOf(item, memory, found);
23
+ } else if (value !== null && typeof value === 'object') {
24
+ for (const item of Object.values(value)) buffersOf(item, memory, found);
25
+ }
26
+ return found;
27
+ }
28
+
13
29
  listen(async (message) => {
14
30
  if (message.type === 'init') {
15
31
  ready = init({ module_or_path: message.module });
16
32
  return;
17
33
  }
18
- const { id, data, attributes } = message;
34
+ const { id, task, args } = message;
19
35
  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);
36
+ const { memory } = await ready;
37
+ const result = await tasks[task](...args);
38
+ port.postMessage({ id, result }, [...buffersOf(result, memory.buffer)]);
25
39
  } catch (error) {
26
40
  port.postMessage({ id, error: String(error?.message ?? error) });
27
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draco-rust/decoder",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Draco geometry decoder in WebAssembly, a safe Rust port of Google Draco: meshes, point clouds and older bitstreams, each as its own small entry.",
5
5
  "keywords": [
6
6
  "draco",
@@ -24,7 +24,7 @@
24
24
  "directory": "web/npm/decoder"
25
25
  },
26
26
  "draco-rust": {
27
- "commit": "6bc95d5c",
27
+ "commit": "4b72cfc8",
28
28
  "draco-core": "2.3.2",
29
29
  "draco-io": "0.6.0",
30
30
  "draco-gltf": "0.7.0"
@@ -72,6 +72,14 @@
72
72
  "**/*.d.ts",
73
73
  "**/*.wasm"
74
74
  ],
75
+ "peerDependencies": {
76
+ "three": ">=0.150.0"
77
+ },
78
+ "peerDependenciesMeta": {
79
+ "three": {
80
+ "optional": true
81
+ }
82
+ },
75
83
  "publishConfig": {
76
84
  "access": "public"
77
85
  }
@@ -35,41 +35,37 @@ export interface DracoAttribute {
35
35
  normalized: boolean;
36
36
  }
37
37
 
38
+ export interface DracoDecodeOptions {
39
+ /**
40
+ * The index's array: `"Uint32Array"`, the default, or `"auto"`, which uses a
41
+ * `Uint16Array` when every point number fits, at most 65536 points, and
42
+ * halves what a worker sends back.
43
+ */
44
+ index?: "Uint32Array" | "auto";
45
+ }
46
+
38
47
  export interface DracoDecodeResult {
39
48
  success: boolean;
40
49
  error?: string;
41
50
  warnings: string[];
42
51
  geometry?: "mesh" | "point_cloud";
43
52
  /** Triangle corners for a mesh, `null` for a point cloud. */
44
- index: Uint32Array | null;
53
+ index: Uint32Array | Uint16Array | null;
45
54
  attributes: DracoAttribute[];
46
55
  }
47
56
 
48
-
49
-
50
57
  /**
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 }`.
58
+ * Decodes a Draco stream and returns its attributes as typed arrays: every
59
+ * attribute in its own type, or the ones `attributes` requests, converted as
60
+ * upstream's `GetAttributeDataArrayForAllPoints` converts them.
71
61
  */
72
- export function decode_draco(data: Uint8Array, attributes: DracoAttributeRequest[] | undefined): DracoDecodeResult;
62
+ export function decode_draco(
63
+ data: Uint8Array,
64
+ attributes?: DracoAttributeRequest[],
65
+ options?: DracoDecodeOptions,
66
+ ): DracoDecodeResult;
67
+
68
+
73
69
 
74
70
  /**
75
71
  * Install the panic hook, so a panic reads as a message rather than a trap.
@@ -95,7 +91,7 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
95
91
 
96
92
  export interface InitOutput {
97
93
  readonly memory: WebAssembly.Memory;
98
- readonly decode_draco: (a: number, b: number, c: number) => number;
94
+ readonly decode_draco: (a: number, b: number, c: number, d: number) => number;
99
95
  readonly init: () => void;
100
96
  readonly parse_drc_bytes: (a: number, b: number) => number;
101
97
  readonly supported_extensions: (a: number) => void;
@@ -18,17 +18,22 @@
18
18
  *
19
19
  * The result is `{ success, error, warnings, geometry, index, attributes }`,
20
20
  * where `geometry` is `"mesh"` or `"point_cloud"` as the stream's header says,
21
- * `index` a `Uint32Array` of triangle corners for a mesh and `null` for a
22
- * point cloud, and each attribute `{ name, uniqueId, semantic, array,
23
- * itemSize, normalized }`.
21
+ * `index` the triangle corners for a mesh and `null` for a point cloud, and
22
+ * each attribute `{ name, uniqueId, semantic, array, itemSize, normalized }`.
23
+ * The index is a `Uint32Array`, or with `options.index` set to `"auto"` a
24
+ * `Uint16Array` where the points number at most 65536.
25
+ *
26
+ * Its TypeScript declaration is written by hand in the section above, where
27
+ * the trailing parameters can be optional.
24
28
  * @param {Uint8Array} data
25
- * @param {DracoAttributeRequest[] | undefined} attributes
26
- * @returns {DracoDecodeResult}
29
+ * @param {any} attributes
30
+ * @param {object | null} [options]
31
+ * @returns {any}
27
32
  */
28
- export function decode_draco(data, attributes) {
33
+ export function decode_draco(data, attributes, options) {
29
34
  const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_export);
30
35
  const len0 = WASM_VECTOR_LEN;
31
- const ret = wasm.decode_draco(ptr0, len0, addHeapObject(attributes));
36
+ const ret = wasm.decode_draco(ptr0, len0, addHeapObject(attributes), isLikeNone(options) ? 0 : addHeapObject(options));
32
37
  return takeObject(ret);
33
38
  }
34
39
 
Binary file
@@ -1,7 +1,7 @@
1
1
  /* tslint:disable */
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
- export const decode_draco: (a: number, b: number, c: number) => number;
4
+ export const decode_draco: (a: number, b: number, c: number, d: number) => number;
5
5
  export const init: () => void;
6
6
  export const parse_drc_bytes: (a: number, b: number) => number;
7
7
  export const supported_extensions: (a: number) => void;
@@ -0,0 +1,44 @@
1
+ export interface PoolOptions {
2
+ /**
3
+ * How many workers to run at most; they start as work arrives. Defaults to
4
+ * one less than the cores, between one and four. `0` runs on the calling
5
+ * thread, as does an environment without workers.
6
+ */
7
+ workers?: number;
8
+ /**
9
+ * The entry's wasm, when it is not next to this module where the pool looks
10
+ * for it: a URL, a `Response`, the bytes, or a compiled module.
11
+ */
12
+ wasm?: string | URL | Response | BufferSource | WebAssembly.Module;
13
+ }
14
+
15
+ export interface TaskOptions {
16
+ /**
17
+ * Hand the input's buffers to the worker instead of copying them, leaving
18
+ * them detached here. For buffers nothing else reads afterwards.
19
+ */
20
+ transfer?: boolean;
21
+ /**
22
+ * Abandons the task: the promise rejects with the signal's reason at once.
23
+ * A task a worker has already started still runs to its end, and its result
24
+ * is dropped; one not yet sent is never sent.
25
+ */
26
+ signal?: AbortSignal;
27
+ }
28
+
29
+ export interface Pool {
30
+ /** Compiles the wasm and starts one worker ahead of the first task. */
31
+ preload(): Promise<void>;
32
+ /** Changes the worker limit; idle workers past it stop now, busy ones when done. */
33
+ setWorkerLimit(workers: number): void;
34
+ readonly workerLimit: number;
35
+ /** Stops every worker and rejects what they held. */
36
+ dispose(): void;
37
+ }
38
+
39
+ export interface TaskPool extends Pool {
40
+ /** Runs the task `tasks.js` exports under `name` on a worker. */
41
+ run(name: string, args?: unknown[], options?: TaskOptions): Promise<any>;
42
+ }
43
+
44
+ export function createPool(options?: PoolOptions): TaskPool;