nova64 0.2.1
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/LICENSE +21 -0
- package/README.md +786 -0
- package/index.html +651 -0
- package/package.json +255 -0
- package/public/os9-shell/assets/index-B1Uvacma.js +32825 -0
- package/public/os9-shell/assets/index-B1Uvacma.js.map +1 -0
- package/public/os9-shell/assets/index-DIHfrTaW.css +1 -0
- package/public/os9-shell/index.html +14 -0
- package/public/os9-shell/nova-icon.svg +12 -0
- package/runtime/api-2d.js +878 -0
- package/runtime/api-3d/camera.js +73 -0
- package/runtime/api-3d/instancing.js +180 -0
- package/runtime/api-3d/lights.js +51 -0
- package/runtime/api-3d/materials.js +47 -0
- package/runtime/api-3d/models.js +84 -0
- package/runtime/api-3d/pbr.js +69 -0
- package/runtime/api-3d/primitives.js +304 -0
- package/runtime/api-3d/scene.js +169 -0
- package/runtime/api-3d/transforms.js +161 -0
- package/runtime/api-3d.js +154 -0
- package/runtime/api-effects.js +753 -0
- package/runtime/api-presets.js +85 -0
- package/runtime/api-skybox.js +178 -0
- package/runtime/api-sprites.js +100 -0
- package/runtime/api-voxel.js +601 -0
- package/runtime/api.js +201 -0
- package/runtime/assets.js +27 -0
- package/runtime/audio.js +114 -0
- package/runtime/collision.js +47 -0
- package/runtime/console.js +101 -0
- package/runtime/editor.js +233 -0
- package/runtime/font.js +233 -0
- package/runtime/framebuffer.js +28 -0
- package/runtime/fullscreen-button.js +185 -0
- package/runtime/gpu-canvas2d.js +47 -0
- package/runtime/gpu-threejs.js +639 -0
- package/runtime/gpu-webgl2.js +310 -0
- package/runtime/index.js +22 -0
- package/runtime/input.js +225 -0
- package/runtime/logger.js +60 -0
- package/runtime/physics.js +101 -0
- package/runtime/screens.js +213 -0
- package/runtime/storage.js +38 -0
- package/runtime/store.js +151 -0
- package/runtime/textinput.js +68 -0
- package/runtime/ui/buttons.js +124 -0
- package/runtime/ui/panels.js +105 -0
- package/runtime/ui/text.js +86 -0
- package/runtime/ui/widgets.js +141 -0
- package/runtime/ui.js +111 -0
- package/src/main.js +474 -0
- package/vite.config.js +63 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// runtime/api-3d/primitives.js
|
|
2
|
+
// Mesh creation (createMesh) and all shape primitives
|
|
3
|
+
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { logger } from '../logger.js';
|
|
6
|
+
|
|
7
|
+
export function primitivesModule({
|
|
8
|
+
scene,
|
|
9
|
+
gpu,
|
|
10
|
+
meshes,
|
|
11
|
+
mixers,
|
|
12
|
+
modelAnimations,
|
|
13
|
+
counters,
|
|
14
|
+
getCachedMaterial,
|
|
15
|
+
}) {
|
|
16
|
+
function createMesh(geometry, material, position = [0, 0, 0]) {
|
|
17
|
+
if (!geometry || !material) {
|
|
18
|
+
logger.error('createMesh: geometry and material are required');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let mesh;
|
|
23
|
+
if (geometry.type && !geometry.isBufferGeometry) {
|
|
24
|
+
// Mock object path (used in tests)
|
|
25
|
+
mesh = {
|
|
26
|
+
type: 'Mesh',
|
|
27
|
+
geometry,
|
|
28
|
+
material,
|
|
29
|
+
position: {
|
|
30
|
+
x: 0,
|
|
31
|
+
y: 0,
|
|
32
|
+
z: 0,
|
|
33
|
+
set(x, y, z) {
|
|
34
|
+
this.x = x;
|
|
35
|
+
this.y = y;
|
|
36
|
+
this.z = z;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
rotation: {
|
|
40
|
+
x: 0,
|
|
41
|
+
y: 0,
|
|
42
|
+
z: 0,
|
|
43
|
+
set(x, y, z) {
|
|
44
|
+
this.x = x;
|
|
45
|
+
this.y = y;
|
|
46
|
+
this.z = z;
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
scale: {
|
|
50
|
+
x: 1,
|
|
51
|
+
y: 1,
|
|
52
|
+
z: 1,
|
|
53
|
+
set(x, y, z) {
|
|
54
|
+
this.x = x;
|
|
55
|
+
this.y = y;
|
|
56
|
+
this.z = z;
|
|
57
|
+
},
|
|
58
|
+
setScalar(s) {
|
|
59
|
+
this.x = s;
|
|
60
|
+
this.y = s;
|
|
61
|
+
this.z = s;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
castShadow: true,
|
|
65
|
+
receiveShadow: true,
|
|
66
|
+
};
|
|
67
|
+
} else {
|
|
68
|
+
mesh = new THREE.Mesh(geometry, material);
|
|
69
|
+
mesh.castShadow = true;
|
|
70
|
+
mesh.receiveShadow = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(position) && position.length >= 3) {
|
|
74
|
+
mesh.position.set(position[0], position[1], position[2]);
|
|
75
|
+
} else if (typeof position === 'object' && position.x !== undefined) {
|
|
76
|
+
mesh.position.set(position.x, position.y || 0, position.z || 0);
|
|
77
|
+
} else {
|
|
78
|
+
mesh.position.set(0, 0, 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (scene.add) scene.add(mesh);
|
|
82
|
+
if (material?.userData?.animated && gpu.registerAnimatedMesh) {
|
|
83
|
+
gpu.registerAnimatedMesh(mesh);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const id = ++counters.mesh;
|
|
87
|
+
meshes.set(id, mesh);
|
|
88
|
+
return id;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function destroyMesh(id) {
|
|
92
|
+
const mesh = meshes.get(id);
|
|
93
|
+
if (mesh) {
|
|
94
|
+
scene.remove(mesh);
|
|
95
|
+
mesh.geometry?.dispose();
|
|
96
|
+
if (mesh.material?.map) mesh.material.map.dispose();
|
|
97
|
+
mesh.material?.dispose();
|
|
98
|
+
meshes.delete(id);
|
|
99
|
+
mixers.delete(id);
|
|
100
|
+
modelAnimations.delete(id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getMesh(id) {
|
|
105
|
+
return meshes.get(id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createCube(size = 1, color = 0xffffff, position = [0, 0, 0], options = {}) {
|
|
109
|
+
try {
|
|
110
|
+
if (typeof size !== 'number' || size <= 0) {
|
|
111
|
+
logger.warn('createCube: invalid size, using default');
|
|
112
|
+
size = 1;
|
|
113
|
+
}
|
|
114
|
+
if (typeof color !== 'number') {
|
|
115
|
+
logger.warn('createCube: invalid color, using white');
|
|
116
|
+
color = 0xffffff;
|
|
117
|
+
}
|
|
118
|
+
return createMesh(
|
|
119
|
+
gpu.createBoxGeometry(size, size, size),
|
|
120
|
+
getCachedMaterial({ color, ...options }),
|
|
121
|
+
position
|
|
122
|
+
);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
logger.error('createCube failed:', e);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createAdvancedCube(size = 1, materialOptions = {}, position = [0, 0, 0]) {
|
|
130
|
+
try {
|
|
131
|
+
if (typeof size !== 'number' || size <= 0) size = 1;
|
|
132
|
+
return createMesh(
|
|
133
|
+
gpu.createBoxGeometry(size, size, size),
|
|
134
|
+
getCachedMaterial(materialOptions),
|
|
135
|
+
position
|
|
136
|
+
);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
logger.error('createAdvancedCube failed:', e);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function createSphere(
|
|
144
|
+
radius = 1,
|
|
145
|
+
color = 0xffffff,
|
|
146
|
+
position = [0, 0, 0],
|
|
147
|
+
segments = 8,
|
|
148
|
+
options = {}
|
|
149
|
+
) {
|
|
150
|
+
try {
|
|
151
|
+
if (typeof radius !== 'number' || radius <= 0) {
|
|
152
|
+
logger.warn('createSphere: invalid radius, using default');
|
|
153
|
+
radius = 1;
|
|
154
|
+
}
|
|
155
|
+
if (typeof segments !== 'number' || segments < 3) {
|
|
156
|
+
logger.warn('createSphere: invalid segments, using default');
|
|
157
|
+
segments = 8;
|
|
158
|
+
}
|
|
159
|
+
return createMesh(
|
|
160
|
+
gpu.createSphereGeometry(radius, segments),
|
|
161
|
+
getCachedMaterial({ color, ...options }),
|
|
162
|
+
position
|
|
163
|
+
);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
logger.error('createSphere failed:', e);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function createAdvancedSphere(
|
|
171
|
+
radius = 1,
|
|
172
|
+
materialOptions = {},
|
|
173
|
+
position = [0, 0, 0],
|
|
174
|
+
segments = 16
|
|
175
|
+
) {
|
|
176
|
+
try {
|
|
177
|
+
if (typeof radius !== 'number' || radius <= 0) radius = 1;
|
|
178
|
+
if (typeof segments !== 'number' || segments < 3) segments = 16;
|
|
179
|
+
return createMesh(
|
|
180
|
+
gpu.createSphereGeometry(radius, segments),
|
|
181
|
+
getCachedMaterial(materialOptions),
|
|
182
|
+
position
|
|
183
|
+
);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
logger.error('createAdvancedSphere failed:', e);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function createPlane(width = 1, height = 1, color = 0xffffff, position = [0, 0, 0]) {
|
|
191
|
+
try {
|
|
192
|
+
if (typeof width !== 'number' || width <= 0) {
|
|
193
|
+
logger.warn('createPlane: invalid width, using default');
|
|
194
|
+
width = 1;
|
|
195
|
+
}
|
|
196
|
+
if (typeof height !== 'number' || height <= 0) {
|
|
197
|
+
logger.warn('createPlane: invalid height, using default');
|
|
198
|
+
height = 1;
|
|
199
|
+
}
|
|
200
|
+
return createMesh(
|
|
201
|
+
gpu.createPlaneGeometry(width, height),
|
|
202
|
+
getCachedMaterial({ color }),
|
|
203
|
+
position
|
|
204
|
+
);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
logger.error('createPlane failed:', e);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function createCylinder(
|
|
212
|
+
radiusTop = 1,
|
|
213
|
+
radiusBottom = 1,
|
|
214
|
+
height = 1,
|
|
215
|
+
color = 0xffffff,
|
|
216
|
+
position = [0, 0, 0],
|
|
217
|
+
options = {}
|
|
218
|
+
) {
|
|
219
|
+
try {
|
|
220
|
+
const geom = gpu.createCylinderGeometry
|
|
221
|
+
? gpu.createCylinderGeometry(radiusTop, radiusBottom, height, options.segments || 16)
|
|
222
|
+
: gpu.createBoxGeometry(radiusTop, height, radiusTop);
|
|
223
|
+
return createMesh(geom, getCachedMaterial({ ...options, color }), position);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
logger.error('createCylinder err', e);
|
|
226
|
+
return createCube(radiusTop * 2, color, position);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function createTorus(
|
|
231
|
+
radius = 1,
|
|
232
|
+
tube = 0.3,
|
|
233
|
+
color = 0xffffff,
|
|
234
|
+
position = [0, 0, 0],
|
|
235
|
+
options = {}
|
|
236
|
+
) {
|
|
237
|
+
try {
|
|
238
|
+
const geometry = new THREE.TorusGeometry(
|
|
239
|
+
radius,
|
|
240
|
+
tube,
|
|
241
|
+
options.radialSegments || 8,
|
|
242
|
+
options.tubularSegments || 16
|
|
243
|
+
);
|
|
244
|
+
return createMesh(geometry, gpu.createN64Material({ color, ...options }), position);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
logger.error('createTorus failed:', e);
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function createCone(
|
|
252
|
+
radius = 1,
|
|
253
|
+
height = 2,
|
|
254
|
+
color = 0xffffff,
|
|
255
|
+
position = [0, 0, 0],
|
|
256
|
+
options = {}
|
|
257
|
+
) {
|
|
258
|
+
try {
|
|
259
|
+
return createMesh(
|
|
260
|
+
gpu.createConeGeometry(radius, height, options.segments || 16),
|
|
261
|
+
getCachedMaterial({ color, ...options }),
|
|
262
|
+
position
|
|
263
|
+
);
|
|
264
|
+
} catch (e) {
|
|
265
|
+
logger.error('createCone failed:', e);
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function createCapsule(
|
|
271
|
+
radius = 0.5,
|
|
272
|
+
height = 1,
|
|
273
|
+
color = 0xffffff,
|
|
274
|
+
position = [0, 0, 0],
|
|
275
|
+
options = {}
|
|
276
|
+
) {
|
|
277
|
+
try {
|
|
278
|
+
return createMesh(
|
|
279
|
+
gpu.createCapsuleGeometry(radius, height, options.segments || 8),
|
|
280
|
+
getCachedMaterial({ color, ...options }),
|
|
281
|
+
position
|
|
282
|
+
);
|
|
283
|
+
} catch (e) {
|
|
284
|
+
logger.error('createCapsule failed:', e);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
createMesh,
|
|
291
|
+
destroyMesh,
|
|
292
|
+
getMesh,
|
|
293
|
+
removeMesh: destroyMesh,
|
|
294
|
+
createCube,
|
|
295
|
+
createAdvancedCube,
|
|
296
|
+
createSphere,
|
|
297
|
+
createAdvancedSphere,
|
|
298
|
+
createPlane,
|
|
299
|
+
createCylinder,
|
|
300
|
+
createTorus,
|
|
301
|
+
createCone,
|
|
302
|
+
createCapsule,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// runtime/api-3d/scene.js
|
|
2
|
+
// clearScene, setupScene, raycasting, stats, N64 effects
|
|
3
|
+
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { logger } from '../logger.js';
|
|
6
|
+
|
|
7
|
+
export function sceneModule(ctx) {
|
|
8
|
+
const {
|
|
9
|
+
scene,
|
|
10
|
+
camera,
|
|
11
|
+
renderer,
|
|
12
|
+
gpu,
|
|
13
|
+
meshes,
|
|
14
|
+
cartLights,
|
|
15
|
+
materialCache,
|
|
16
|
+
instancedMeshes,
|
|
17
|
+
lodObjects,
|
|
18
|
+
} = ctx;
|
|
19
|
+
|
|
20
|
+
// These come from other modules — resolved at call-time via ctx
|
|
21
|
+
function getDisposeMaterial() {
|
|
22
|
+
return ctx.disposeMaterial;
|
|
23
|
+
}
|
|
24
|
+
function getRemoveLight() {
|
|
25
|
+
return ctx.removeLight;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clearScene() {
|
|
29
|
+
const disposeMaterial = getDisposeMaterial();
|
|
30
|
+
const removeLight = getRemoveLight();
|
|
31
|
+
|
|
32
|
+
logger.info(
|
|
33
|
+
'🧹 Clearing 3D scene... meshes:',
|
|
34
|
+
meshes.size,
|
|
35
|
+
'lights:',
|
|
36
|
+
cartLights.size,
|
|
37
|
+
'materials:',
|
|
38
|
+
materialCache.size
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Dispose all cart meshes
|
|
42
|
+
for (const [, mesh] of meshes) {
|
|
43
|
+
scene.remove(mesh);
|
|
44
|
+
if (mesh.geometry) mesh.geometry.dispose();
|
|
45
|
+
if (mesh.material) {
|
|
46
|
+
if (Array.isArray(mesh.material)) mesh.material.forEach(disposeMaterial);
|
|
47
|
+
else disposeMaterial(mesh.material);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
meshes.clear();
|
|
51
|
+
|
|
52
|
+
// Remove dynamic lights
|
|
53
|
+
cartLights.forEach((_, id) => removeLight(id));
|
|
54
|
+
cartLights.clear();
|
|
55
|
+
|
|
56
|
+
// Flush material cache
|
|
57
|
+
materialCache.forEach(disposeMaterial);
|
|
58
|
+
materialCache.clear();
|
|
59
|
+
|
|
60
|
+
// Remove any remaining scene children (e.g. from loadModel / direct additions)
|
|
61
|
+
while (scene.children.length > 0) {
|
|
62
|
+
const child = scene.children[0];
|
|
63
|
+
scene.remove(child);
|
|
64
|
+
if (child.geometry) child.geometry.dispose();
|
|
65
|
+
if (child.material) {
|
|
66
|
+
if (Array.isArray(child.material)) child.material.forEach(disposeMaterial);
|
|
67
|
+
else disposeMaterial(child.material);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Re-add minimal default lighting so scenes aren't black
|
|
72
|
+
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
|
|
73
|
+
const dir = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
74
|
+
dir.position.set(5, 10, 7.5);
|
|
75
|
+
scene.add(dir);
|
|
76
|
+
|
|
77
|
+
// Reset animated mesh registry in GPU backend
|
|
78
|
+
if (gpu.clearAnimatedMeshes) gpu.clearAnimatedMeshes();
|
|
79
|
+
|
|
80
|
+
// Dispose instanced meshes
|
|
81
|
+
for (const { mesh } of instancedMeshes.values()) {
|
|
82
|
+
scene.remove(mesh);
|
|
83
|
+
mesh.geometry?.dispose();
|
|
84
|
+
mesh.material?.dispose();
|
|
85
|
+
}
|
|
86
|
+
instancedMeshes.clear();
|
|
87
|
+
|
|
88
|
+
// Dispose LOD objects
|
|
89
|
+
for (const lod of lodObjects.values()) {
|
|
90
|
+
scene.remove(lod);
|
|
91
|
+
for (const { object } of lod.levels) {
|
|
92
|
+
object.geometry?.dispose();
|
|
93
|
+
object.material?.dispose();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
lodObjects.clear();
|
|
97
|
+
|
|
98
|
+
logger.info('✅ Scene cleared completely');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* setupScene(opts) — One-call scene configuration.
|
|
103
|
+
* opts: { camera, light, fog, skybox, effects }
|
|
104
|
+
*/
|
|
105
|
+
function setupScene(opts = {}) {
|
|
106
|
+
const cam = opts.camera ?? {};
|
|
107
|
+
ctx.setCameraPosition(cam.x ?? 0, cam.y ?? 5, cam.z ?? 10);
|
|
108
|
+
ctx.setCameraTarget(cam.targetX ?? 0, cam.targetY ?? 0, cam.targetZ ?? 0);
|
|
109
|
+
if (cam.fov) ctx.setCameraFOV(cam.fov);
|
|
110
|
+
|
|
111
|
+
const light = opts.light ?? {};
|
|
112
|
+
if (light.direction) {
|
|
113
|
+
const d = light.direction;
|
|
114
|
+
ctx.setLightDirection(d[0] ?? -0.3, d[1] ?? -1, d[2] ?? -0.5);
|
|
115
|
+
}
|
|
116
|
+
if (light.color !== undefined) ctx.setLightColor(light.color);
|
|
117
|
+
if (light.ambient !== undefined) ctx.setAmbientLight(light.ambient);
|
|
118
|
+
|
|
119
|
+
if (opts.fog && opts.fog !== false) {
|
|
120
|
+
ctx.setFog(opts.fog.color ?? 0x000000, opts.fog.near ?? 10, opts.fog.far ?? 50);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (opts.skybox && typeof globalThis.createSpaceSkybox === 'function') {
|
|
124
|
+
globalThis.createSpaceSkybox(opts.skybox);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (opts.effects && typeof globalThis.enableRetroEffects === 'function') {
|
|
128
|
+
globalThis.enableRetroEffects(typeof opts.effects === 'object' ? opts.effects : {});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function raycastFromCamera(x, y) {
|
|
133
|
+
const raycaster = new THREE.Raycaster();
|
|
134
|
+
const mouse = new THREE.Vector2(
|
|
135
|
+
(x / renderer.domElement.clientWidth) * 2 - 1,
|
|
136
|
+
-(y / renderer.domElement.clientHeight) * 2 + 1
|
|
137
|
+
);
|
|
138
|
+
raycaster.setFromCamera(mouse, camera);
|
|
139
|
+
const objects = Array.from(meshes.values()).filter(m => m.type === 'Mesh');
|
|
140
|
+
const hits = raycaster.intersectObjects(objects);
|
|
141
|
+
if (hits.length > 0) {
|
|
142
|
+
for (const [id, mesh] of meshes) {
|
|
143
|
+
if (mesh === hits[0].object) {
|
|
144
|
+
return { meshId: id, point: hits[0].point, distance: hits[0].distance };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function get3DStats() {
|
|
152
|
+
return gpu.getStats();
|
|
153
|
+
}
|
|
154
|
+
function enablePixelation(factor = 2) {
|
|
155
|
+
gpu.enablePixelation(factor);
|
|
156
|
+
}
|
|
157
|
+
function enableDithering(enabled = true) {
|
|
158
|
+
gpu.enableDithering(enabled);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
clearScene,
|
|
163
|
+
setupScene,
|
|
164
|
+
raycastFromCamera,
|
|
165
|
+
get3DStats,
|
|
166
|
+
enablePixelation,
|
|
167
|
+
enableDithering,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// runtime/api-3d/transforms.js
|
|
2
|
+
// Mesh position, rotation, scale, visibility, and shadow controls
|
|
3
|
+
|
|
4
|
+
import { logger } from '../logger.js';
|
|
5
|
+
|
|
6
|
+
export function transformsModule({ getMesh }) {
|
|
7
|
+
function setPosition(meshId, x, y, z) {
|
|
8
|
+
try {
|
|
9
|
+
const mesh = getMesh(meshId);
|
|
10
|
+
if (!mesh) {
|
|
11
|
+
logger.warn(`setPosition: mesh with id ${meshId} not found`);
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
mesh.position.set(
|
|
15
|
+
typeof x === 'number' ? x : 0,
|
|
16
|
+
typeof y === 'number' ? y : 0,
|
|
17
|
+
typeof z === 'number' ? z : 0
|
|
18
|
+
);
|
|
19
|
+
return true;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
logger.error('setPosition failed:', e);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setRotation(meshId, x, y, z) {
|
|
27
|
+
try {
|
|
28
|
+
const mesh = getMesh(meshId);
|
|
29
|
+
if (!mesh) {
|
|
30
|
+
logger.warn(`setRotation: mesh with id ${meshId} not found`);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
mesh.rotation.set(
|
|
34
|
+
typeof x === 'number' ? x : 0,
|
|
35
|
+
typeof y === 'number' ? y : 0,
|
|
36
|
+
typeof z === 'number' ? z : 0
|
|
37
|
+
);
|
|
38
|
+
return true;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
logger.error('setRotation failed:', e);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setScale(meshId, x, y, z) {
|
|
46
|
+
try {
|
|
47
|
+
const mesh = getMesh(meshId);
|
|
48
|
+
if (!mesh) {
|
|
49
|
+
logger.warn(`setScale: mesh with id ${meshId} not found`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
x = typeof x === 'number' && x > 0 ? x : 1;
|
|
53
|
+
if (typeof y === 'undefined') {
|
|
54
|
+
mesh.scale.setScalar(x);
|
|
55
|
+
} else {
|
|
56
|
+
mesh.scale.set(
|
|
57
|
+
x,
|
|
58
|
+
typeof y === 'number' && y > 0 ? y : 1,
|
|
59
|
+
typeof z === 'number' && z > 0 ? z : 1
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
logger.error('setScale failed:', e);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getPosition(meshId) {
|
|
70
|
+
const mesh = getMesh(meshId);
|
|
71
|
+
return mesh ? [mesh.position.x, mesh.position.y, mesh.position.z] : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getRotation(meshId) {
|
|
75
|
+
const mesh = getMesh(meshId);
|
|
76
|
+
return mesh ? [mesh.rotation.x, mesh.rotation.y, mesh.rotation.z] : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function rotateMesh(meshId, dX, dY, dZ) {
|
|
80
|
+
try {
|
|
81
|
+
const mesh = getMesh(meshId);
|
|
82
|
+
if (!mesh) {
|
|
83
|
+
logger.warn(`rotateMesh: mesh with id ${meshId} not found`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
mesh.rotation.x += typeof dX === 'number' ? dX : 0;
|
|
87
|
+
mesh.rotation.y += typeof dY === 'number' ? dY : 0;
|
|
88
|
+
mesh.rotation.z += typeof dZ === 'number' ? dZ : 0;
|
|
89
|
+
return true;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
logger.error('rotateMesh failed:', e);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function moveMesh(meshId, x, y, z) {
|
|
97
|
+
const mesh = getMesh(meshId);
|
|
98
|
+
if (mesh) {
|
|
99
|
+
mesh.position.x += x;
|
|
100
|
+
mesh.position.y += y;
|
|
101
|
+
mesh.position.z += z;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setFlatShading(meshId, enabled = true) {
|
|
106
|
+
const mesh = getMesh(meshId);
|
|
107
|
+
if (!mesh) return false;
|
|
108
|
+
if (mesh.material) {
|
|
109
|
+
mesh.material.flatShading = enabled;
|
|
110
|
+
mesh.material.needsUpdate = true;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function setMeshVisible(meshId, visible) {
|
|
116
|
+
const mesh = getMesh(meshId);
|
|
117
|
+
if (!mesh) return false;
|
|
118
|
+
mesh.visible = visible;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setMeshOpacity(meshId, opacity) {
|
|
123
|
+
const mesh = getMesh(meshId);
|
|
124
|
+
if (!mesh) return false;
|
|
125
|
+
if (mesh.material) {
|
|
126
|
+
mesh.material.transparent = opacity < 1;
|
|
127
|
+
mesh.material.opacity = opacity;
|
|
128
|
+
mesh.material.needsUpdate = true;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setCastShadow(meshId, cast) {
|
|
134
|
+
const mesh = getMesh(meshId);
|
|
135
|
+
if (!mesh) return false;
|
|
136
|
+
mesh.castShadow = cast;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function setReceiveShadow(meshId, receive) {
|
|
141
|
+
const mesh = getMesh(meshId);
|
|
142
|
+
if (!mesh) return false;
|
|
143
|
+
mesh.receiveShadow = receive;
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
setPosition,
|
|
149
|
+
setRotation,
|
|
150
|
+
setScale,
|
|
151
|
+
getPosition,
|
|
152
|
+
getRotation,
|
|
153
|
+
rotateMesh,
|
|
154
|
+
moveMesh,
|
|
155
|
+
setFlatShading,
|
|
156
|
+
setMeshVisible,
|
|
157
|
+
setMeshOpacity,
|
|
158
|
+
setCastShadow,
|
|
159
|
+
setReceiveShadow,
|
|
160
|
+
};
|
|
161
|
+
}
|