@draco-rust/decoder 0.1.2 → 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/README.md CHANGED
@@ -85,36 +85,36 @@ webpack do, emits the worker on its own.
85
85
 
86
86
  ### With three.js
87
87
 
88
- `createDracoLoader` stands in for three.js's `DRACOLoader`, on the pool. It
89
- takes three's classes rather than importing three, so it builds with your own
90
- copy:
88
+ `@draco-rust/decoder/three` exports `DRACOLoader`: three.js's own
89
+ `DRACOLoader`, decoding on this package's workers instead of the Draco decoder
90
+ three fetches with `setDecoderPath`. Everything else is three's class, so it
91
+ goes wherever one is expected, TypeScript included:
91
92
 
92
93
  ```js
93
- import * as THREE from "three";
94
94
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
95
- import { createDracoLoader } from "@draco-rust/decoder/mesh/three";
95
+ import { DRACOLoader } from "@draco-rust/decoder/mesh/three";
96
96
 
97
- const gltfLoader = new GLTFLoader();
98
- gltfLoader.setDRACOLoader(createDracoLoader(THREE));
97
+ const gltfLoader = new GLTFLoader().setDRACOLoader(new DRACOLoader());
99
98
  const gltf = await gltfLoader.loadAsync("model.glb");
100
99
  ```
101
100
 
102
- For a `.drc` file, `await loader.parseAsync(arrayBuffer)` returns a
103
- `BufferGeometry`, and `parse(buffer, onLoad, onError)` works as DRACOLoader's
104
- does. Vertex colours from a `.drc` file are converted from sRGB, as
105
- DRACOLoader converts them, and an attribute whose items are not a multiple of
106
- four bytes is padded and interleaved, as DRACOLoader pads it since r181. The
107
- geometry is the one DRACOLoader builds, byte for byte, with one difference: an
108
- attribute that does not convert to the type asked for -- a negative value
109
- into a `Uint16Array`, say -- is an error here, where DRACOLoader fills the
110
- attribute with whatever its failed conversion left in memory. `setWorkerLimit`,
111
- `preload` and `dispose` are there too. In TypeScript, `setDRACOLoader` expects the `DRACOLoader` class, so cast
112
- the loader: `createDracoLoader(THREE) as unknown as DRACOLoader`.
101
+ `load`, `parse`, `setWorkerLimit`, `preload` and `dispose` work as three's do,
102
+ and so does the geometry: three builds it, so vertex colours, normalization and
103
+ the padding r181 added follow the three you have. Byte for byte it is the
104
+ geometry three's own loader builds, with one difference: an attribute that
105
+ does not convert to the type asked for -- a negative value into a
106
+ `Uint16Array`, say -- is an error here, where three's loader fills the
107
+ attribute with whatever its failed conversion left in memory.
108
+
109
+ The constructor takes three's `LoadingManager` and then the pool's options,
110
+ `new DRACOLoader(manager, { workers: 2 })`, or `{ pool }` to share one pool
111
+ between loaders. `three` is a peer dependency, from r150 (`0.150.0`); the
112
+ package's other entries do not need it.
113
113
 
114
114
  ### From a CDN, without a bundler
115
115
 
116
116
  ```js
117
- import init, { parse_drc_bytes } from "https://cdn.jsdelivr.net/npm/@draco-rust/decoder@0.1/mesh/index.js";
117
+ import init, { parse_drc_bytes } from "https://cdn.jsdelivr.net/npm/@draco-rust/decoder@0.2/mesh/index.js";
118
118
  await init(); // finds index_bg.wasm next to the script
119
119
  ```
120
120
 
@@ -141,4 +141,4 @@ For a subpath entry, resolve its own file, for example
141
141
  - Licensed under Apache-2.0, like Draco.
142
142
  ## This build
143
143
 
144
- `@draco-rust/decoder@0.1.2` was built from draco-rust commit `6bc95d5c`, with draco-core 2.3.2, draco-io 0.6.0, draco-gltf 0.7.0.
144
+ `@draco-rust/decoder@0.2.0` was built from draco-rust commit `8650e1ca`, with draco-core 2.3.2, draco-io 0.6.0, draco-gltf 0.7.0.
package/legacy/three.d.ts CHANGED
@@ -1,65 +1,19 @@
1
- import type { DracoArrayType } from './index.js';
1
+ import type { LoadingManager } from 'three';
2
+ import { DRACOLoader as ThreeDRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
2
3
  import type { DecoderPool, DecoderPoolOptions } from './pool.js';
3
4
 
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 {
5
+ export interface DRACOLoaderOptions extends DecoderPoolOptions {
25
6
  /** A pool to share with other loaders, instead of one of the loader's own. */
26
7
  pool?: DecoderPool;
27
8
  }
28
9
 
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
10
  /**
59
- * A stand-in for three.js's DRACOLoader on the decoder pool:
60
- * `gltfLoader.setDRACOLoader(createDracoLoader(THREE))`.
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())`.
61
15
  */
62
- export function createDracoLoader<Geometry>(
63
- three: ThreeClasses<Geometry>,
64
- options?: DracoLoaderOptions,
65
- ): DracoLoader<Geometry>;
16
+ export class DRACOLoader extends ThreeDRACOLoader {
17
+ constructor(manager?: LoadingManager, options?: DRACOLoaderOptions);
18
+ readonly pool: DecoderPool;
19
+ }
package/legacy/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/three.d.ts CHANGED
@@ -1,65 +1,19 @@
1
- import type { DracoArrayType } from './index.js';
1
+ import type { LoadingManager } from 'three';
2
+ import { DRACOLoader as ThreeDRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
2
3
  import type { DecoderPool, DecoderPoolOptions } from './pool.js';
3
4
 
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 {
5
+ export interface DRACOLoaderOptions extends DecoderPoolOptions {
25
6
  /** A pool to share with other loaders, instead of one of the loader's own. */
26
7
  pool?: DecoderPool;
27
8
  }
28
9
 
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
10
  /**
59
- * A stand-in for three.js's DRACOLoader on the decoder pool:
60
- * `gltfLoader.setDRACOLoader(createDracoLoader(THREE))`.
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())`.
61
15
  */
62
- export function createDracoLoader<Geometry>(
63
- three: ThreeClasses<Geometry>,
64
- options?: DracoLoaderOptions,
65
- ): DracoLoader<Geometry>;
16
+ export class DRACOLoader extends ThreeDRACOLoader {
17
+ constructor(manager?: LoadingManager, options?: DRACOLoaderOptions);
18
+ readonly pool: DecoderPool;
19
+ }
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draco-rust/decoder",
3
- "version": "0.1.2",
3
+ "version": "0.2.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": "8650e1ca",
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
  }
@@ -1,65 +1,19 @@
1
- import type { DracoArrayType } from './index.js';
1
+ import type { LoadingManager } from 'three';
2
+ import { DRACOLoader as ThreeDRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
2
3
  import type { DecoderPool, DecoderPoolOptions } from './pool.js';
3
4
 
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 {
5
+ export interface DRACOLoaderOptions extends DecoderPoolOptions {
25
6
  /** A pool to share with other loaders, instead of one of the loader's own. */
26
7
  pool?: DecoderPool;
27
8
  }
28
9
 
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
10
  /**
59
- * A stand-in for three.js's DRACOLoader on the decoder pool:
60
- * `gltfLoader.setDRACOLoader(createDracoLoader(THREE))`.
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())`.
61
15
  */
62
- export function createDracoLoader<Geometry>(
63
- three: ThreeClasses<Geometry>,
64
- options?: DracoLoaderOptions,
65
- ): DracoLoader<Geometry>;
16
+ export class DRACOLoader extends ThreeDRACOLoader {
17
+ constructor(manager?: LoadingManager, options?: DRACOLoaderOptions);
18
+ readonly pool: DecoderPool;
19
+ }
@@ -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/three.d.ts CHANGED
@@ -1,65 +1,19 @@
1
- import type { DracoArrayType } from './index.js';
1
+ import type { LoadingManager } from 'three';
2
+ import { DRACOLoader as ThreeDRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
2
3
  import type { DecoderPool, DecoderPoolOptions } from './pool.js';
3
4
 
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 {
5
+ export interface DRACOLoaderOptions extends DecoderPoolOptions {
25
6
  /** A pool to share with other loaders, instead of one of the loader's own. */
26
7
  pool?: DecoderPool;
27
8
  }
28
9
 
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
10
  /**
59
- * A stand-in for three.js's DRACOLoader on the decoder pool:
60
- * `gltfLoader.setDRACOLoader(createDracoLoader(THREE))`.
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())`.
61
15
  */
62
- export function createDracoLoader<Geometry>(
63
- three: ThreeClasses<Geometry>,
64
- options?: DracoLoaderOptions,
65
- ): DracoLoader<Geometry>;
16
+ export class DRACOLoader extends ThreeDRACOLoader {
17
+ constructor(manager?: LoadingManager, options?: DRACOLoaderOptions);
18
+ readonly pool: DecoderPool;
19
+ }
package/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
  }