@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.
@@ -0,0 +1,253 @@
1
+ import * as THREE from "three";
2
+ import { LidarViz } from "../sensors/index.js";
3
+ import { applyTransforms, buildBodyIdx } from "../sync.js";
4
+ import type { SimScene as SimSceneMsg } from "../types.js";
5
+ import { ContactViz } from "../sensors/contact.js";
6
+ import type { SimCamera, VisualScene } from "./types.js";
7
+
8
+ /** The subset of state that SimScene needs for per-frame visualization. */
9
+ export interface VizState {
10
+ showContacts: boolean;
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ contacts: any[];
13
+ sensordata: Record<string, number[]> | null;
14
+ }
15
+
16
+ /**
17
+ * SimScene — Three.js renderer for simulation visualization.
18
+ *
19
+ * Lifecycle:
20
+ * 1. new SimScene(scene)
21
+ * 2. setBodies(map) — register body objects
22
+ * 3. init(msg, visual) — build bodyIdx, proxy meshes, lidars, thumb cameras
23
+ * 4. Per frame: applyFrame() → updateViz() → renderThumbnails()
24
+ * 5. dispose()
25
+ */
26
+ export class SimScene {
27
+ static readonly THUMB_W = 160;
28
+ static readonly THUMB_H = 100;
29
+
30
+ #scene: THREE.Scene;
31
+ #bodies: Map<string, THREE.Object3D> = new Map();
32
+ #bodyIdx: Map<string, number> = new Map();
33
+ #proxies: THREE.Object3D[] = [];
34
+ #lidars: LidarViz[] = [];
35
+ #contactViz: ContactViz;
36
+ #thumbCams: {
37
+ name: string;
38
+ cam: THREE.PerspectiveCamera;
39
+ entityName?: string;
40
+ lookAtName?: string;
41
+ }[] = [];
42
+ #cameras: SimCamera[] = [];
43
+ #thumbTarget: THREE.WebGLRenderTarget | null = null;
44
+
45
+ /** Config list — updated on init(). */
46
+ thumbConfigs: { key: string; label: string }[] = [];
47
+
48
+ constructor(scene: THREE.Scene) {
49
+ this.#scene = scene;
50
+ this.#contactViz = new ContactViz(scene);
51
+ }
52
+
53
+ // ── Body management ────────────────────────────────────────────────────────
54
+
55
+ setBodies(bodies: Map<string, THREE.Object3D>): void {
56
+ this.#bodies = bodies;
57
+ }
58
+
59
+ get bodies(): Map<string, THREE.Object3D> {
60
+ return this.#bodies;
61
+ }
62
+
63
+ // ── Lifecycle ──────────────────────────────────────────────────────────────
64
+
65
+ init(msg: SimSceneMsg, visual: VisualScene): void {
66
+ this.#bodyIdx.clear();
67
+ buildBodyIdx(msg.bodies.map((b) => b.name)).forEach((v, k) => this.#bodyIdx.set(k, v));
68
+ this.#cameras = visual.cameras;
69
+ this.#initThumbCams(visual.cameras);
70
+ this.#createProxies(visual);
71
+ this.#createLidars(visual);
72
+ }
73
+
74
+ // ── Per-frame updates ──────────────────────────────────────────────────────
75
+
76
+ applyFrame(xpos: Float32Array, xquat: Float32Array): void {
77
+ applyTransforms(xpos, xquat, this.#bodyIdx, this.#bodies);
78
+ }
79
+
80
+ updateViz(state: VizState): void {
81
+ this.#syncThumbCams();
82
+ this.#contactViz.visible = state.showContacts;
83
+ this.#contactViz.update(state.contacts as Parameters<ContactViz["update"]>[0]);
84
+ for (const viz of this.#lidars) viz.update(state.sensordata?.[viz.name] ?? null);
85
+ }
86
+
87
+ // ── Thumbnail cameras ──────────────────────────────────────────────────────
88
+
89
+ renderThumbnails(
90
+ renderer: THREE.WebGLRenderer,
91
+ ctxs: (CanvasRenderingContext2D | null)[],
92
+ running: boolean,
93
+ time: number,
94
+ ): void {
95
+ if (!running || !this.#thumbCams.length) return;
96
+ if (Math.round(time * 50) % 3 !== 0) return;
97
+ const W = SimScene.THUMB_W;
98
+ const H = SimScene.THUMB_H;
99
+ const target = this.#getThumbTarget();
100
+ const prev = renderer.outputColorSpace;
101
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
102
+ for (let i = 0; i < this.#thumbCams.length; i++) {
103
+ const ctx2d = ctxs[i];
104
+ if (!ctx2d) continue;
105
+ renderer.setRenderTarget(target);
106
+ renderer.render(this.#scene, this.#thumbCams[i]!.cam);
107
+ renderer.setRenderTarget(null);
108
+ const buf = new Uint8Array(W * H * 4);
109
+ renderer.readRenderTargetPixels(target, 0, 0, W, H, buf);
110
+ const img = ctx2d.createImageData(W, H);
111
+ for (let y = 0; y < H; y++) {
112
+ const src = (H - 1 - y) * W * 4;
113
+ img.data.set(buf.subarray(src, src + W * 4), y * W * 4);
114
+ }
115
+ ctx2d.putImageData(img, 0, 0);
116
+ }
117
+ renderer.outputColorSpace = prev;
118
+ }
119
+
120
+ getThumbCam(key: string): THREE.PerspectiveCamera | null {
121
+ return this.#thumbCams.find((c) => c.name === key)?.cam ?? null;
122
+ }
123
+
124
+ getThumbConfig(key: string): SimCamera | null {
125
+ return this.#cameras.find((c) => c.name === key) ?? null;
126
+ }
127
+
128
+ // ── Dispose ────────────────────────────────────────────────────────────────
129
+
130
+ dispose(): void {
131
+ for (const viz of this.#lidars) {
132
+ this.#scene.remove(viz.group);
133
+ viz.dispose();
134
+ }
135
+ this.#lidars.length = 0;
136
+
137
+ for (const obj of this.#proxies) {
138
+ this.#scene.remove(obj);
139
+ obj.traverse((c) => {
140
+ if (c instanceof THREE.Mesh) {
141
+ c.geometry.dispose();
142
+ (c.material as THREE.Material).dispose();
143
+ }
144
+ });
145
+ }
146
+ this.#proxies.length = 0;
147
+
148
+ this.#contactViz.dispose();
149
+ this.#thumbCams = [];
150
+ this.#cameras = [];
151
+ this.thumbConfigs = [];
152
+ this.#thumbTarget?.dispose();
153
+ this.#thumbTarget = null;
154
+ this.#bodyIdx.clear();
155
+ }
156
+
157
+ // ── Private ────────────────────────────────────────────────────────────────
158
+
159
+ #getThumbTarget(): THREE.WebGLRenderTarget {
160
+ if (!this.#thumbTarget) {
161
+ this.#thumbTarget = new THREE.WebGLRenderTarget(SimScene.THUMB_W, SimScene.THUMB_H, {
162
+ minFilter: THREE.LinearFilter,
163
+ magFilter: THREE.LinearFilter,
164
+ format: THREE.RGBAFormat,
165
+ type: THREE.UnsignedByteType,
166
+ });
167
+ this.#thumbTarget.texture.colorSpace = THREE.SRGBColorSpace;
168
+ }
169
+ return this.#thumbTarget;
170
+ }
171
+
172
+ #syncThumbCams(): void {
173
+ for (const tc of this.#thumbCams) {
174
+ if (!tc.entityName) continue;
175
+ const obj = this.#bodies.get(tc.entityName);
176
+ if (obj) {
177
+ tc.cam.position.copy(obj.position);
178
+ tc.cam.quaternion.copy(obj.quaternion);
179
+ }
180
+ if (tc.lookAtName) {
181
+ const target = this.#bodies.get(tc.lookAtName);
182
+ if (target) tc.cam.lookAt(target.position);
183
+ }
184
+ }
185
+ }
186
+
187
+ #initThumbCams(cameras: SimCamera[]): void {
188
+ const W = SimScene.THUMB_W;
189
+ const H = SimScene.THUMB_H;
190
+ this.#thumbCams = cameras.map((c) => {
191
+ const cam = new THREE.PerspectiveCamera(c.fov, W / H, 0.1, 100);
192
+ cam.position.set(...c.position);
193
+ cam.lookAt(...c.lookAt);
194
+ return { name: c.name, cam, entityName: c.entityName, lookAtName: c.lookAtName };
195
+ });
196
+ this.thumbConfigs = this.#thumbCams.map((c) => ({ key: c.name, label: c.name }));
197
+ }
198
+
199
+ #createProxies(visual: VisualScene): void {
200
+ for (const body of visual.bodies) {
201
+ if (body.kind !== "rigid") continue;
202
+ if (this.#bodies.has(body.name)) continue;
203
+ const grp = new THREE.Group();
204
+ grp.position.set(...body.pos);
205
+ if (body.quat) {
206
+ const [w, x, y, z] = body.quat;
207
+ grp.quaternion.set(x, y, z, w);
208
+ }
209
+ for (const geom of body.geoms) {
210
+ if (geom.type === "box") {
211
+ const [sx, sy, sz] = geom.size as [number, number, number];
212
+ const mesh = new THREE.Mesh(
213
+ new THREE.BoxGeometry(sx, sy, sz),
214
+ new THREE.MeshStandardMaterial({
215
+ color: 0x1f1f3e,
216
+ roughness: 0.7,
217
+ metalness: 0.2,
218
+ transparent: true,
219
+ opacity: 0.9,
220
+ }),
221
+ );
222
+ grp.add(mesh);
223
+ }
224
+ // TODO: add sphere, cylinder, capsule, mesh (GLB) handling
225
+ }
226
+ if (grp.children.length > 0) {
227
+ this.#scene.add(grp);
228
+ this.#bodies.set(body.name, grp);
229
+ this.#proxies.push(grp);
230
+ }
231
+ }
232
+ }
233
+
234
+ #createLidars(visual: VisualScene): void {
235
+ for (const s of visual.sensors) {
236
+ if (s.type !== "lidar") continue;
237
+ const body = this.#bodies.get(s.parentBody ?? s.name);
238
+ if (!body) continue;
239
+ const ch = s.channels ?? 1;
240
+ const viz = new LidarViz(s.name, body, {
241
+ pattern: ch > 1 ? "3d" : "2d",
242
+ channels: ch,
243
+ horizontalSamples: s.horizontalSamples ?? 120,
244
+ horizontalFov: s.horizontalFov ?? [-180, 180],
245
+ verticalFov: s.verticalFov as [number, number] | undefined,
246
+ range: s.range ?? [0.1, 10],
247
+ height: 0.3,
248
+ });
249
+ this.#scene.add(viz.group);
250
+ this.#lidars.push(viz);
251
+ }
252
+ }
253
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * VisualScene — engine-agnostic visual description of a scene.
3
+ *
4
+ * Designed to be consumed by any renderer (Three.js, wgpu-rs headless, etc.).
5
+ * Produced by @simarena/format (SceneDoc → VisualScene) or any sim engine
6
+ * (MuJoCo, Genesis, Newton, USD, ...).
7
+ *
8
+ * Asset tables (materials, textures, meshes) deduplicate shared resources.
9
+ * GeomDesc references them by name; renderers build GPU resources once per
10
+ * unique material/mesh and batch draw calls accordingly.
11
+ */
12
+
13
+ // ── Asset tables ──────────────────────────────────────────────────────────────
14
+
15
+ /** PBR material — engine-agnostic, all values 0-1. */
16
+ export interface MaterialDesc {
17
+ color?: [number, number, number, number]; // RGBA 0-1
18
+ roughness?: number;
19
+ metalness?: number;
20
+ texture?: string; // ref to VisualScene.textures
21
+ }
22
+
23
+ // ── Geometry ──────────────────────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Geometry primitive.
27
+ * size convention: box=[w,h,d] | sphere=[r] | cylinder=[r,h] | capsule=[r,h] | plane=[w,d] | mesh=[]
28
+ */
29
+ export interface GeomDesc {
30
+ type: string; // 'box' | 'sphere' | 'capsule' | 'cylinder' | 'plane' | 'mesh'
31
+ size: number[];
32
+ pos?: [number, number, number];
33
+ rot?: [number, number, number]; // Euler degrees XYZ
34
+ quat?: [number, number, number, number]; // WXYZ
35
+ material?: string | MaterialDesc; // name ref OR inline MaterialDesc
36
+ mesh?: string; // name ref to VisualScene.meshes
37
+ meshUrl?: string; // direct URL (convenience; prefer mesh ref)
38
+ }
39
+
40
+ // ── Bodies ────────────────────────────────────────────────────────────────────
41
+
42
+ /** Rigid body — updated per frame via transform (pos + quat). */
43
+ export interface RigidBodyDesc {
44
+ kind: "rigid";
45
+ name: string;
46
+ pos: [number, number, number];
47
+ rot?: [number, number, number];
48
+ quat?: [number, number, number, number];
49
+ geoms: GeomDesc[];
50
+ parent?: string;
51
+ }
52
+
53
+ /** Deformable body — updated per frame via per-vertex positions. */
54
+ export interface SoftBodyDesc {
55
+ kind: "soft";
56
+ name: string;
57
+ /** Total vertex count — pre-allocate GPU buffers. */
58
+ vertexCount: number;
59
+ /** Initial topology mesh (GLB/OBJ). Triangle indices stay fixed. */
60
+ mesh?: string; // ref to VisualScene.meshes
61
+ meshUrl?: string; // direct URL fallback
62
+ /** Inline triangle indices (alternative to mesh ref). */
63
+ indices?: number[];
64
+ material?: string | MaterialDesc;
65
+ doubleSided?: boolean;
66
+ }
67
+
68
+ /** Bone-driven mesh — GPU skeletal animation. One draw call; efficient for articulated robots. */
69
+ export interface SkinnedBodyDesc {
70
+ kind: "skinned";
71
+ name: string;
72
+ /** GLB with embedded skeleton and skin weights. */
73
+ mesh?: string;
74
+ meshUrl?: string;
75
+ /** Number of bones — pre-allocate bone transform buffer. */
76
+ boneCount: number;
77
+ material?: string | MaterialDesc;
78
+ }
79
+
80
+ /** GPU-instanced copies of the same shape. One draw call for `count` copies. */
81
+ export interface InstancedBodyDesc {
82
+ kind: "instanced";
83
+ name: string;
84
+ /** Number of instances. */
85
+ count: number;
86
+ /** Shared geometry for all instances. */
87
+ geom: GeomDesc;
88
+ /** Per-instance color overrides (optional). */
89
+ colors?: [number, number, number, number][];
90
+ }
91
+
92
+ export type BodyDesc = RigidBodyDesc | SoftBodyDesc | SkinnedBodyDesc | InstancedBodyDesc;
93
+
94
+ // ── Lights ────────────────────────────────────────────────────────────────────
95
+
96
+ export type LightType = "point" | "directional" | "spot" | "ambient" | "hemisphere";
97
+
98
+ export interface LightDesc {
99
+ type: LightType;
100
+ name: string;
101
+ pos?: [number, number, number];
102
+ color?: [number, number, number]; // RGB 0-1
103
+ groundColor?: [number, number, number]; // hemisphere bottom color
104
+ intensity?: number;
105
+ castShadow?: boolean;
106
+ dir?: [number, number, number];
107
+ range?: number;
108
+ angle?: number;
109
+ }
110
+
111
+ // ── Cameras ───────────────────────────────────────────────────────────────────
112
+
113
+ export interface SimCamera {
114
+ name: string;
115
+ fov: number;
116
+ position: [number, number, number];
117
+ lookAt: [number, number, number];
118
+ entityName?: string;
119
+ lookAtName?: string;
120
+ }
121
+
122
+ // ── Sensors ───────────────────────────────────────────────────────────────────
123
+
124
+ export interface SensorDesc {
125
+ name: string;
126
+ type: string; // 'lidar' | 'camera' | ...
127
+ parentBody?: string;
128
+ channels?: number;
129
+ horizontalSamples?: number;
130
+ horizontalFov?: [number, number];
131
+ verticalFov?: [number, number];
132
+ range?: [number, number];
133
+ }
134
+
135
+ // ── Dynamic elements ──────────────────────────────────────────────────────────
136
+
137
+ /** Point cloud — MPM particles, granular, fluid, debug markers. */
138
+ export interface ParticleDesc {
139
+ name: string;
140
+ material?: string | MaterialDesc;
141
+ size?: number;
142
+ sizeAttenuation?: boolean;
143
+ }
144
+
145
+ // ── Gaussian Splats ───────────────────────────────────────────────────────────
146
+
147
+ /**
148
+ * 3D Gaussian Splat scene — photorealistic environments from NeRF/3DGS reconstruction.
149
+ * Rendered via @sparkjsdev/spark (Three.js-integrated Gaussian rasterizer).
150
+ * Splat data is loaded from a .ply, .splat, or .spz file.
151
+ */
152
+ export interface GaussianSplatDesc {
153
+ name: string;
154
+ /** URL to .ply / .splat / .spz file. */
155
+ url?: string;
156
+ /** Name ref to meshes table. */
157
+ splat?: string;
158
+ pos?: [number, number, number];
159
+ quat?: [number, number, number, number];
160
+ scale?: number;
161
+ }
162
+
163
+ // ── Environment ───────────────────────────────────────────────────────────────
164
+
165
+ export interface EnvironmentDesc {
166
+ /** Background color [R,G,B] 0-1 or HDR/EXR URL. */
167
+ background?: [number, number, number] | string;
168
+ fog?: { color: [number, number, number]; near: number; far: number };
169
+ }
170
+
171
+ // ── Root ──────────────────────────────────────────────────────────────────────
172
+
173
+ export interface VisualScene {
174
+ // ── Shared asset tables ──────────────────────────────────────────────────
175
+ /** name → PBR material. Geoms reference by name for GPU batching. */
176
+ materials?: Record<string, MaterialDesc>;
177
+ /** name → texture URL. Materials reference by name. */
178
+ textures?: Record<string, string>;
179
+ /** name → mesh URL (.stl/.obj/.glb). Geoms reference by name. */
180
+ meshes?: Record<string, string>;
181
+
182
+ // ── Scene graph ──────────────────────────────────────────────────────────
183
+ /** Rigid + soft bodies. Discriminate with body.kind. */
184
+ bodies: BodyDesc[];
185
+ lights: LightDesc[];
186
+ cameras: SimCamera[];
187
+ sensors: SensorDesc[];
188
+ /** Per-frame point cloud channels (particles, fluid, granular). */
189
+ particles?: ParticleDesc[];
190
+ /** Gaussian Splat environments — loaded via @sparkjsdev/spark. */
191
+ splats?: GaussianSplatDesc[];
192
+ environment?: EnvironmentDesc;
193
+ }
package/src/viewport.ts CHANGED
@@ -1,78 +1,91 @@
1
- import * as THREE from 'three';
2
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
1
+ import * as THREE from "three";
2
+ import { OrbitControls } from "three/addons/controls/OrbitControls.js";
3
3
 
4
4
  export interface ViewportOpts {
5
- fov?: number;
6
- near?: number;
7
- far?: number;
8
- pos?: [number, number, number];
9
- target?: [number, number, number];
10
- grid?: number;
5
+ fov?: number;
6
+ near?: number;
7
+ far?: number;
8
+ pos?: [number, number, number];
9
+ target?: [number, number, number];
10
+ grid?: number;
11
11
  }
12
12
 
13
13
  export interface Viewport {
14
- scene: THREE.Scene;
15
- camera: THREE.PerspectiveCamera;
16
- renderer: THREE.WebGLRenderer;
17
- orbit: OrbitControls;
18
- grid: THREE.GridHelper | null;
19
- resize(): void;
20
- dispose(): void;
14
+ scene: THREE.Scene;
15
+ camera: THREE.PerspectiveCamera;
16
+ renderer: THREE.WebGLRenderer;
17
+ orbit: OrbitControls;
18
+ grid: THREE.GridHelper | null;
19
+ resize(): void;
20
+ dispose(): void;
21
21
  }
22
22
 
23
23
  export function createViewport(
24
- canvas: HTMLCanvasElement,
25
- container: HTMLElement,
26
- opts: ViewportOpts = {}
24
+ canvas: HTMLCanvasElement,
25
+ container: HTMLElement,
26
+ opts: ViewportOpts = {},
27
27
  ): Viewport {
28
- const { fov = 60, near = 0.1, far = 100, pos = [1.5, -1.5, 1.5], target = [0, 0, 0.3], grid = 2 } = opts;
28
+ const {
29
+ fov = 60,
30
+ near = 0.1,
31
+ far = 100,
32
+ pos = [1.5, -1.5, 1.5],
33
+ target = [0, 0, 0.3],
34
+ grid = 2,
35
+ } = opts;
29
36
 
30
- THREE.Object3D.DEFAULT_UP.set(0, 0, 1);
37
+ THREE.Object3D.DEFAULT_UP.set(0, 0, 1);
31
38
 
32
- const scene = new THREE.Scene();
39
+ const scene = new THREE.Scene();
33
40
 
34
- const camera = new THREE.PerspectiveCamera(fov, container.clientWidth / container.clientHeight, near, far);
35
- camera.position.set(...pos);
36
- camera.lookAt(...target);
41
+ const camera = new THREE.PerspectiveCamera(
42
+ fov,
43
+ container.clientWidth / container.clientHeight,
44
+ near,
45
+ far,
46
+ );
47
+ camera.position.set(...pos);
48
+ camera.lookAt(...target);
37
49
 
38
- const gl = canvas.getContext('webgl2');
39
- const renderer = new THREE.WebGLRenderer({ canvas, context: gl || undefined, antialias: true });
40
- renderer.setSize(container.clientWidth, container.clientHeight);
41
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
42
- renderer.shadowMap.enabled = true;
43
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
44
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
45
- renderer.toneMappingExposure = 1.0;
46
- renderer.outputColorSpace = THREE.SRGBColorSpace;
50
+ const gl = canvas.getContext("webgl2");
51
+ const renderer = new THREE.WebGLRenderer({ canvas, context: gl || undefined, antialias: true });
52
+ renderer.setSize(container.clientWidth, container.clientHeight);
53
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
54
+ renderer.shadowMap.enabled = true;
55
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
56
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
57
+ renderer.toneMappingExposure = 1.0;
58
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
47
59
 
48
- const orbit = new OrbitControls(camera, renderer.domElement);
49
- orbit.enableDamping = true;
50
- orbit.dampingFactor = 0.05;
51
- orbit.target.set(...target);
52
- orbit.update();
60
+ const orbit = new OrbitControls(camera, renderer.domElement);
61
+ orbit.enableDamping = true;
62
+ orbit.dampingFactor = 0.05;
63
+ orbit.target.set(...target);
64
+ orbit.update();
53
65
 
54
- let gridHelper: THREE.GridHelper | null = null;
55
- if (grid > 0) {
56
- gridHelper = new THREE.GridHelper(grid * 10, 20, 0x444444, 0x222222);
57
- gridHelper.rotation.x = Math.PI / 2;
58
- scene.add(gridHelper);
59
- }
66
+ let gridHelper: THREE.GridHelper | null = null;
67
+ if (grid > 0) {
68
+ gridHelper = new THREE.GridHelper(grid * 10, 20, 0x444444, 0x222222);
69
+ gridHelper.rotation.x = Math.PI / 2;
70
+ scene.add(gridHelper);
71
+ }
60
72
 
61
- function resize() {
62
- const w = container.clientWidth, h = container.clientHeight;
63
- camera.aspect = w / h;
64
- camera.updateProjectionMatrix();
65
- renderer.setSize(w, h);
66
- }
73
+ function resize() {
74
+ const w = container.clientWidth,
75
+ h = container.clientHeight;
76
+ camera.aspect = w / h;
77
+ camera.updateProjectionMatrix();
78
+ renderer.setSize(w, h);
79
+ }
67
80
 
68
- const obs = new ResizeObserver(resize);
69
- obs.observe(container);
81
+ const obs = new ResizeObserver(resize);
82
+ obs.observe(container);
70
83
 
71
- function dispose() {
72
- obs.disconnect();
73
- renderer.dispose();
74
- orbit.dispose();
75
- }
84
+ function dispose() {
85
+ obs.disconnect();
86
+ renderer.dispose();
87
+ orbit.dispose();
88
+ }
76
89
 
77
- return { scene, camera, renderer, orbit, grid: gridHelper, resize, dispose };
90
+ return { scene, camera, renderer, orbit, grid: gridHelper, resize, dispose };
78
91
  }