Skip to content

Commit 3125a6a

Browse files
fehnomenaltoddeTV
andauthored
feat: patch three-stdlib to allow loading gltf models with node (#15)
Co-authored-by: Thorsten Seyschab <business@todde.tv>
1 parent d0a4828 commit 3125a6a

File tree

4 files changed

+438
-11
lines changed

4 files changed

+438
-11
lines changed

.github/workflows/github-release-and-npm-package-publish.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,7 @@ jobs:
118118
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
119119

120120
- name: Install dependencies
121-
# TODO fix this bc currently using the frozen lockfile will fail due to the patch inside `package.json`
122-
# run: pnpm install --frozen-lockfile
123-
run: pnpm install --no-frozen-lockfile
121+
run: pnpm install --frozen-lockfile
124122

125123
- name: Build the project
126124
run: pnpm run build

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"@nuxt/schema": "^3",
122122
"@rspack/core": "^1",
123123
"astro": "*",
124+
"draco3d": "^1.5.7",
124125
"esbuild": "*",
125126
"rollup": "^3 || ^4",
126127
"three": ">=0.170.0",
@@ -168,8 +169,15 @@
168169
"esno": "~4.8.0",
169170
"fast-glob": "~3.3.3",
170171
"npm-run-all2": "~7.0.2",
172+
"three": "~0.172.0",
173+
"three-stdlib": "~2.35.12",
171174
"tsup": "~8.3.5",
172175
"typescript": "~5.7.3",
173176
"unplugin": "~2.1.2"
177+
},
178+
"pnpm": {
179+
"patchedDependencies": {
180+
"three-stdlib": "patches/three-stdlib.patch"
181+
}
174182
}
175183
}

patches/three-stdlib.patch

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
diff --git a/loaders/DRACOLoader.cjs b/loaders/DRACOLoader.cjs
2+
index d957dd8319f97b82f026bc4d8a2cbcf36c9bbb5e..a5561635127e0f0141f68356724e55f03d314de0 100644
3+
--- a/loaders/DRACOLoader.cjs
4+
+++ b/loaders/DRACOLoader.cjs
5+
@@ -1,5 +1,6 @@
6+
"use strict";
7+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8+
+const { createDecoderModule } = require("draco3d");
9+
const THREE = require("three");
10+
const _taskCache = /* @__PURE__ */ new WeakMap();
11+
class DRACOLoader extends THREE.Loader {
12+
@@ -8,7 +9,7 @@ class DRACOLoader extends THREE.Loader {
13+
this.decoderPath = "";
14+
this.decoderConfig = {};
15+
this.decoderBinary = null;
16+
- this.decoderPending = null;
17+
+ this.decoderPending = new DRACODecoder();
18+
this.workerLimit = 4;
19+
this.workerPool = [];
20+
this.workerNextTaskID = 1;
21+
@@ -88,13 +89,13 @@ class DRACOLoader extends THREE.Loader {
22+
let worker;
23+
const taskID = this.workerNextTaskID++;
24+
const taskCost = buffer.byteLength;
25+
- const geometryPending = this._getWorker(taskID, taskCost).then((_worker) => {
26+
- worker = _worker;
27+
- return new Promise((resolve, reject) => {
28+
- worker._callbacks[taskID] = { resolve, reject };
29+
- worker.postMessage({ type: "decode", id: taskID, taskConfig, buffer }, [buffer]);
30+
- });
31+
- }).then((message) => this._createGeometry(message.geometry));
32+
+ const geometryPending = this.decoderPending.onmessage({
33+
+ data: {
34+
+ type: 'decode',
35+
+ buffer,
36+
+ taskConfig,
37+
+ },
38+
+ }).then((geometry) => this._createGeometry(geometry));
39+
geometryPending.catch(() => true).then(() => {
40+
if (worker && taskID) {
41+
this._releaseTask(worker, taskID);
42+
@@ -130,7 +131,6 @@ class DRACOLoader extends THREE.Loader {
43+
});
44+
}
45+
preload() {
46+
- this._initDecoder();
47+
return this;
48+
}
49+
_initDecoder() {
50+
@@ -213,10 +213,12 @@ class DRACOLoader extends THREE.Loader {
51+
return this;
52+
}
53+
}
54+
-function DRACOWorker() {
55+
- let decoderConfig;
56+
- let decoderPending;
57+
- onmessage = function(e) {
58+
+class DRACODecoder {
59+
+ decoderModulePending;
60+
+ constructor() {
61+
+ this.decoderModulePending = createDecoderModule();
62+
+ }
63+
+ onmessage(e) {
64+
const message = e.data;
65+
switch (message.type) {
66+
case "init":
67+
@@ -231,20 +233,12 @@ function DRACOWorker() {
68+
case "decode":
69+
const buffer = message.buffer;
70+
const taskConfig = message.taskConfig;
71+
- decoderPending.then((module2) => {
72+
- const draco = module2.draco;
73+
+ return this.decoderModulePending.then((draco) => {
74+
const decoder = new draco.Decoder();
75+
const decoderBuffer = new draco.DecoderBuffer();
76+
decoderBuffer.Init(new Int8Array(buffer), buffer.byteLength);
77+
try {
78+
- const geometry = decodeGeometry(draco, decoder, decoderBuffer, taskConfig);
79+
- const buffers = geometry.attributes.map((attr) => attr.array.buffer);
80+
- if (geometry.index)
81+
- buffers.push(geometry.index.array.buffer);
82+
- self.postMessage({ type: "decode", id: message.id, geometry }, buffers);
83+
- } catch (error) {
84+
- console.error(error);
85+
- self.postMessage({ type: "error", id: message.id, error: error.message });
86+
+ return this.decodeGeometry(draco, decoder, decoderBuffer, taskConfig);
87+
} finally {
88+
draco.destroy(decoderBuffer);
89+
draco.destroy(decoder);
90+
@@ -253,7 +247,7 @@ function DRACOWorker() {
91+
break;
92+
}
93+
};
94+
- function decodeGeometry(draco, decoder, decoderBuffer, taskConfig) {
95+
+ decodeGeometry(draco, decoder, decoderBuffer, taskConfig) {
96+
const attributeIDs = taskConfig.attributeIDs;
97+
const attributeTypes = taskConfig.attributeTypes;
98+
let dracoGeometry;
99+
@@ -273,7 +267,7 @@ function DRACOWorker() {
100+
}
101+
const geometry = { index: null, attributes: [] };
102+
for (const attributeName in attributeIDs) {
103+
- const attributeType = self[attributeTypes[attributeName]];
104+
+ const attributeType = global[attributeTypes[attributeName]];
105+
let attribute;
106+
let attributeID;
107+
if (taskConfig.useUniqueIDs) {
108+
@@ -285,15 +279,15 @@ function DRACOWorker() {
109+
continue;
110+
attribute = decoder.GetAttribute(dracoGeometry, attributeID);
111+
}
112+
- geometry.attributes.push(decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute));
113+
+ geometry.attributes.push(this.decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute));
114+
}
115+
if (geometryType === draco.TRIANGULAR_MESH) {
116+
- geometry.index = decodeIndex(draco, decoder, dracoGeometry);
117+
+ geometry.index = this.decodeIndex(draco, decoder, dracoGeometry);
118+
}
119+
draco.destroy(dracoGeometry);
120+
return geometry;
121+
}
122+
- function decodeIndex(draco, decoder, dracoGeometry) {
123+
+ decodeIndex(draco, decoder, dracoGeometry) {
124+
const numFaces = dracoGeometry.num_faces();
125+
const numIndices = numFaces * 3;
126+
const byteLength = numIndices * 4;
127+
@@ -303,12 +297,12 @@ function DRACOWorker() {
128+
draco._free(ptr);
129+
return { array: index, itemSize: 1 };
130+
}
131+
- function decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute) {
132+
+ decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute) {
133+
const numComponents = attribute.num_components();
134+
const numPoints = dracoGeometry.num_points();
135+
const numValues = numPoints * numComponents;
136+
const byteLength = numValues * attributeType.BYTES_PER_ELEMENT;
137+
- const dataType = getDracoDataType(draco, attributeType);
138+
+ const dataType = this.getDracoDataType(draco, attributeType);
139+
const ptr = draco._malloc(byteLength);
140+
decoder.GetAttributeDataArrayForAllPoints(dracoGeometry, attribute, dataType, byteLength, ptr);
141+
const array = new attributeType(draco.HEAPF32.buffer, ptr, numValues).slice();
142+
@@ -319,7 +313,7 @@ function DRACOWorker() {
143+
itemSize: numComponents
144+
};
145+
}
146+
- function getDracoDataType(draco, attributeType) {
147+
+ getDracoDataType(draco, attributeType) {
148+
switch (attributeType) {
149+
case Float32Array:
150+
return draco.DT_FLOAT32;
151+
diff --git a/loaders/DRACOLoader.js b/loaders/DRACOLoader.js
152+
index 43ca01558bbf0fffbfcebe85c2eabd962537a94e..b3ea813413c2fde835bd9d630b12113c35dda72a 100644
153+
--- a/loaders/DRACOLoader.js
154+
+++ b/loaders/DRACOLoader.js
155+
@@ -1,3 +1,4 @@
156+
+import { createDecoderModule } from "draco3d";
157+
import { Loader, FileLoader, BufferGeometry, BufferAttribute } from "three";
158+
const _taskCache = /* @__PURE__ */ new WeakMap();
159+
class DRACOLoader extends Loader {
160+
@@ -6,7 +7,7 @@ class DRACOLoader extends Loader {
161+
this.decoderPath = "";
162+
this.decoderConfig = {};
163+
this.decoderBinary = null;
164+
- this.decoderPending = null;
165+
+ this.decoderPending = new DRACODecoder();
166+
this.workerLimit = 4;
167+
this.workerPool = [];
168+
this.workerNextTaskID = 1;
169+
@@ -86,13 +87,13 @@ class DRACOLoader extends Loader {
170+
let worker;
171+
const taskID = this.workerNextTaskID++;
172+
const taskCost = buffer.byteLength;
173+
- const geometryPending = this._getWorker(taskID, taskCost).then((_worker) => {
174+
- worker = _worker;
175+
- return new Promise((resolve, reject) => {
176+
- worker._callbacks[taskID] = { resolve, reject };
177+
- worker.postMessage({ type: "decode", id: taskID, taskConfig, buffer }, [buffer]);
178+
- });
179+
- }).then((message) => this._createGeometry(message.geometry));
180+
+ const geometryPending = this.decoderPending.onmessage({
181+
+ data: {
182+
+ type: 'decode',
183+
+ buffer,
184+
+ taskConfig,
185+
+ },
186+
+ }).then((geometry) => this._createGeometry(geometry));
187+
geometryPending.catch(() => true).then(() => {
188+
if (worker && taskID) {
189+
this._releaseTask(worker, taskID);
190+
@@ -128,7 +129,6 @@ class DRACOLoader extends Loader {
191+
});
192+
}
193+
preload() {
194+
- this._initDecoder();
195+
return this;
196+
}
197+
_initDecoder() {
198+
@@ -211,10 +211,12 @@ class DRACOLoader extends Loader {
199+
return this;
200+
}
201+
}
202+
-function DRACOWorker() {
203+
- let decoderConfig;
204+
- let decoderPending;
205+
- onmessage = function(e) {
206+
+class DRACODecoder {
207+
+ decoderModulePending;
208+
+ constructor() {
209+
+ this.decoderModulePending = createDecoderModule();
210+
+ }
211+
+ onmessage(e) {
212+
const message = e.data;
213+
switch (message.type) {
214+
case "init":
215+
@@ -229,20 +231,12 @@ function DRACOWorker() {
216+
case "decode":
217+
const buffer = message.buffer;
218+
const taskConfig = message.taskConfig;
219+
- decoderPending.then((module) => {
220+
- const draco = module.draco;
221+
+ return this.decoderModulePending.then((draco) => {
222+
const decoder = new draco.Decoder();
223+
const decoderBuffer = new draco.DecoderBuffer();
224+
decoderBuffer.Init(new Int8Array(buffer), buffer.byteLength);
225+
try {
226+
- const geometry = decodeGeometry(draco, decoder, decoderBuffer, taskConfig);
227+
- const buffers = geometry.attributes.map((attr) => attr.array.buffer);
228+
- if (geometry.index)
229+
- buffers.push(geometry.index.array.buffer);
230+
- self.postMessage({ type: "decode", id: message.id, geometry }, buffers);
231+
- } catch (error) {
232+
- console.error(error);
233+
- self.postMessage({ type: "error", id: message.id, error: error.message });
234+
+ return this.decodeGeometry(draco, decoder, decoderBuffer, taskConfig);
235+
} finally {
236+
draco.destroy(decoderBuffer);
237+
draco.destroy(decoder);
238+
@@ -251,7 +245,7 @@ function DRACOWorker() {
239+
break;
240+
}
241+
};
242+
- function decodeGeometry(draco, decoder, decoderBuffer, taskConfig) {
243+
+ decodeGeometry(draco, decoder, decoderBuffer, taskConfig) {
244+
const attributeIDs = taskConfig.attributeIDs;
245+
const attributeTypes = taskConfig.attributeTypes;
246+
let dracoGeometry;
247+
@@ -271,7 +265,7 @@ function DRACOWorker() {
248+
}
249+
const geometry = { index: null, attributes: [] };
250+
for (const attributeName in attributeIDs) {
251+
- const attributeType = self[attributeTypes[attributeName]];
252+
+ const attributeType = global[attributeTypes[attributeName]];
253+
let attribute;
254+
let attributeID;
255+
if (taskConfig.useUniqueIDs) {
256+
@@ -283,15 +277,15 @@ function DRACOWorker() {
257+
continue;
258+
attribute = decoder.GetAttribute(dracoGeometry, attributeID);
259+
}
260+
- geometry.attributes.push(decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute));
261+
+ geometry.attributes.push(this.decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute));
262+
}
263+
if (geometryType === draco.TRIANGULAR_MESH) {
264+
- geometry.index = decodeIndex(draco, decoder, dracoGeometry);
265+
+ geometry.index = this.decodeIndex(draco, decoder, dracoGeometry);
266+
}
267+
draco.destroy(dracoGeometry);
268+
return geometry;
269+
}
270+
- function decodeIndex(draco, decoder, dracoGeometry) {
271+
+ decodeIndex(draco, decoder, dracoGeometry) {
272+
const numFaces = dracoGeometry.num_faces();
273+
const numIndices = numFaces * 3;
274+
const byteLength = numIndices * 4;
275+
@@ -301,12 +295,12 @@ function DRACOWorker() {
276+
draco._free(ptr);
277+
return { array: index, itemSize: 1 };
278+
}
279+
- function decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute) {
280+
+ decodeAttribute(draco, decoder, dracoGeometry, attributeName, attributeType, attribute) {
281+
const numComponents = attribute.num_components();
282+
const numPoints = dracoGeometry.num_points();
283+
const numValues = numPoints * numComponents;
284+
const byteLength = numValues * attributeType.BYTES_PER_ELEMENT;
285+
- const dataType = getDracoDataType(draco, attributeType);
286+
+ const dataType = this.getDracoDataType(draco, attributeType);
287+
const ptr = draco._malloc(byteLength);
288+
decoder.GetAttributeDataArrayForAllPoints(dracoGeometry, attribute, dataType, byteLength, ptr);
289+
const array = new attributeType(draco.HEAPF32.buffer, ptr, numValues).slice();
290+
@@ -317,7 +311,7 @@ function DRACOWorker() {
291+
itemSize: numComponents
292+
};
293+
}
294+
- function getDracoDataType(draco, attributeType) {
295+
+ getDracoDataType(draco, attributeType) {
296+
switch (attributeType) {
297+
case Float32Array:
298+
return draco.DT_FLOAT32;
299+
diff --git a/loaders/GLTFLoader.cjs b/loaders/GLTFLoader.cjs
300+
index 15aba5027f97e93b048e9738b45ea8b7e2193b0d..30f8e250a35f65eb5e4789d5a27f8f40beeee56d 100644
301+
--- a/loaders/GLTFLoader.cjs
302+
+++ b/loaders/GLTFLoader.cjs
303+
@@ -1,5 +1,7 @@
304+
"use strict";
305+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
306+
+const { readFile } = require("node:fs/promises");
307+
+const { join } = require("node:path");
308+
const THREE = require("three");
309+
const BufferGeometryUtils = require("../utils/BufferGeometryUtils.cjs");
310+
const constants = require("../_polyfill/constants.cjs");
311+
@@ -1632,6 +1634,7 @@ class GLTFParser {
312+
if (bufferDef.uri === void 0 && bufferIndex === 0) {
313+
return Promise.resolve(this.extensions[EXTENSIONS.KHR_BINARY_GLTF].body);
314+
}
315+
+ return readFile(join(this.options.path, bufferDef.uri));
316+
const options = this.options;
317+
return new Promise(function(resolve, reject) {
318+
loader.load(THREE.LoaderUtils.resolveURL(bufferDef.uri, options.path), resolve, void 0, function() {
319+
@@ -1794,6 +1797,7 @@ class GLTFParser {
320+
return promise;
321+
}
322+
loadImageSource(sourceIndex, loader) {
323+
+ return Promise.resolve(null);
324+
const parser = this;
325+
const json = this.json;
326+
const options = this.options;
327+
diff --git a/loaders/GLTFLoader.js b/loaders/GLTFLoader.js
328+
index 851226bb4c491b990459cae65009113035f200cc..f1dd2991c9d88adf6ae1b6bfff5f85350d6b891e 100644
329+
--- a/loaders/GLTFLoader.js
330+
+++ b/loaders/GLTFLoader.js
331+
@@ -1,3 +1,5 @@
332+
+import { readFile } from "node:fs/promises";
333+
+import { join } from "node:path";
334+
import { Loader, LoaderUtils, FileLoader, Color, SpotLight, PointLight, DirectionalLight, MeshBasicMaterial, MeshPhysicalMaterial, Vector2, Matrix4, Vector3, Quaternion, InstancedMesh, InstancedBufferAttribute, Object3D, TextureLoader, ImageBitmapLoader, BufferAttribute, InterleavedBuffer, InterleavedBufferAttribute, LinearFilter, LinearMipmapLinearFilter, RepeatWrapping, PointsMaterial, Material, LineBasicMaterial, MeshStandardMaterial, DoubleSide, PropertyBinding, BufferGeometry, SkinnedMesh, Mesh, TriangleStripDrawMode, TriangleFanDrawMode, LineSegments, Line, LineLoop, Points, Group, PerspectiveCamera, MathUtils, OrthographicCamera, Skeleton, AnimationClip, Bone, InterpolateLinear, NearestFilter, NearestMipmapNearestFilter, LinearMipmapNearestFilter, NearestMipmapLinearFilter, ClampToEdgeWrapping, MirroredRepeatWrapping, InterpolateDiscrete, FrontSide, Texture, VectorKeyframeTrack, NumberKeyframeTrack, QuaternionKeyframeTrack, Box3, Sphere, Interpolant } from "three";
335+
import { toTrianglesDrawMode } from "../utils/BufferGeometryUtils.js";
336+
import { version } from "../_polyfill/constants.js";
337+
@@ -1630,6 +1632,7 @@ class GLTFParser {
338+
if (bufferDef.uri === void 0 && bufferIndex === 0) {
339+
return Promise.resolve(this.extensions[EXTENSIONS.KHR_BINARY_GLTF].body);
340+
}
341+
+ return readFile(join(this.options.path, bufferDef.uri));
342+
const options = this.options;
343+
return new Promise(function(resolve, reject) {
344+
loader.load(LoaderUtils.resolveURL(bufferDef.uri, options.path), resolve, void 0, function() {
345+
@@ -1792,6 +1795,7 @@ class GLTFParser {
346+
return promise;
347+
}
348+
loadImageSource(sourceIndex, loader) {
349+
+ return Promise.resolve(null);
350+
const parser = this;
351+
const json = this.json;
352+
const options = this.options;

0 commit comments

Comments
 (0)