@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
package/src/sync.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import * as THREE from
|
|
2
|
-
import { GLTFLoader } from
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
3
3
|
|
|
4
4
|
// ── Body index ─────────────────────────────────────────────────────────────────
|
|
5
5
|
// Maps entity names → frame indices (matches the order bodies appear in sim state).
|
|
6
6
|
|
|
7
7
|
export function buildBodyIdx(names: string[]): Map<string, number> {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const m = new Map<string, number>();
|
|
9
|
+
names.forEach((n, i) => m.set(n, i));
|
|
10
|
+
return m;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// ── Apply world-space transforms ───────────────────────────────────────────────
|
|
@@ -17,19 +17,20 @@ export function buildBodyIdx(names: string[]): Map<string, number> {
|
|
|
17
17
|
// Bodies not in the map are skipped automatically.
|
|
18
18
|
|
|
19
19
|
export function applyTransforms(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
xpos: Float32Array,
|
|
21
|
+
xquat: Float32Array,
|
|
22
|
+
bodyIdx: Map<string, number>,
|
|
23
|
+
bodies: Map<string, THREE.Object3D>,
|
|
24
24
|
): void {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
for (const [name, idx] of bodyIdx) {
|
|
26
|
+
const obj = bodies.get(name);
|
|
27
|
+
if (!obj) continue;
|
|
28
|
+
const p = idx * 3,
|
|
29
|
+
q = idx * 4;
|
|
30
|
+
obj.position.set(xpos[p]!, xpos[p + 1]!, xpos[p + 2]!);
|
|
31
|
+
// MuJoCo wxyz → Three.js xyzw
|
|
32
|
+
obj.quaternion.set(xquat[q + 1]!, xquat[q + 2]!, xquat[q + 3]!, xquat[q]!);
|
|
33
|
+
}
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// ── Hierarchy flattening ───────────────────────────────────────────────────────
|
|
@@ -37,44 +38,44 @@ export function applyTransforms(
|
|
|
37
38
|
// of the scene root. Flatten before sync, restore after.
|
|
38
39
|
|
|
39
40
|
export interface BodySnapshot {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
parent: THREE.Object3D;
|
|
42
|
+
pos: THREE.Vector3;
|
|
43
|
+
quat: THREE.Quaternion;
|
|
44
|
+
scale: THREE.Vector3;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export function flattenBodies(
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
bodies: Map<string, THREE.Object3D>,
|
|
49
|
+
scene: THREE.Scene,
|
|
49
50
|
): Map<string, BodySnapshot> {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
const saved = new Map<string, BodySnapshot>();
|
|
52
|
+
for (const [name, obj] of bodies) {
|
|
53
|
+
saved.set(name, {
|
|
54
|
+
parent: obj.parent ?? scene,
|
|
55
|
+
pos: obj.position.clone(),
|
|
56
|
+
quat: obj.quaternion.clone(),
|
|
57
|
+
scale: obj.scale.clone(),
|
|
58
|
+
});
|
|
59
|
+
if (obj.parent !== scene) scene.attach(obj);
|
|
60
|
+
}
|
|
61
|
+
return saved;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function restoreBodies(
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
flatBodies: Map<string, THREE.Object3D>,
|
|
66
|
+
saved: Map<string, BodySnapshot>,
|
|
66
67
|
): void {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
for (const [name, obj] of flatBodies) {
|
|
69
|
+
const snap = saved.get(name);
|
|
70
|
+
if (snap) {
|
|
71
|
+
snap.parent.add(obj);
|
|
72
|
+
obj.position.copy(snap.pos);
|
|
73
|
+
obj.quaternion.copy(snap.quat);
|
|
74
|
+
obj.scale.copy(snap.scale);
|
|
75
|
+
}
|
|
76
|
+
obj.matrixWorldAutoUpdate = true;
|
|
77
|
+
obj.matrixWorldNeedsUpdate = true;
|
|
78
|
+
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
// ── HTTP body loading ──────────────────────────────────────────────────────────
|
|
@@ -82,52 +83,55 @@ export function restoreBodies(
|
|
|
82
83
|
// Returns Map<name, Object3D> keyed by entity name, matching SceneDoc entity names.
|
|
83
84
|
|
|
84
85
|
export interface BodyInfo {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
id: number;
|
|
87
|
+
name: string;
|
|
88
|
+
fixed?: boolean;
|
|
89
|
+
size?: number;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
export interface LoadBodiesOpts {
|
|
92
|
-
|
|
93
|
+
onProgress?: (loaded: number, total: number) => void;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
const loader = new GLTFLoader();
|
|
96
97
|
|
|
97
98
|
export interface LoadBodiesResult {
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
bodies: Map<string, THREE.Object3D>;
|
|
100
|
+
manifest: BodyInfo[];
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
export async function loadBodies(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
httpBase: string,
|
|
105
|
+
parent: THREE.Object3D,
|
|
106
|
+
opts: LoadBodiesOpts = {},
|
|
106
107
|
): Promise<LoadBodiesResult> {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
108
|
+
const base = httpBase.replace(/\/$/, "");
|
|
109
|
+
const res = await fetch(`${base}/bodies`);
|
|
110
|
+
const manifest: BodyInfo[] = await res.json();
|
|
111
|
+
const bodies = new Map<string, THREE.Object3D>();
|
|
112
|
+
|
|
113
|
+
await Promise.all(
|
|
114
|
+
manifest.map(
|
|
115
|
+
(b) =>
|
|
116
|
+
new Promise<void>((resolve) => {
|
|
117
|
+
loader.load(`${base}/body/${b.id}.glb`, (gltf) => {
|
|
118
|
+
const mesh = gltf.scene;
|
|
119
|
+
mesh.traverse((c) => {
|
|
120
|
+
if ((c as THREE.Mesh).isMesh) {
|
|
121
|
+
(c as THREE.Mesh).castShadow = true;
|
|
122
|
+
(c as THREE.Mesh).receiveShadow = true;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
parent.add(mesh);
|
|
126
|
+
bodies.set(b.name, mesh);
|
|
127
|
+
opts.onProgress?.(bodies.size, manifest.length);
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
return { bodies, manifest };
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
// ── WebSocket sim transport ────────────────────────────────────────────────────
|
|
@@ -136,54 +140,54 @@ export async function loadBodies(
|
|
|
136
140
|
// Buffer length must be divisible by 7 (nbody*7 = pos+quat per body).
|
|
137
141
|
|
|
138
142
|
export interface SimOpts {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
onFps?: (fps: number) => void;
|
|
144
|
+
onParticles?: (positions: Float32Array) => void;
|
|
145
|
+
onConnect?: () => void;
|
|
146
|
+
onError?: (msg: string) => void;
|
|
147
|
+
onClose?: () => void;
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
export interface SimHandle {
|
|
147
|
-
|
|
151
|
+
close(): void;
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
export function connectSim(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
wsUrl: string,
|
|
156
|
+
nbody: number,
|
|
157
|
+
onFrame: (xpos: Float32Array, xquat: Float32Array) => void,
|
|
158
|
+
opts: SimOpts = {},
|
|
155
159
|
): SimHandle {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
160
|
+
const ws = new WebSocket(wsUrl);
|
|
161
|
+
ws.binaryType = "arraybuffer";
|
|
162
|
+
|
|
163
|
+
let frames = 0;
|
|
164
|
+
let lastTick = performance.now();
|
|
165
|
+
|
|
166
|
+
ws.onopen = () => opts.onConnect?.();
|
|
167
|
+
|
|
168
|
+
ws.onmessage = (e) => {
|
|
169
|
+
const buf = new Float32Array(e.data as ArrayBuffer);
|
|
170
|
+
const xpos = buf.subarray(0, nbody * 3);
|
|
171
|
+
const xquat = buf.subarray(nbody * 3, nbody * 7);
|
|
172
|
+
onFrame(xpos, xquat);
|
|
173
|
+
|
|
174
|
+
// Optional particle data appended after transforms
|
|
175
|
+
if (buf.length > nbody * 7 && opts.onParticles) {
|
|
176
|
+
const rest = buf.subarray(nbody * 7);
|
|
177
|
+
if (rest.length % 3 === 0) opts.onParticles(rest);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
frames++;
|
|
181
|
+
const now = performance.now();
|
|
182
|
+
if (now - lastTick >= 1000) {
|
|
183
|
+
opts.onFps?.(frames);
|
|
184
|
+
frames = 0;
|
|
185
|
+
lastTick = now;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
ws.onerror = () => opts.onError?.("WebSocket error");
|
|
190
|
+
ws.onclose = () => opts.onClose?.();
|
|
191
|
+
|
|
192
|
+
return { close: () => ws.close() };
|
|
189
193
|
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// ── Engine-agnostic simulation state protocol ─────────────────────────────────
|
|
2
|
+
// Types live in @simarena/viewport (the bridge layer), not in engine adapters.
|
|
3
|
+
// Tier 1: transforms (universal)
|
|
4
|
+
// Tier 2: sensors, qpos (most engines)
|
|
5
|
+
// Tier 3: contacts (MuJoCo/Isaac only)
|
|
6
|
+
|
|
7
|
+
export interface SimScene {
|
|
8
|
+
bodies: { name: string; id: number }[];
|
|
9
|
+
actuators: { name: string; range: [number, number] }[];
|
|
10
|
+
/** No engine-specific address offsets — adapters map internally. */
|
|
11
|
+
sensors: { name: string; dim: number }[];
|
|
12
|
+
/** Geom→body mapping for contact name resolution. */
|
|
13
|
+
geoms: { name: string; body: string }[];
|
|
14
|
+
timestep: number;
|
|
15
|
+
nbody: number;
|
|
16
|
+
nq: number;
|
|
17
|
+
nu: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Contact {
|
|
21
|
+
geom1: string;
|
|
22
|
+
geom2: string;
|
|
23
|
+
pos: [number, number, number];
|
|
24
|
+
/** Scalar contact force magnitude. */
|
|
25
|
+
force: number;
|
|
26
|
+
/** Contact frame normal (unit vector, first row of contact.frame). */
|
|
27
|
+
normal?: [number, number, number];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SimFrame {
|
|
31
|
+
time: number;
|
|
32
|
+
/** Flat world-space positions [x0,y0,z0, x1,...] per body. */
|
|
33
|
+
xpos: Float32Array;
|
|
34
|
+
/** Flat world-space quaternions wxyz [w0,x0,y0,z0, w1,...] per body. */
|
|
35
|
+
xquat: Float32Array;
|
|
36
|
+
qpos?: Float64Array;
|
|
37
|
+
ctrl?: Float64Array;
|
|
38
|
+
/** Named sensor readings: Record<sensorName, values[]>. */
|
|
39
|
+
sensors?: Record<string, number[]>;
|
|
40
|
+
/** Per-contact detail — Tier 3 only (MuJoCo/Isaac). */
|
|
41
|
+
contacts?: Contact[];
|
|
42
|
+
/** Engine-agnostic eval result — frontend narrows to EvalResult. */
|
|
43
|
+
output?: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Commands (consumer → engine) ──────────────────────────────────────────────
|
|
47
|
+
export type SimCommand =
|
|
48
|
+
| { type: "load"; scene: unknown }
|
|
49
|
+
| { type: "stop" }
|
|
50
|
+
| { type: "reset" }
|
|
51
|
+
| { type: "ctrl"; values: Float64Array }
|
|
52
|
+
| { type: "command"; values: Float32Array }
|
|
53
|
+
| { type: "setqpos"; qpos: Float64Array }
|
|
54
|
+
| { type: "pause" }
|
|
55
|
+
| { type: "resume" }
|
|
56
|
+
| {
|
|
57
|
+
type: "configure";
|
|
58
|
+
contacts?: boolean;
|
|
59
|
+
sensors?: boolean;
|
|
60
|
+
output?: boolean;
|
|
61
|
+
snapshotHz?: number;
|
|
62
|
+
speed?: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ── Events (engine → consumer) ────────────────────────────────────────────────
|
|
66
|
+
export type SimEvent =
|
|
67
|
+
| { type: "scene"; data: SimScene }
|
|
68
|
+
| { type: "frame"; data: SimFrame }
|
|
69
|
+
| { type: "status"; text: string }
|
|
70
|
+
| { type: "error"; message: string };
|
|
71
|
+
|
|
72
|
+
// ── Connection ────────────────────────────────────────────────────────────────
|
|
73
|
+
// A bidirectional channel to any sim engine — local Worker or remote server.
|
|
74
|
+
// Obtained from a transport-specific factory (connectWorker, connectWs, etc.).
|
|
75
|
+
export interface SimConnection {
|
|
76
|
+
send(cmd: SimCommand): void;
|
|
77
|
+
on(type: "scene", cb: (data: SimScene) => void): void;
|
|
78
|
+
on(type: "frame", cb: (data: SimFrame) => void): void;
|
|
79
|
+
on(type: "status", cb: (text: string) => void): void;
|
|
80
|
+
on(type: "error", cb: (message: string) => void): void;
|
|
81
|
+
off(type: string, cb: (...args: never[]) => void): void;
|
|
82
|
+
close(): void;
|
|
83
|
+
}
|