@simarena/viewport 0.0.1-alpha.0 → 0.0.1-alpha.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/package.json +24 -18
- package/src/index.ts +40 -10
- package/src/sensors/contact.ts +124 -0
- package/src/sensors/directions.ts +103 -0
- package/src/sensors/index.ts +4 -0
- package/src/sensors/lidar.ts +120 -0
- package/src/sync.ts +128 -124
- package/src/types.ts +83 -0
- package/src/viewer/builder.ts +492 -0
- package/src/viewer/scene.ts +253 -0
- package/src/viewer/types.ts +193 -0
- package/src/viewport.ts +71 -58
- package/src/compiler/threejs/index.ts +0 -389
- package/src/parser/threejs/index.ts +0 -84
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
2
|
-
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
|
|
3
|
-
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
|
|
4
|
-
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
|
|
5
|
-
import type { SceneDoc, DocEntity, MaterialDef, MeshDef, TextureDef, LightComponent, CameraComponent } from '@simarena/format';
|
|
6
|
-
import type { Geom } from '@simarena/format';
|
|
7
|
-
import type { Vec3, Vec4 } from '@simarena/format';
|
|
8
|
-
|
|
9
|
-
const stlLoader = new STLLoader();
|
|
10
|
-
const geoCache = new Map<string, THREE.BufferGeometry>();
|
|
11
|
-
|
|
12
|
-
async function loadSTL(url: string): Promise<THREE.BufferGeometry> {
|
|
13
|
-
const hit = geoCache.get(url);
|
|
14
|
-
if (hit) return hit.clone();
|
|
15
|
-
const geo = await stlLoader.loadAsync(url);
|
|
16
|
-
geo.computeVertexNormals();
|
|
17
|
-
geoCache.set(url, geo);
|
|
18
|
-
return geo.clone();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const objGroupCache = new Map<string, THREE.Group>();
|
|
22
|
-
const mtlCreatorCache = new Map<string, MTLLoader.MaterialCreator | null>();
|
|
23
|
-
|
|
24
|
-
async function loadMTL(url: string): Promise<MTLLoader.MaterialCreator | null> {
|
|
25
|
-
if (mtlCreatorCache.has(url)) return mtlCreatorCache.get(url)!;
|
|
26
|
-
try {
|
|
27
|
-
const dir = url.substring(0, url.lastIndexOf('/') + 1);
|
|
28
|
-
const loader = new MTLLoader();
|
|
29
|
-
loader.setResourcePath(dir);
|
|
30
|
-
const creator = await loader.loadAsync(url);
|
|
31
|
-
creator.preload();
|
|
32
|
-
mtlCreatorCache.set(url, creator);
|
|
33
|
-
return creator;
|
|
34
|
-
} catch {
|
|
35
|
-
mtlCreatorCache.set(url, null);
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const texLoader = new THREE.TextureLoader();
|
|
41
|
-
const texCache = new Map<string, THREE.Texture>();
|
|
42
|
-
|
|
43
|
-
async function loadTex(url: string): Promise<THREE.Texture> {
|
|
44
|
-
const hit = texCache.get(url);
|
|
45
|
-
if (hit) return hit;
|
|
46
|
-
const tex = await texLoader.loadAsync(url);
|
|
47
|
-
tex.colorSpace = THREE.SRGBColorSpace;
|
|
48
|
-
texCache.set(url, tex);
|
|
49
|
-
return tex;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function loadOBJGroup(url: string): Promise<THREE.Group> {
|
|
53
|
-
const mtlUrl = url.replace(/\.(obj|OBJ)$/, '.mtl');
|
|
54
|
-
const creator = await loadMTL(mtlUrl);
|
|
55
|
-
const cacheKey = url + (creator ? ':mtl' : '');
|
|
56
|
-
const hit = objGroupCache.get(cacheKey);
|
|
57
|
-
if (hit) return hit.clone() as THREE.Group;
|
|
58
|
-
|
|
59
|
-
const loader = new OBJLoader();
|
|
60
|
-
if (creator) loader.setMaterials(creator);
|
|
61
|
-
const grp = await loader.loadAsync(url);
|
|
62
|
-
|
|
63
|
-
grp.traverse((c) => {
|
|
64
|
-
if (c instanceof THREE.Mesh) {
|
|
65
|
-
const old = (Array.isArray(c.material) ? c.material[0] : c.material) as THREE.MeshPhongMaterial;
|
|
66
|
-
c.material = new THREE.MeshStandardMaterial({
|
|
67
|
-
map: old?.map ?? null,
|
|
68
|
-
color: old?.map ? new THREE.Color(1, 1, 1) : (old?.color ?? new THREE.Color(0xaaaaaa)),
|
|
69
|
-
metalness: 0.0,
|
|
70
|
-
roughness: 0.8
|
|
71
|
-
});
|
|
72
|
-
c.castShadow = true;
|
|
73
|
-
c.receiveShadow = true;
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
objGroupCache.set(cacheKey, grp);
|
|
77
|
-
return grp.clone() as THREE.Group;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function stdMat(color?: string, opacity?: number): THREE.MeshStandardMaterial {
|
|
81
|
-
const c = color ? new THREE.Color(color) : new THREE.Color(0x6b7280);
|
|
82
|
-
const a = opacity ?? 1;
|
|
83
|
-
return new THREE.MeshStandardMaterial({ color: c, transparent: a < 1, opacity: a, metalness: 0.1, roughness: 0.6 });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function renderGeoms(entity: DocEntity, all: Geom[]): Geom[] {
|
|
87
|
-
if (!entity.visual) return all;
|
|
88
|
-
if (!entity.visual.groups?.length) return all;
|
|
89
|
-
return all.filter(g => entity.visual!.groups!.includes(g.group ?? ''));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function buildGeo(g: Geom): THREE.BufferGeometry | null {
|
|
93
|
-
switch (g.type) {
|
|
94
|
-
case 'box': {
|
|
95
|
-
const [w, h, d] = g.size;
|
|
96
|
-
return new THREE.BoxGeometry(w, h, d);
|
|
97
|
-
}
|
|
98
|
-
case 'sphere': return new THREE.SphereGeometry(g.radius, 32, 32);
|
|
99
|
-
case 'cylinder': {
|
|
100
|
-
const geo = new THREE.CylinderGeometry(g.radius, g.radius, g.height, 32);
|
|
101
|
-
geo.rotateX(Math.PI / 2);
|
|
102
|
-
return geo;
|
|
103
|
-
}
|
|
104
|
-
case 'capsule': {
|
|
105
|
-
const geo = new THREE.CapsuleGeometry(g.radius, g.height, 16, 32);
|
|
106
|
-
geo.rotateX(Math.PI / 2);
|
|
107
|
-
return geo;
|
|
108
|
-
}
|
|
109
|
-
case 'plane': {
|
|
110
|
-
const [w, d] = g.size ?? [10, 10];
|
|
111
|
-
return new THREE.PlaneGeometry(w * 2, d * 2);
|
|
112
|
-
}
|
|
113
|
-
default: return null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function applyT(
|
|
118
|
-
obj: THREE.Object3D,
|
|
119
|
-
t?: { pos?: Vec3; rot?: Vec3; quat?: Vec4; scale?: number }
|
|
120
|
-
): void {
|
|
121
|
-
if (!t) return;
|
|
122
|
-
if (t.pos) obj.position.set(t.pos[0], t.pos[1], t.pos[2]);
|
|
123
|
-
if (t.quat) {
|
|
124
|
-
const [w, x, y, z] = t.quat;
|
|
125
|
-
obj.quaternion.set(x, y, z, w).normalize();
|
|
126
|
-
} else if (t.rot) {
|
|
127
|
-
const D = Math.PI / 180;
|
|
128
|
-
obj.rotation.set(t.rot[0] * D, t.rot[1] * D, t.rot[2] * D);
|
|
129
|
-
}
|
|
130
|
-
if (t.scale !== undefined) obj.scale.setScalar(t.scale);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function resolveMat(g: Geom, matMap: Map<string, MaterialDef>): THREE.MeshStandardMaterial {
|
|
134
|
-
const opacity = g.opacity ?? 1;
|
|
135
|
-
if (g.material && matMap.has(g.material)) {
|
|
136
|
-
const def = matMap.get(g.material)!;
|
|
137
|
-
if (def.rgba) {
|
|
138
|
-
const alpha = g.opacity ?? def.rgba[3] ?? 1;
|
|
139
|
-
return new THREE.MeshStandardMaterial({
|
|
140
|
-
color: new THREE.Color(def.rgba[0], def.rgba[1], def.rgba[2]),
|
|
141
|
-
transparent: alpha < 1, opacity: alpha,
|
|
142
|
-
metalness: def.metalness ?? 0.1, roughness: def.roughness ?? 0.6
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
if (def.color) return stdMat(def.color, opacity);
|
|
146
|
-
}
|
|
147
|
-
if (g.color) return stdMat(g.color, opacity);
|
|
148
|
-
return new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.1, roughness: 0.6 });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function buildLightComponent(lc: LightComponent): THREE.Light | null {
|
|
152
|
-
const color = lc.color ? new THREE.Color(lc.color) : new THREE.Color(0xffffff);
|
|
153
|
-
const intensity = lc.intensity ?? 1;
|
|
154
|
-
switch (lc.type) {
|
|
155
|
-
case 'point': {
|
|
156
|
-
const l = new THREE.PointLight(color, intensity, lc.range ?? 0);
|
|
157
|
-
l.castShadow = lc.castShadow ?? false;
|
|
158
|
-
return l;
|
|
159
|
-
}
|
|
160
|
-
case 'directional': {
|
|
161
|
-
const l = new THREE.DirectionalLight(color, intensity);
|
|
162
|
-
if (lc.dir) l.position.set(-lc.dir[0], -lc.dir[1], -lc.dir[2]).normalize().multiplyScalar(5);
|
|
163
|
-
l.castShadow = lc.castShadow ?? false;
|
|
164
|
-
return l;
|
|
165
|
-
}
|
|
166
|
-
case 'spot': {
|
|
167
|
-
const l = new THREE.SpotLight(color, intensity, lc.range ?? 0, lc.angle ? lc.angle * Math.PI / 180 : Math.PI / 6);
|
|
168
|
-
l.castShadow = lc.castShadow ?? false;
|
|
169
|
-
return l;
|
|
170
|
-
}
|
|
171
|
-
case 'ambient': return new THREE.AmbientLight(color, intensity);
|
|
172
|
-
case 'hemisphere': {
|
|
173
|
-
const g = lc.groundColor ? new THREE.Color(lc.groundColor) : new THREE.Color(0x545454);
|
|
174
|
-
return new THREE.HemisphereLight(color, g, intensity);
|
|
175
|
-
}
|
|
176
|
-
default: return null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function buildMeshUrlMap(meshes: MeshDef[], basePath: string): Map<string, string> {
|
|
181
|
-
const map = new Map<string, string>();
|
|
182
|
-
for (const m of meshes) {
|
|
183
|
-
const url = m.file.startsWith('/') || m.file.startsWith('http') ? m.file : basePath + m.file;
|
|
184
|
-
map.set(m.name, url);
|
|
185
|
-
}
|
|
186
|
-
return map;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function buildMatMap(materials: MaterialDef[]): Map<string, MaterialDef> {
|
|
190
|
-
const map = new Map<string, MaterialDef>();
|
|
191
|
-
for (const m of materials) map.set(m.name, m);
|
|
192
|
-
return map;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function buildTextureUrlMap(textures: TextureDef[], basePath: string): Map<string, string> {
|
|
196
|
-
const map = new Map<string, string>();
|
|
197
|
-
for (const t of textures) {
|
|
198
|
-
const url = t.file.startsWith('/') || t.file.startsWith('http') ? t.file : basePath + t.file;
|
|
199
|
-
map.set(t.name, url);
|
|
200
|
-
}
|
|
201
|
-
return map;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
interface BuildCtx {
|
|
205
|
-
bodies: Map<string, THREE.Object3D>;
|
|
206
|
-
entityRoots: Map<string, THREE.Object3D>;
|
|
207
|
-
meshUrls: Map<string, string>;
|
|
208
|
-
textureUrls: Map<string, string>;
|
|
209
|
-
materials: Map<string, MaterialDef>;
|
|
210
|
-
threeScene: THREE.Scene;
|
|
211
|
-
deferredParents: Array<{ group: THREE.Group; parentPath: string }>;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function applyTexture(mat: THREE.MeshStandardMaterial, g: Geom, ctx: BuildCtx): Promise<void> {
|
|
215
|
-
if (!g.material) return;
|
|
216
|
-
const def = ctx.materials.get(g.material);
|
|
217
|
-
if (!def?.map) return;
|
|
218
|
-
const texUrl = ctx.textureUrls.get(def.map);
|
|
219
|
-
if (!texUrl) return;
|
|
220
|
-
try {
|
|
221
|
-
mat.map = await loadTex(texUrl);
|
|
222
|
-
mat.color.set(0xffffff);
|
|
223
|
-
mat.needsUpdate = true;
|
|
224
|
-
} catch { /* skip */ }
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async function buildGeom(g: Geom, parent: THREE.Group, ctx: BuildCtx): Promise<void> {
|
|
228
|
-
if (g.type === 'mesh') {
|
|
229
|
-
const url = ctx.meshUrls.get(g.mesh);
|
|
230
|
-
if (!url) { console.warn(`[threejs] mesh not in catalog: "${g.mesh}"`); return; }
|
|
231
|
-
try {
|
|
232
|
-
if (/\.(obj|OBJ)$/.test(url)) {
|
|
233
|
-
const grp = await loadOBJGroup(url);
|
|
234
|
-
if (g.material || g.color) {
|
|
235
|
-
const mat = resolveMat(g, ctx.materials);
|
|
236
|
-
await applyTexture(mat, g, ctx);
|
|
237
|
-
grp.traverse((c) => { if (c instanceof THREE.Mesh) { c.material = mat.clone(); c.castShadow = true; c.receiveShadow = true; } });
|
|
238
|
-
} else {
|
|
239
|
-
grp.traverse((c) => { if (c instanceof THREE.Mesh) { c.castShadow = true; c.receiveShadow = true; } });
|
|
240
|
-
}
|
|
241
|
-
applyT(grp, { pos: g.pos, rot: g.rot, quat: g.quat });
|
|
242
|
-
parent.add(grp);
|
|
243
|
-
} else {
|
|
244
|
-
const mat = resolveMat(g, ctx.materials);
|
|
245
|
-
await applyTexture(mat, g, ctx);
|
|
246
|
-
const geo = await loadSTL(url);
|
|
247
|
-
geo.computeVertexNormals();
|
|
248
|
-
const mesh = new THREE.Mesh(geo, mat);
|
|
249
|
-
mesh.castShadow = true; mesh.receiveShadow = true;
|
|
250
|
-
applyT(mesh, { pos: g.pos, rot: g.rot, quat: g.quat });
|
|
251
|
-
parent.add(mesh);
|
|
252
|
-
}
|
|
253
|
-
} catch (e) { console.error(`[threejs] failed to load mesh "${g.mesh}" from ${url}`, e); }
|
|
254
|
-
} else {
|
|
255
|
-
const geo = buildGeo(g);
|
|
256
|
-
if (!geo) return;
|
|
257
|
-
const mat = resolveMat(g, ctx.materials);
|
|
258
|
-
await applyTexture(mat, g, ctx);
|
|
259
|
-
geo.computeVertexNormals();
|
|
260
|
-
const mesh = new THREE.Mesh(geo, mat);
|
|
261
|
-
mesh.castShadow = true; mesh.receiveShadow = true;
|
|
262
|
-
applyT(mesh, { pos: g.pos, rot: g.rot, quat: g.quat });
|
|
263
|
-
parent.add(mesh);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function buildCameraIcon(cc: CameraComponent, parent: THREE.Group): void {
|
|
268
|
-
const mat = new THREE.MeshStandardMaterial({ color: 0x00bcd4, metalness: 0.3, roughness: 0.5 });
|
|
269
|
-
const body = new THREE.Mesh(new THREE.BoxGeometry(0.10, 0.065, 0.075), mat);
|
|
270
|
-
body.castShadow = true;
|
|
271
|
-
|
|
272
|
-
const lensMat = new THREE.MeshStandardMaterial({ color: 0x007a9e });
|
|
273
|
-
const lens = new THREE.Mesh(new THREE.CylinderGeometry(0.03, 0.02, 0.05, 8), lensMat);
|
|
274
|
-
lens.rotation.x = Math.PI / 2;
|
|
275
|
-
lens.position.z = 0.06;
|
|
276
|
-
|
|
277
|
-
const fov = (cc.fov ?? 50) * Math.PI / 180;
|
|
278
|
-
const aspect = (cc.resolution?.[0] ?? 256) / (cc.resolution?.[1] ?? 256);
|
|
279
|
-
const near = cc.near ?? 0.1;
|
|
280
|
-
const far = Math.min(cc.far ?? 100, 2);
|
|
281
|
-
const hNear = Math.tan(fov / 2) * near;
|
|
282
|
-
const wNear = hNear * aspect;
|
|
283
|
-
const hFar = Math.tan(fov / 2) * far;
|
|
284
|
-
const wFar = hFar * aspect;
|
|
285
|
-
|
|
286
|
-
const pts = [
|
|
287
|
-
new THREE.Vector3(-wNear, -hNear, near), new THREE.Vector3( wNear, -hNear, near),
|
|
288
|
-
new THREE.Vector3( wNear, -hNear, near), new THREE.Vector3( wNear, hNear, near),
|
|
289
|
-
new THREE.Vector3( wNear, hNear, near), new THREE.Vector3(-wNear, hNear, near),
|
|
290
|
-
new THREE.Vector3(-wNear, hNear, near), new THREE.Vector3(-wNear, -hNear, near),
|
|
291
|
-
new THREE.Vector3(-wFar, -hFar, far), new THREE.Vector3( wFar, -hFar, far),
|
|
292
|
-
new THREE.Vector3( wFar, -hFar, far), new THREE.Vector3( wFar, hFar, far),
|
|
293
|
-
new THREE.Vector3( wFar, hFar, far), new THREE.Vector3(-wFar, hFar, far),
|
|
294
|
-
new THREE.Vector3(-wFar, hFar, far), new THREE.Vector3(-wFar, -hFar, far),
|
|
295
|
-
new THREE.Vector3(0, 0, 0), new THREE.Vector3(-wFar, -hFar, far),
|
|
296
|
-
new THREE.Vector3(0, 0, 0), new THREE.Vector3( wFar, -hFar, far),
|
|
297
|
-
new THREE.Vector3(0, 0, 0), new THREE.Vector3( wFar, hFar, far),
|
|
298
|
-
new THREE.Vector3(0, 0, 0), new THREE.Vector3(-wFar, hFar, far),
|
|
299
|
-
];
|
|
300
|
-
const geo = new THREE.BufferGeometry().setFromPoints(pts);
|
|
301
|
-
const lines = new THREE.LineSegments(geo, new THREE.LineBasicMaterial({ color: 0x00bcd4, opacity: 0.6, transparent: true }));
|
|
302
|
-
lines.userData['isCameraIcon'] = true;
|
|
303
|
-
|
|
304
|
-
parent.add(body);
|
|
305
|
-
parent.add(lens);
|
|
306
|
-
parent.add(lines);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async function buildEntity(entity: DocEntity, parentObject: THREE.Object3D, ctx: BuildCtx): Promise<void> {
|
|
310
|
-
const grp = new THREE.Group();
|
|
311
|
-
grp.name = entity.name;
|
|
312
|
-
applyT(grp, entity.transform);
|
|
313
|
-
ctx.bodies.set(entity.name, grp);
|
|
314
|
-
|
|
315
|
-
const parentRef = entity.relation?.parent;
|
|
316
|
-
if (parentRef) {
|
|
317
|
-
ctx.deferredParents.push({ group: grp, parentPath: parentRef });
|
|
318
|
-
} else {
|
|
319
|
-
parentObject.add(grp);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
for (const g of renderGeoms(entity, entity.geoms ?? [])) await buildGeom(g, grp, ctx);
|
|
323
|
-
|
|
324
|
-
if (entity.light) {
|
|
325
|
-
const l = buildLightComponent(entity.light);
|
|
326
|
-
if (l) {
|
|
327
|
-
if (entity.transform?.pos) l.position.set(...entity.transform.pos);
|
|
328
|
-
grp.add(l);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (entity.camera) {
|
|
333
|
-
buildCameraIcon(entity.camera, grp);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export interface ViewerScene {
|
|
338
|
-
bodies: Map<string, THREE.Object3D>;
|
|
339
|
-
entityRoots: Map<string, THREE.Object3D>;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export async function buildScene(doc: SceneDoc, threeScene: THREE.Scene, basePath = ''): Promise<ViewerScene> {
|
|
343
|
-
const bodies = new Map<string, THREE.Object3D>();
|
|
344
|
-
const entityRoots = new Map<string, THREE.Object3D>();
|
|
345
|
-
const deferredParents: Array<{ group: THREE.Group; parentPath: string }> = [];
|
|
346
|
-
|
|
347
|
-
const ctx: BuildCtx = {
|
|
348
|
-
bodies, entityRoots,
|
|
349
|
-
meshUrls: buildMeshUrlMap(doc.meshes ?? [], basePath),
|
|
350
|
-
textureUrls: buildTextureUrlMap(doc.textures ?? [], basePath),
|
|
351
|
-
materials: buildMatMap(doc.materials ?? []),
|
|
352
|
-
threeScene, deferredParents
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
for (const entity of doc.scene ?? []) {
|
|
356
|
-
await buildEntity(entity, threeScene, ctx);
|
|
357
|
-
if (!entity.relation) {
|
|
358
|
-
const root = bodies.get(entity.name);
|
|
359
|
-
if (root) entityRoots.set(entity.name, root);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
for (const { group, parentPath } of deferredParents) {
|
|
364
|
-
const target = bodies.get(parentPath);
|
|
365
|
-
if (target) {
|
|
366
|
-
group.removeFromParent();
|
|
367
|
-
target.add(group);
|
|
368
|
-
} else {
|
|
369
|
-
console.warn(`[threejs] parent ref unresolved: "${parentPath}" for "${group.name}"`);
|
|
370
|
-
threeScene.add(group);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return { bodies, entityRoots };
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
export function createGeomMesh(g: Geom, color?: string): THREE.Mesh | null {
|
|
378
|
-
const geo = buildGeo(g);
|
|
379
|
-
if (!geo) return null;
|
|
380
|
-
geo.computeVertexNormals();
|
|
381
|
-
const mat = new THREE.MeshStandardMaterial({
|
|
382
|
-
color: color ?? g.color ?? '#6b7280',
|
|
383
|
-
roughness: 0.6, metalness: 0.1
|
|
384
|
-
});
|
|
385
|
-
const mesh = new THREE.Mesh(geo, mat);
|
|
386
|
-
mesh.castShadow = true;
|
|
387
|
-
mesh.receiveShadow = true;
|
|
388
|
-
return mesh;
|
|
389
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import * as THREE from 'three';
|
|
2
|
-
import type { SceneDoc, DocEntity, Physics, Joint } from '@simarena/format';
|
|
3
|
-
import type { Vec3, Geom } from '@simarena/format';
|
|
4
|
-
|
|
5
|
-
export type PrimitiveKind = 'box' | 'sphere' | 'cylinder' | 'capsule' | 'plane';
|
|
6
|
-
|
|
7
|
-
export interface ThreeEntity {
|
|
8
|
-
name: string;
|
|
9
|
-
obj: THREE.Object3D;
|
|
10
|
-
kind: 'primitive' | 'robot';
|
|
11
|
-
geom?: PrimitiveKind;
|
|
12
|
-
size?: Vec3;
|
|
13
|
-
color?: string;
|
|
14
|
-
asset?: string;
|
|
15
|
-
physics?: { dynamic?: boolean; mass?: number; friction?: number };
|
|
16
|
-
joint?: { type: 'free' | 'hinge' | 'slide' | 'fixed' };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ParseOptions {
|
|
20
|
-
name?: string;
|
|
21
|
-
gravity?: Vec3;
|
|
22
|
-
timestep?: number;
|
|
23
|
-
substeps?: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function buildGeom(kind: PrimitiveKind | undefined, size: Vec3, color?: string): Geom {
|
|
27
|
-
const [a, b, c] = size;
|
|
28
|
-
switch (kind) {
|
|
29
|
-
case 'sphere': return { type: 'sphere', radius: a, color, group: 'default' };
|
|
30
|
-
case 'cylinder': return { type: 'cylinder', radius: a, height: b, color, group: 'default' };
|
|
31
|
-
case 'capsule': return { type: 'capsule', radius: a, height: b, color, group: 'default' };
|
|
32
|
-
case 'plane': return { type: 'plane', size: [a, b], color, group: 'default' };
|
|
33
|
-
default: return { type: 'box', size: [a, b, c] as Vec3, color, group: 'default' };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function parseThreeScene(entities: ThreeEntity[], opts: ParseOptions = {}): SceneDoc {
|
|
38
|
-
const scene: DocEntity[] = entities.map(e => {
|
|
39
|
-
const pos = e.obj.position.toArray() as Vec3;
|
|
40
|
-
const q = e.obj.quaternion;
|
|
41
|
-
const hasRot = Math.abs(q.x) + Math.abs(q.y) + Math.abs(q.z) > 1e-6;
|
|
42
|
-
const quat = hasRot ? [q.w, q.x, q.y, q.z] as [number,number,number,number] : undefined;
|
|
43
|
-
const transform = { pos, ...(quat ? { quat } : {}) };
|
|
44
|
-
|
|
45
|
-
if (e.kind === 'robot' && e.asset) {
|
|
46
|
-
const scl = (e.obj.scale.x + e.obj.scale.y + e.obj.scale.z) / 3;
|
|
47
|
-
return {
|
|
48
|
-
name: e.name,
|
|
49
|
-
asset: e.asset,
|
|
50
|
-
transform: { ...transform, ...(Math.abs(scl - 1) > 0.001 ? { scale: scl } : {}) }
|
|
51
|
-
} satisfies DocEntity;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const dyn = e.physics?.dynamic ?? false;
|
|
55
|
-
const physics: Physics = {
|
|
56
|
-
dynamic: dyn,
|
|
57
|
-
...(e.physics?.mass !== undefined ? { mass: e.physics.mass } : {}),
|
|
58
|
-
...(e.physics?.friction !== undefined ? { friction: e.physics.friction } : {})
|
|
59
|
-
};
|
|
60
|
-
const joint: Joint | undefined = e.joint
|
|
61
|
-
? { type: e.joint.type }
|
|
62
|
-
: dyn ? { type: 'free' } : undefined;
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
name: e.name,
|
|
66
|
-
transform,
|
|
67
|
-
geoms: [buildGeom(e.geom, e.size ?? [0.1, 0.1, 0.1], e.color)],
|
|
68
|
-
physics,
|
|
69
|
-
collider: {},
|
|
70
|
-
...(joint ? { joint } : {})
|
|
71
|
-
} satisfies DocEntity;
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
_format: 'simarena/scene@1.0',
|
|
76
|
-
name: opts.name ?? 'Untitled Scene',
|
|
77
|
-
sim: {
|
|
78
|
-
timestep: opts.timestep ?? 0.002,
|
|
79
|
-
gravity: opts.gravity ?? [0, 0, -9.81],
|
|
80
|
-
substeps: opts.substeps ?? 4
|
|
81
|
-
},
|
|
82
|
-
scene
|
|
83
|
-
};
|
|
84
|
-
}
|