@gjsify/example-dom-three-loader-ldraw 0.1.7 → 0.38.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 +78 -0
- package/dist/browser.js +8628 -33688
- package/dist/gjs.js +3967 -47953
- package/dist/gjs.node.mjs +4381 -0
- package/dist/index.html +1 -0
- package/dist/webgl.css +7 -32
- package/package.json +51 -21
- package/src/assets/models/ldraw/officialLibrary/CAlicense.txt +248 -0
- package/src/assets/models/ldraw/officialLibrary/CAreadme.txt +81 -0
- package/src/assets/models/ldraw/officialLibrary/Readme.txt +125 -0
- package/src/assets/models/ldraw/officialLibrary/models/10174-1-ImperialAT-ST-UCS.mpd_Packed.mpd +32672 -0
- package/src/assets/models/ldraw/officialLibrary/models/1621-1-LunarMPVVehicle.mpd_Packed.mpd +12465 -0
- package/src/assets/models/ldraw/officialLibrary/models/30023-1-Lighthouse.ldr_Packed.mpd +4364 -0
- package/src/assets/models/ldraw/officialLibrary/models/30051-1-X-wingFighter-Mini.mpd_Packed.mpd +4500 -0
- package/src/assets/models/ldraw/officialLibrary/models/30054-1-AT-ST-Mini.mpd_Packed.mpd +6449 -0
- package/src/assets/models/ldraw/officialLibrary/models/4489-1-AT-AT-Mini.mpd_Packed.mpd +7242 -0
- package/src/assets/models/ldraw/officialLibrary/models/4494-1-Imperial Shuttle-Mini.mpd_Packed.mpd +5530 -0
- package/src/assets/models/ldraw/officialLibrary/models/4838-1-MiniVehicles.mpd_Packed.mpd +4880 -0
- package/src/assets/models/ldraw/officialLibrary/models/4915-1-MiniConstruction.mpd_Packed.mpd +6481 -0
- package/src/assets/models/ldraw/officialLibrary/models/4918-1-MiniFlyers.mpd_Packed.mpd +5518 -0
- package/src/assets/models/ldraw/officialLibrary/models/5935-1-IslandHopper.mpd_Packed.mpd +17313 -0
- package/src/assets/models/ldraw/officialLibrary/models/6965-1-TIEIntercep_4h4MXk5.mpd_Packed.mpd +2839 -0
- package/src/assets/models/ldraw/officialLibrary/models/6966-1-JediStarfighter-Mini.mpd_Packed.mpd +4087 -0
- package/src/assets/models/ldraw/officialLibrary/models/7140-1-X-wingFighter.mpd_Packed.mpd +28548 -0
- package/src/assets/models/ldraw/officialLibrary/models/889-1-RadarTruck.mpd_Packed.mpd +8429 -0
- package/src/assets/models/ldraw/officialLibrary/models/car.ldr_Packed.mpd +5969 -0
- package/src/browser/browser-main.ts +4 -0
- package/src/browser/browser.ts +199 -0
- package/src/browser/index.html +11 -0
- package/src/browser/webgl.css +28 -0
- package/src/gjs/gjs.ts +31 -0
- package/src/gjs/ldraw-window.blp +87 -0
- package/src/gjs/ldraw-window.ts +132 -0
- package/src/models.ts +28 -0
- package/src/three-demo.ts +242 -0
- package/dist/adwaita-sans-400-VCQV646T.ttf +0 -0
- package/dist/browser.css +0 -10
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Ported from refs/three/examples/webgl_loader_ldraw.html
|
|
2
|
+
// Original: MIT license, three.js authors (https://threejs.org)
|
|
3
|
+
// This software uses the LDraw Parts Library (http://www.ldraw.org), CC BY 2.0.
|
|
4
|
+
// Reimplemented for GJS and browser targets using @gjsify packages.
|
|
5
|
+
|
|
6
|
+
import * as THREE from 'three';
|
|
7
|
+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
8
|
+
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
|
9
|
+
import { LDrawLoader } from 'three/addons/loaders/LDrawLoader.js';
|
|
10
|
+
import { LDrawUtils } from 'three/addons/utils/LDrawUtils.js';
|
|
11
|
+
import { LDrawConditionalLineMaterial } from 'three/addons/materials/LDrawConditionalLineMaterial.js';
|
|
12
|
+
import { MODEL_LIST, DEFAULT_MODEL_INDEX, type ModelEntry } from './models.js';
|
|
13
|
+
|
|
14
|
+
export { MODEL_LIST, DEFAULT_MODEL_INDEX, type ModelEntry };
|
|
15
|
+
|
|
16
|
+
export interface LDrawEffectController {
|
|
17
|
+
modelIndex: number;
|
|
18
|
+
flatColors: boolean;
|
|
19
|
+
mergeModel: boolean;
|
|
20
|
+
buildingStep: number;
|
|
21
|
+
smoothNormals: boolean;
|
|
22
|
+
displayLines: boolean;
|
|
23
|
+
conditionalLines: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LDrawDemo {
|
|
27
|
+
readonly effectController: LDrawEffectController;
|
|
28
|
+
reloadObject(resetCamera: boolean): void;
|
|
29
|
+
updateVisibility(): void;
|
|
30
|
+
numBuildingSteps: number;
|
|
31
|
+
/** Stop the render loop — the website embed calls this when it scrolls out of view. */
|
|
32
|
+
pause(): void;
|
|
33
|
+
/** Restart the render loop after `pause()`. */
|
|
34
|
+
resume(): void;
|
|
35
|
+
readonly isPaused: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StartOptions {
|
|
39
|
+
assetBase?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type OnModelLoaded = (numBuildingSteps: number) => void;
|
|
43
|
+
|
|
44
|
+
export function start(canvas: HTMLCanvasElement, options?: StartOptions, onModelLoaded?: OnModelLoaded): LDrawDemo {
|
|
45
|
+
const assetBase = options?.assetBase ?? './';
|
|
46
|
+
const ldrawPath = `${assetBase}assets/models/ldraw/officialLibrary/`;
|
|
47
|
+
|
|
48
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- THREE.WebGLRenderer canvas option expects OffscreenCanvas; GJS canvas type is incompatible
|
|
49
|
+
const renderer = new THREE.WebGLRenderer({ canvas: canvas as any, antialias: true });
|
|
50
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
51
|
+
// Pass updateStyle=false: the canvas CSS width/height are owned by the host
|
|
52
|
+
// container (GTK allocation / browser flex layout), we only size the
|
|
53
|
+
// drawing buffer.
|
|
54
|
+
renderer.setSize(canvas.width, canvas.height, false);
|
|
55
|
+
// The last size handed to the renderer, tracked separately because `renderer.domElement` IS this
|
|
56
|
+
// canvas: the obvious `renderer.domElement.width !== w` compares a value with itself, leaving the
|
|
57
|
+
// resize branch dead and the camera aspect frozen at the first allocation.
|
|
58
|
+
let prevW = canvas.width;
|
|
59
|
+
let prevH = canvas.height;
|
|
60
|
+
|
|
61
|
+
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
|
62
|
+
const scene = new THREE.Scene();
|
|
63
|
+
scene.background = new THREE.Color(0xdeebed);
|
|
64
|
+
scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
65
|
+
|
|
66
|
+
const camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 1, 10000);
|
|
67
|
+
camera.position.set(150, 200, 250);
|
|
68
|
+
|
|
69
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- OrbitControls domElement type is HTMLElement; GJS canvas type is incompatible
|
|
70
|
+
const controls = new OrbitControls(camera, canvas as any);
|
|
71
|
+
controls.enableDamping = true;
|
|
72
|
+
|
|
73
|
+
let model: THREE.Group | null = null;
|
|
74
|
+
|
|
75
|
+
const effectController: LDrawEffectController = {
|
|
76
|
+
modelIndex: DEFAULT_MODEL_INDEX,
|
|
77
|
+
flatColors: false,
|
|
78
|
+
mergeModel: false,
|
|
79
|
+
buildingStep: 0,
|
|
80
|
+
smoothNormals: true,
|
|
81
|
+
displayLines: true,
|
|
82
|
+
conditionalLines: true,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
let numBuildingSteps = 0;
|
|
86
|
+
|
|
87
|
+
function updateObjectsVisibility() {
|
|
88
|
+
if (!model) return;
|
|
89
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- THREE.Object3D.traverse callback; LDraw-specific properties (isConditionalLine, buildingStep) not in THREE types
|
|
90
|
+
model.traverse((c: any) => {
|
|
91
|
+
if (c.isLineSegments) {
|
|
92
|
+
if (c.isConditionalLine) {
|
|
93
|
+
c.visible = effectController.conditionalLines;
|
|
94
|
+
} else {
|
|
95
|
+
c.visible = effectController.displayLines;
|
|
96
|
+
}
|
|
97
|
+
} else if (c.isGroup) {
|
|
98
|
+
c.visible = c.userData.buildingStep <= effectController.buildingStep;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function reloadObject(resetCamera: boolean) {
|
|
104
|
+
if (model) {
|
|
105
|
+
scene.remove(model);
|
|
106
|
+
model = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const entry = MODEL_LIST[effectController.modelIndex];
|
|
110
|
+
if (!entry) return;
|
|
111
|
+
|
|
112
|
+
const lDrawLoader = new LDrawLoader();
|
|
113
|
+
lDrawLoader.setConditionalLineMaterial(LDrawConditionalLineMaterial);
|
|
114
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- LDrawLoader.smoothNormals is a runtime property not in the three.js TypeScript types
|
|
115
|
+
(lDrawLoader as any).smoothNormals = effectController.smoothNormals && !effectController.flatColors;
|
|
116
|
+
lDrawLoader.setPath(ldrawPath);
|
|
117
|
+
|
|
118
|
+
lDrawLoader.load(
|
|
119
|
+
entry.file,
|
|
120
|
+
(group2: THREE.Group) => {
|
|
121
|
+
if (model) scene.remove(model);
|
|
122
|
+
|
|
123
|
+
model = group2;
|
|
124
|
+
|
|
125
|
+
// Flat colors: convert to MeshBasicMaterial (LEGO instruction look)
|
|
126
|
+
if (effectController.flatColors) {
|
|
127
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- THREE.Object3D.traverse callback; LDraw-specific isMesh/material not guaranteed typed
|
|
128
|
+
model.traverse((c: any) => {
|
|
129
|
+
if (c.isMesh) {
|
|
130
|
+
if (Array.isArray(c.material)) {
|
|
131
|
+
c.material = c.material.map(convertMaterial);
|
|
132
|
+
} else {
|
|
133
|
+
c.material = convertMaterial(c.material);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (effectController.mergeModel) model = LDrawUtils.mergeObject(model);
|
|
140
|
+
|
|
141
|
+
// Convert from LDraw coordinates: rotate 180 degrees around OX
|
|
142
|
+
model.rotation.x = Math.PI;
|
|
143
|
+
scene.add(model);
|
|
144
|
+
|
|
145
|
+
numBuildingSteps = ((model.userData as Record<string, unknown>).numBuildingSteps as number) ?? 1;
|
|
146
|
+
effectController.buildingStep = numBuildingSteps - 1;
|
|
147
|
+
|
|
148
|
+
updateObjectsVisibility();
|
|
149
|
+
|
|
150
|
+
const bbox = new THREE.Box3().setFromObject(model);
|
|
151
|
+
const size = bbox.getSize(new THREE.Vector3());
|
|
152
|
+
const radius = Math.max(size.x, Math.max(size.y, size.z)) * 0.5;
|
|
153
|
+
|
|
154
|
+
if (resetCamera) {
|
|
155
|
+
controls.target0.copy(bbox.getCenter(new THREE.Vector3()));
|
|
156
|
+
controls.position0.set(-2.3, 1, 2).multiplyScalar(radius).add(controls.target0);
|
|
157
|
+
controls.reset();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
onModelLoaded?.(numBuildingSteps);
|
|
161
|
+
},
|
|
162
|
+
undefined,
|
|
163
|
+
(error: unknown) => {
|
|
164
|
+
// The onError handler is load-bearing: LDrawLoader routes failures into a promise
|
|
165
|
+
// chain, so without it a failed model is a silent rejection and the viewer just shows
|
|
166
|
+
// the empty background.
|
|
167
|
+
console.error(`[ldraw] loading ${ldrawPath}${entry.file} failed:`, error);
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function convertMaterial(material: THREE.Material): THREE.MeshBasicMaterial {
|
|
173
|
+
const m = material as THREE.MeshStandardMaterial;
|
|
174
|
+
const newMat = new THREE.MeshBasicMaterial();
|
|
175
|
+
newMat.color.copy(m.color);
|
|
176
|
+
newMat.polygonOffset = m.polygonOffset;
|
|
177
|
+
newMat.polygonOffsetUnits = m.polygonOffsetUnits;
|
|
178
|
+
newMat.polygonOffsetFactor = m.polygonOffsetFactor;
|
|
179
|
+
newMat.opacity = m.opacity;
|
|
180
|
+
newMat.transparent = m.transparent;
|
|
181
|
+
newMat.depthWrite = m.depthWrite;
|
|
182
|
+
newMat.toneMapped = false;
|
|
183
|
+
return newMat;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function animate() {
|
|
187
|
+
controls.update();
|
|
188
|
+
|
|
189
|
+
// canvas.width/height are the host-owned drawing buffer: GTK allocation on GJS, a parent
|
|
190
|
+
// ResizeObserver in the browser.
|
|
191
|
+
const w = canvas.width;
|
|
192
|
+
const h = canvas.height;
|
|
193
|
+
if (w !== prevW || h !== prevH) {
|
|
194
|
+
prevW = w;
|
|
195
|
+
prevH = h;
|
|
196
|
+
renderer.setSize(w, h, false);
|
|
197
|
+
camera.aspect = w / h;
|
|
198
|
+
camera.updateProjectionMatrix();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
renderer.render(scene, camera);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// requestAnimationFrame directly, which maps onto the GTK frame clock.
|
|
205
|
+
let animPending = false;
|
|
206
|
+
let paused = false;
|
|
207
|
+
function scheduleFrame() {
|
|
208
|
+
if (animPending || paused) return;
|
|
209
|
+
animPending = true;
|
|
210
|
+
requestAnimationFrame(() => {
|
|
211
|
+
animPending = false;
|
|
212
|
+
animate();
|
|
213
|
+
scheduleFrame();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
scheduleFrame();
|
|
217
|
+
|
|
218
|
+
reloadObject(true);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
effectController,
|
|
222
|
+
reloadObject,
|
|
223
|
+
updateVisibility: updateObjectsVisibility,
|
|
224
|
+
pause() {
|
|
225
|
+
paused = true;
|
|
226
|
+
},
|
|
227
|
+
resume() {
|
|
228
|
+
if (!paused) return;
|
|
229
|
+
paused = false;
|
|
230
|
+
scheduleFrame();
|
|
231
|
+
},
|
|
232
|
+
get isPaused() {
|
|
233
|
+
return paused;
|
|
234
|
+
},
|
|
235
|
+
get numBuildingSteps() {
|
|
236
|
+
return numBuildingSteps;
|
|
237
|
+
},
|
|
238
|
+
set numBuildingSteps(v) {
|
|
239
|
+
numBuildingSteps = v;
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
Binary file
|
package/dist/browser.css
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/* ../../../packages/web/adwaita-fonts/400.css */
|
|
2
|
-
@font-face {
|
|
3
|
-
font-family: "Adwaita Sans";
|
|
4
|
-
font-style: normal;
|
|
5
|
-
font-display: swap;
|
|
6
|
-
font-weight: 400;
|
|
7
|
-
src: url("./adwaita-sans-400-VCQV646T.ttf") format("truetype");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/* ../../../packages/web/adwaita-fonts/index.css */
|