@wave3d/core 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/_virtual/_rolldown/runtime.js +13 -0
  4. package/dist/config/model.d.ts +240 -0
  5. package/dist/config/model.js +496 -0
  6. package/dist/config/model.js.map +1 -0
  7. package/dist/core-loader.d.ts +10 -0
  8. package/dist/core-loader.js +12 -0
  9. package/dist/core-loader.js.map +1 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +3 -0
  12. package/dist/presets.d.ts +8 -0
  13. package/dist/presets.js +544 -0
  14. package/dist/presets.js.map +1 -0
  15. package/dist/renderer/WaveGeometry.d.ts +31 -0
  16. package/dist/renderer/WaveGeometry.js +92 -0
  17. package/dist/renderer/WaveGeometry.js.map +1 -0
  18. package/dist/renderer/WaveRenderer.d.ts +232 -0
  19. package/dist/renderer/WaveRenderer.js +1118 -0
  20. package/dist/renderer/WaveRenderer.js.map +1 -0
  21. package/dist/renderer/heroPalette.d.ts +10 -0
  22. package/dist/renderer/heroPalette.js +34 -0
  23. package/dist/renderer/heroPalette.js.map +1 -0
  24. package/dist/renderer/index.d.ts +4 -0
  25. package/dist/renderer/index.js +4 -0
  26. package/dist/renderer/palette.d.ts +67 -0
  27. package/dist/renderer/palette.js +535 -0
  28. package/dist/renderer/palette.js.map +1 -0
  29. package/dist/renderer/shaders.js +545 -0
  30. package/dist/renderer/shaders.js.map +1 -0
  31. package/dist/shell/createWave.d.ts +58 -0
  32. package/dist/shell/createWave.js +161 -0
  33. package/dist/shell/createWave.js.map +1 -0
  34. package/dist/shell/poster.js +64 -0
  35. package/dist/shell/poster.js.map +1 -0
  36. package/dist/shell/probe.js +34 -0
  37. package/dist/shell/probe.js.map +1 -0
  38. package/dist/standalone/wave3d.standalone.js +13507 -0
  39. package/dist/standalone.d.ts +13 -0
  40. package/dist/standalone.js +17 -0
  41. package/dist/standalone.js.map +1 -0
  42. package/dist/studio/StudioWaveRenderer.d.ts +187 -0
  43. package/dist/studio/StudioWaveRenderer.js +905 -0
  44. package/dist/studio/StudioWaveRenderer.js.map +1 -0
  45. package/dist/studio/index.d.ts +3 -0
  46. package/dist/studio/index.js +3 -0
  47. package/dist/studio/randomize.d.ts +28 -0
  48. package/dist/studio/randomize.js +264 -0
  49. package/dist/studio/randomize.js.map +1 -0
  50. package/dist/util/base64.js +14 -0
  51. package/dist/util/base64.js.map +1 -0
  52. package/dist/util/math.js +18 -0
  53. package/dist/util/math.js.map +1 -0
  54. package/package.json +76 -0
  55. package/skills/wave3d/SKILL.md +158 -0
@@ -0,0 +1,905 @@
1
+ import { roundTo } from "../util/math.js";
2
+ import { DEFAULT_LIGHT_POSITION, createLight } from "../config/model.js";
3
+ import { FRAME_W, WaveRenderer, hexToLinearVec3 } from "../renderer/WaveRenderer.js";
4
+ import * as THREE from "three";
5
+ //#region src/studio/StudioWaveRenderer.ts
6
+ const MINIMAP_VANTAGE = new THREE.Vector3(.85, .6, 1).normalize();
7
+ var StudioWaveRenderer = class extends WaveRenderer {
8
+ /** Set while the panel drives the camera, so orbit's 'change' doesn't re-refresh the
9
+ * panel mid-drag (the panel already knows the new value). */
10
+ suppressCameraChange = false;
11
+ miniBox = new THREE.Box3();
12
+ miniSphere = new THREE.Sphere();
13
+ miniTmpA = new THREE.Vector3();
14
+ miniTmpB = new THREE.Vector3();
15
+ miniPrevColor = new THREE.Color();
16
+ miniSize = new THREE.Vector2();
17
+ miniBlendPrev = [];
18
+ miniTransPrev = [];
19
+ cameraRigOn = false;
20
+ cameraRigCollapsed = false;
21
+ minimapCamera;
22
+ camMarker;
23
+ /** Gold markers in the minimap, one per rig light (positions/colours tracked live). */
24
+ minimapLights = [];
25
+ /** Shown in the rig when no light has been added yet, so the light is always visible there.
26
+ * Matches where "drag in 3D" creates the first light, so the marker doesn't jump when added. */
27
+ defaultRigLight = createLight({ ...DEFAULT_LIGHT_POSITION }, 1);
28
+ minimapBtn;
29
+ overlay = new THREE.Scene();
30
+ raycaster = new THREE.Raycaster();
31
+ orbit;
32
+ transform;
33
+ /** Whether the main view orbit/zoom/pan is on (studio); off for the embed. */
34
+ mainOrbitOn = false;
35
+ lightHelpers = [];
36
+ /** Which 3D-editing gizmo is active: none, dragging lights, or dragging the wave/waves. */
37
+ editMode = "none";
38
+ selectedLight = 0;
39
+ /** Wave/wave drag handles: index 0 = the whole-wave box (moves config.position); 1..N =
40
+ * per-wave spheres (move each layer's offset), shown only when there's >1 wave. */
41
+ waveHelpers = [];
42
+ selectedWave = 0;
43
+ /** Gizmo operation: "translate" moves the handle, "rotate" spins the whole wave. */
44
+ gizmoMode = "translate";
45
+ /** Active free screen-plane drag of a handle (grab anywhere on the marker, camera locked). */
46
+ dragState;
47
+ dragPlane = new THREE.Plane();
48
+ /** Active left-drag camera pan in edit mode (the press missed every handle → move the view). */
49
+ panState;
50
+ /** Camera snapshot taken when entering a 3D-edit mode and restored verbatim on exit, so the
51
+ * view returns exactly where it was (position + ortho zoom + up) rather than snapping to the
52
+ * authored hero framing. */
53
+ returnCamera = null;
54
+ /** Set by the panel: fired after a gizmo drag/selection so sliders can refresh. */
55
+ onLightsChanged;
56
+ /** Set by the panel: fired after orbit moves the camera so sliders can refresh. */
57
+ onCameraChanged;
58
+ /** Set by the panel: fired after a wave/wave gizmo drag/selection so the position and
59
+ * per-wave offset sliders can refresh. */
60
+ onWaveChanged;
61
+ /** True while any drag-in-3D gizmo owns the camera (light or wave). */
62
+ get editing() {
63
+ return this.editMode !== "none";
64
+ }
65
+ isLightEditMode() {
66
+ return this.editMode === "light";
67
+ }
68
+ isWaveEditMode() {
69
+ return this.editMode === "wave";
70
+ }
71
+ /** Toggle 3D light editing: show draggable light handles; off restores the prior view. */
72
+ async setLightEditMode(on) {
73
+ await this.setEditMode(on ? "light" : "none");
74
+ }
75
+ /** Toggle 3D wave/wave editing: drag the whole wave (and each wave when there's >1). */
76
+ async setWaveEditMode(on) {
77
+ await this.setEditMode(on ? "wave" : "none");
78
+ }
79
+ /** Enter/leave/switch a 3D-edit mode. Modes are mutually exclusive — turning one on turns
80
+ * the other off. The camera is snapshotted on the first entry and restored on the final exit
81
+ * (so light↔wave switches keep the same return view). */
82
+ async setEditMode(mode) {
83
+ if (mode === this.editMode) return;
84
+ const prev = this.editMode;
85
+ if (prev !== "none") {
86
+ if (this.transform) this.transform.enabled = false;
87
+ this.transform?.detach();
88
+ if (prev === "light") this.clearLightHelpers();
89
+ else this.clearWaveHelpers();
90
+ }
91
+ this.editMode = mode;
92
+ if (prev === "light") this.restoreReturnCamera();
93
+ if (mode === "none") {
94
+ if (this.orbit) this.orbit.enabled = this.mainOrbitOn;
95
+ this.setOrbitForEdit(false);
96
+ this.dragState = void 0;
97
+ this.renderOnce();
98
+ return;
99
+ }
100
+ await this.ensureGizmo();
101
+ if (this.editMode !== mode) return;
102
+ if (this.orbit) this.orbit.enabled = true;
103
+ this.setOrbitForEdit(true);
104
+ this.gizmoMode = "translate";
105
+ this.transform?.setMode("translate");
106
+ this.transform?.setSpace("world");
107
+ if (this.transform) this.transform.enabled = true;
108
+ if (mode === "light") {
109
+ this.captureReturnCamera();
110
+ this.syncLightHelpers();
111
+ this.frameEditCamera();
112
+ this.selectLight(Math.min(this.selectedLight, Math.max(0, this.lightHelpers.length - 1)));
113
+ } else {
114
+ this.syncWaveHelpers();
115
+ this.selectWaveHandle(Math.min(this.selectedWave, Math.max(0, this.waveHelpers.length - 1)));
116
+ }
117
+ this.renderOnce();
118
+ }
119
+ /** In edit mode, take PAN off OrbitControls' left button so left-drag is free to grab handles;
120
+ * onPointerDown/Move pan the camera manually when a left-press misses every handle (so panning
121
+ * never fights the object drag). Right-drag still rotates the camera; scroll/middle zooms. */
122
+ setOrbitForEdit(editing) {
123
+ if (!this.orbit) return;
124
+ this.orbit.mouseButtons = editing ? {
125
+ MIDDLE: THREE.MOUSE.DOLLY,
126
+ RIGHT: THREE.MOUSE.ROTATE
127
+ } : {
128
+ LEFT: THREE.MOUSE.PAN,
129
+ MIDDLE: THREE.MOUSE.DOLLY,
130
+ RIGHT: THREE.MOUSE.ROTATE
131
+ };
132
+ }
133
+ /** Switch the wave-edit gizmo between moving handles and rotating the whole wave. Rotate
134
+ * targets the whole-wave box (config.rotation), so selecting it makes the intent obvious. */
135
+ setGizmoMode(mode) {
136
+ this.gizmoMode = mode;
137
+ this.transform?.setMode(mode);
138
+ this.transform?.setSpace(mode === "rotate" ? "local" : "world");
139
+ if (mode === "rotate" && this.editMode === "wave") {
140
+ const waveIdx = this.waveHelpers.findIndex((h) => h.userData.kind === "wave");
141
+ if (waveIdx >= 0) this.selectWaveHandle(waveIdx);
142
+ }
143
+ if (!this.running) this.renderOnce();
144
+ }
145
+ getGizmoMode() {
146
+ return this.gizmoMode;
147
+ }
148
+ /** Snapshot the live camera so leaving edit mode returns exactly here (incl. ortho zoom). */
149
+ captureReturnCamera() {
150
+ this.returnCamera = {
151
+ pos: this.camera.position.clone(),
152
+ target: this.orbit ? this.orbit.target.clone() : this.camera.getWorldDirection(new THREE.Vector3()).add(this.camera.position),
153
+ zoom: this.camera.zoom,
154
+ up: this.camera.up.clone()
155
+ };
156
+ }
157
+ /** Restore the snapshot from captureReturnCamera (falls back to the authored hero camera). */
158
+ restoreReturnCamera() {
159
+ const s = this.returnCamera;
160
+ this.returnCamera = null;
161
+ if (!s) {
162
+ this.restoreHeroCamera();
163
+ return;
164
+ }
165
+ this.camera.position.copy(s.pos);
166
+ this.camera.up.copy(s.up);
167
+ this.camera.zoom = s.zoom;
168
+ this.camera.updateProjectionMatrix();
169
+ if (this.orbit) {
170
+ this.orbit.target.copy(s.target);
171
+ this.orbit.update();
172
+ } else this.camera.lookAt(s.target);
173
+ }
174
+ /** Turn on mouse/trackpad orbit + zoom + pan + arrow-key orbit (studio only). */
175
+ async enableOrbit() {
176
+ this.mainOrbitOn = true;
177
+ this.renderer.domElement.style.cursor = "move";
178
+ window.addEventListener("keydown", this.onKeyDown);
179
+ await this.ensureOrbit();
180
+ if (this.orbit && !this.editing) this.orbit.enabled = true;
181
+ }
182
+ /** Arrow keys orbit the camera around the target (←/→ azimuth, ↑/↓ elevation). */
183
+ onKeyDown = (e) => {
184
+ if (!this.mainOrbitOn || !this.orbit || this.editing) return;
185
+ const t = e.target instanceof HTMLElement ? e.target : null;
186
+ if (t && (t.closest("#panel") || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
187
+ const step = e.shiftKey ? .015 : .05;
188
+ let az = 0;
189
+ let pol = 0;
190
+ if (e.key === "ArrowLeft") az = -step;
191
+ else if (e.key === "ArrowRight") az = step;
192
+ else if (e.key === "ArrowUp") pol = -step;
193
+ else if (e.key === "ArrowDown") pol = step;
194
+ else return;
195
+ e.preventDefault();
196
+ const offset = this.camera.position.clone().sub(this.orbit.target);
197
+ const sph = new THREE.Spherical().setFromVector3(offset);
198
+ sph.theta += az;
199
+ sph.phi = THREE.MathUtils.clamp(sph.phi + pol, .05, Math.PI - .05);
200
+ offset.setFromSpherical(sph);
201
+ this.camera.position.copy(this.orbit.target).add(offset);
202
+ this.orbit.update();
203
+ };
204
+ /** Reset the camera to the straight-on hero framing at the configured distance. */
205
+ resetView() {
206
+ this.camera.position.copy(this.homeCamPos);
207
+ this.camera.up.set(0, 1, 0);
208
+ this.camera.lookAt(this.homeCamTarget);
209
+ if (this.orbit) {
210
+ this.orbit.target.copy(this.homeCamTarget);
211
+ this.orbit.update();
212
+ }
213
+ this.writeCameraToConfig();
214
+ this.onCameraChanged?.();
215
+ if (!this.running) this.renderOnce();
216
+ }
217
+ /** Dolly/aim the camera so the whole wave fills the viewport (keeps the view angle).
218
+ * Fits the geometry box's actual *projected* screen extent — tighter than a bounding
219
+ * sphere for a flat, diagonal ribbon. */
220
+ fitToView() {
221
+ const box = new THREE.Box3();
222
+ for (const s of this.waves) {
223
+ s.mesh.updateWorldMatrix(true, false);
224
+ if (!s.mesh.geometry.boundingBox) s.mesh.geometry.computeBoundingBox();
225
+ const bb = s.mesh.geometry.boundingBox;
226
+ if (bb) box.union(bb.clone().applyMatrix4(s.mesh.matrixWorld));
227
+ }
228
+ if (box.isEmpty()) return;
229
+ const center = box.getCenter(new THREE.Vector3());
230
+ if (this.orbit) this.orbit.target.copy(center);
231
+ this.camera.up.set(0, 1, 0);
232
+ this.camera.lookAt(center);
233
+ this.camera.updateMatrixWorld(true);
234
+ const c = box.min, m = box.max;
235
+ let frac = 0;
236
+ const v = new THREE.Vector3();
237
+ for (let i = 0; i < 8; i++) {
238
+ v.set(i & 1 ? m.x : c.x, i & 2 ? m.y : c.y, i & 4 ? m.z : c.z).project(this.camera);
239
+ frac = Math.max(frac, Math.abs(v.x), Math.abs(v.y));
240
+ }
241
+ const target = 1.18;
242
+ this.config.cameraZoom = (this.config.cameraZoom ?? 1) * target / Math.max(.001, frac);
243
+ this.applyZoom();
244
+ if (this.orbit) this.orbit.update();
245
+ this.writeCameraToConfig();
246
+ this.onCameraChanged?.();
247
+ if (!this.running) this.renderOnce();
248
+ }
249
+ /** Dolly the camera to a distance from the orbit target (or set z when no orbit). */
250
+ setCameraDistance(d) {
251
+ if (this.orbit) {
252
+ const dir = this.camera.position.clone().sub(this.orbit.target);
253
+ const len = dir.length() || 1;
254
+ this.camera.position.copy(this.orbit.target).addScaledVector(dir.multiplyScalar(1 / len), d);
255
+ this.orbit.update();
256
+ } else this.camera.position.z = d;
257
+ this.writeCameraToConfig();
258
+ if (!this.running) this.renderOnce();
259
+ }
260
+ /** The current look-at target (orbit's if present, else from config). */
261
+ camTarget() {
262
+ if (this.orbit) return this.orbit.target;
263
+ const t = this.config.cameraTarget;
264
+ return new THREE.Vector3(t.x, t.y, t.z);
265
+ }
266
+ /** Read the camera as orbit values for the panel (angles in degrees). */
267
+ getCameraOrbit() {
268
+ const t = this.camTarget();
269
+ const sph = new THREE.Spherical().setFromVector3(this.camera.position.clone().sub(t));
270
+ return {
271
+ azimuth: THREE.MathUtils.radToDeg(sph.theta),
272
+ elevation: 90 - THREE.MathUtils.radToDeg(sph.phi),
273
+ distance: sph.radius,
274
+ panX: t.x,
275
+ panY: t.y
276
+ };
277
+ }
278
+ /** Place the camera at azimuth/elevation (degrees) + distance around the target. */
279
+ setCameraOrbit(azimuthDeg, elevationDeg, distance) {
280
+ const target = this.camTarget();
281
+ const sph = new THREE.Spherical(Math.max(.01, distance), THREE.MathUtils.degToRad(90 - elevationDeg), THREE.MathUtils.degToRad(azimuthDeg));
282
+ sph.makeSafe();
283
+ this.suppressCameraChange = true;
284
+ this.camera.position.copy(target).add(new THREE.Vector3().setFromSpherical(sph));
285
+ this.camera.up.set(0, 1, 0);
286
+ this.camera.lookAt(target);
287
+ if (this.orbit) this.orbit.update();
288
+ this.suppressCameraChange = false;
289
+ this.writeCameraToConfig();
290
+ if (!this.running) this.renderOnce();
291
+ }
292
+ /** Roll the camera around its view axis (degrees) — tilts the composition without
293
+ * moving the camera. Applied after positioning; reset by any orbit interaction. */
294
+ rollView(deg) {
295
+ this.camera.rotateZ(THREE.MathUtils.degToRad(deg));
296
+ this.camera.updateMatrixWorld();
297
+ if (!this.running) this.renderOnce();
298
+ }
299
+ /** Pan: move the look-at target (and camera with it) to (x, y) in world units. */
300
+ setCameraTarget(x, y) {
301
+ const target = this.camTarget();
302
+ const delta = new THREE.Vector3(x - target.x, y - target.y, 0);
303
+ this.suppressCameraChange = true;
304
+ this.camera.position.add(delta);
305
+ if (this.orbit) this.orbit.target.add(delta);
306
+ else this.config.cameraTarget = {
307
+ x,
308
+ y,
309
+ z: target.z
310
+ };
311
+ this.camera.lookAt(this.camTarget());
312
+ if (this.orbit) this.orbit.update();
313
+ this.suppressCameraChange = false;
314
+ this.writeCameraToConfig();
315
+ if (!this.running) this.renderOnce();
316
+ }
317
+ /** Ortho zoom MULTIPLIER (the camera has no real fov). 1 = the responsive base framing (the hero crop). */
318
+ getZoom() {
319
+ return this.config.cameraZoom ?? 1;
320
+ }
321
+ setZoom(zoom) {
322
+ this.config.cameraZoom = THREE.MathUtils.clamp(zoom, .1, 6);
323
+ this.applyZoom();
324
+ if (!this.running) this.renderOnce();
325
+ }
326
+ duplicateOffset() {
327
+ this.camera.updateMatrixWorld();
328
+ const worldW = (this.camera.right - this.camera.left) / this.camera.zoom;
329
+ const worldH = (this.camera.top - this.camera.bottom) / this.camera.zoom;
330
+ const right = new THREE.Vector3().setFromMatrixColumn(this.camera.matrixWorld, 0).normalize();
331
+ const up = new THREE.Vector3().setFromMatrixColumn(this.camera.matrixWorld, 1).normalize();
332
+ const off = right.multiplyScalar(-.4 * worldW).add(up.multiplyScalar(-.15 * worldH));
333
+ return {
334
+ x: off.x,
335
+ y: off.y,
336
+ z: 0
337
+ };
338
+ }
339
+ /** Toggle the corner camera-rig minimap (studio aid; off in the embed). */
340
+ setCameraRig(on) {
341
+ this.cameraRigOn = on;
342
+ if (on) this.ensureMinimap();
343
+ if (this.minimapBtn) this.minimapBtn.style.display = on ? "" : "none";
344
+ if (on) this.positionMinimapBtn();
345
+ if (!this.running) this.renderOnce();
346
+ }
347
+ /** Corner rectangle (logical px) for the minimap viewport. */
348
+ minimapRect() {
349
+ const sz = this.renderer.getSize(new THREE.Vector2());
350
+ const size = Math.round(Math.min(sz.x, sz.y) * .27);
351
+ const pad = Math.round(size * .06);
352
+ return {
353
+ x: sz.x - size - pad,
354
+ y: pad,
355
+ size,
356
+ pad
357
+ };
358
+ }
359
+ /** Place the collapse button at the minimap's top-right (or bottom corner when collapsed). */
360
+ positionMinimapBtn() {
361
+ const b = this.minimapBtn;
362
+ if (!b) return;
363
+ const { size, pad } = this.minimapRect();
364
+ const canvas = this.renderer.domElement;
365
+ const buf = this.renderer.getSize(new THREE.Vector2());
366
+ const sx = buf.x > 0 ? canvas.clientWidth / buf.x : 1;
367
+ const sy = buf.y > 0 ? canvas.clientHeight / buf.y : 1;
368
+ b.style.right = pad * sx + "px";
369
+ b.style.bottom = (this.cameraRigCollapsed ? pad * sy : (pad + size) * sy - 22) + "px";
370
+ b.textContent = this.cameraRigCollapsed ? "▴ camera" : "▾ camera";
371
+ }
372
+ /** Build the minimap's fixed 3rd-person camera + the camera/light markers (once). */
373
+ ensureMinimap() {
374
+ if (this.minimapCamera) return;
375
+ this.minimapCamera = new THREE.PerspectiveCamera(42, 1, 1, 1e4);
376
+ const marker = new THREE.Group();
377
+ marker.add(new THREE.Mesh(new THREE.BoxGeometry(3.4, 2.4, 4.2), new THREE.MeshBasicMaterial({ color: 2764605 })));
378
+ const lens = new THREE.Mesh(new THREE.ConeGeometry(1.3, 2.4, 18), new THREE.MeshBasicMaterial({ color: 7252222 }));
379
+ lens.rotation.x = -Math.PI / 2;
380
+ lens.position.z = -2.7;
381
+ marker.add(lens);
382
+ marker.visible = false;
383
+ this.scene.add(marker);
384
+ this.camMarker = marker;
385
+ const btn = document.createElement("button");
386
+ btn.style.cssText = "position:absolute;z-index:30;padding:2px 8px;border-radius:5px;cursor:pointer;font:11px ui-sans-serif,system-ui,-apple-system,sans-serif;color:#cdd0d6;background:rgba(18,18,26,0.85);border:1px solid rgba(255,255,255,0.16);";
387
+ btn.addEventListener("click", () => {
388
+ this.cameraRigCollapsed = !this.cameraRigCollapsed;
389
+ this.positionMinimapBtn();
390
+ this.renderOnce();
391
+ });
392
+ this.container.appendChild(btn);
393
+ this.minimapBtn = btn;
394
+ this.positionMinimapBtn();
395
+ }
396
+ /** The lights the rig should show: the configured lights, or a single default-position
397
+ * marker when none has been added yet — so the light is always visible in the rig. */
398
+ rigLights() {
399
+ const lights = this.config.lights ?? [];
400
+ return lights.length ? lights : [this.defaultRigLight];
401
+ }
402
+ /** Reconcile the minimap's light markers with the rig lights (count, position, colour). */
403
+ syncMinimapLights(visible) {
404
+ const lights = this.rigLights();
405
+ while (this.minimapLights.length < lights.length) {
406
+ const m = new THREE.Mesh(new THREE.SphereGeometry(2.2, 16, 12), new THREE.MeshBasicMaterial({ color: 16765514 }));
407
+ m.visible = false;
408
+ this.scene.add(m);
409
+ this.minimapLights.push(m);
410
+ }
411
+ while (this.minimapLights.length > lights.length) {
412
+ const m = this.minimapLights.pop();
413
+ if (!m) break;
414
+ this.scene.remove(m);
415
+ m.geometry.dispose();
416
+ m.material.dispose();
417
+ }
418
+ lights.forEach((l, i) => {
419
+ const m = this.minimapLights[i];
420
+ m.position.set(l.position.x, l.position.y, l.position.z);
421
+ m.material.color.set(l.color);
422
+ m.visible = visible;
423
+ });
424
+ }
425
+ /** Fit the minimap's 3rd-person camera to the wave (+ lights) and size/place the camera
426
+ * proxy, so the rig reads at any scene scale. The main camera is orthographic — its literal
427
+ * distance is arbitrary — so the proxy sits a fixed multiple of the scene radius back along
428
+ * the true view direction rather than at the far-away ortho position. */
429
+ frameMinimap() {
430
+ const cam = this.minimapCamera;
431
+ const marker = this.camMarker;
432
+ if (!cam || !marker) return;
433
+ const box = this.miniBox.setFromObject(this.group);
434
+ if (box.isEmpty()) return;
435
+ for (const l of this.rigLights()) box.expandByPoint(this.miniTmpA.set(l.position.x, l.position.y, l.position.z));
436
+ const subject = box.getBoundingSphere(this.miniSphere);
437
+ const radius = Math.max(subject.radius, 1);
438
+ const viewDir = this.camera.getWorldDirection(this.miniTmpA);
439
+ const markerPos = this.miniTmpB.copy(subject.center).addScaledVector(viewDir, -radius * 1.5);
440
+ marker.position.copy(markerPos);
441
+ marker.quaternion.copy(this.camera.quaternion);
442
+ marker.scale.setScalar(radius * .05);
443
+ for (const m of this.minimapLights) m.scale.setScalar(radius * .045);
444
+ box.expandByPoint(markerPos);
445
+ const rig = box.getBoundingSphere(this.miniSphere);
446
+ const frameR = Math.max(rig.radius, 1);
447
+ cam.position.copy(rig.center).addScaledVector(MINIMAP_VANTAGE, frameR * 2.9);
448
+ cam.near = Math.max(1, frameR * .02);
449
+ cam.far = frameR * 10;
450
+ cam.up.set(0, 1, 0);
451
+ cam.lookAt(rig.center);
452
+ cam.updateProjectionMatrix();
453
+ }
454
+ /** Draw the camera-rig minimap into a corner viewport (called after the main render). */
455
+ renderMinimap() {
456
+ if (!this.cameraRigOn || this.cameraRigCollapsed || !this.mainOrbitOn || this.capturing) return;
457
+ if (!this.minimapCamera || !this.camMarker) return;
458
+ const { x, y, size } = this.minimapRect();
459
+ this.camMarker.visible = true;
460
+ this.syncMinimapLights(true);
461
+ this.frameMinimap();
462
+ const r = this.renderer;
463
+ const prevColor = this.miniPrevColor;
464
+ r.getClearColor(prevColor);
465
+ const prevAlpha = r.getClearAlpha();
466
+ r.autoClear = false;
467
+ r.setRenderTarget(null);
468
+ r.setScissorTest(true);
469
+ r.setViewport(x, y, size, size);
470
+ r.setScissor(x, y, size, size);
471
+ r.setClearColor(1184282, .92);
472
+ r.clear(true, true);
473
+ const prevBg = this.scene.background;
474
+ this.scene.background = null;
475
+ for (let i = 0; i < this.waves.length; i++) {
476
+ const m = this.waves[i].material;
477
+ this.miniBlendPrev[i] = m.blending;
478
+ this.miniTransPrev[i] = m.transparent;
479
+ m.blending = THREE.NormalBlending;
480
+ m.transparent = false;
481
+ }
482
+ r.render(this.scene, this.minimapCamera);
483
+ for (let i = 0; i < this.waves.length; i++) {
484
+ const m = this.waves[i].material;
485
+ m.blending = this.miniBlendPrev[i];
486
+ m.transparent = this.miniTransPrev[i];
487
+ }
488
+ this.scene.background = prevBg;
489
+ r.setScissorTest(false);
490
+ const full = this.renderer.getSize(this.miniSize);
491
+ r.setViewport(0, 0, full.x, full.y);
492
+ r.setClearColor(prevColor, prevAlpha);
493
+ r.autoClear = true;
494
+ this.camMarker.visible = false;
495
+ for (const m of this.minimapLights) m.visible = false;
496
+ }
497
+ async ensureOrbit() {
498
+ if (this.orbit) return;
499
+ const { OrbitControls } = await import("three/addons/controls/OrbitControls.js");
500
+ if (this.orbit) return;
501
+ this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
502
+ this.orbit.enableDamping = false;
503
+ this.orbit.enabled = false;
504
+ this.orbit.screenSpacePanning = true;
505
+ this.orbit.zoomToCursor = true;
506
+ this.orbit.minDistance = 12;
507
+ this.orbit.maxDistance = 600;
508
+ this.orbit.mouseButtons = {
509
+ LEFT: THREE.MOUSE.PAN,
510
+ MIDDLE: THREE.MOUSE.DOLLY,
511
+ RIGHT: THREE.MOUSE.ROTATE
512
+ };
513
+ this.orbit.target.set(this.config.cameraTarget.x, this.config.cameraTarget.y, this.config.cameraTarget.z);
514
+ this.orbit.update();
515
+ this.orbit.addEventListener("change", this.onControlsChange);
516
+ this.renderer.domElement.addEventListener("pointerdown", this.onCursorDown);
517
+ window.addEventListener("pointerup", this.onCursorUp);
518
+ }
519
+ onCursorDown = (e) => {
520
+ if (this.mainOrbitOn) this.renderer.domElement.style.cursor = e.button === 2 ? "grabbing" : "move";
521
+ };
522
+ onCursorUp = () => {
523
+ if (this.mainOrbitOn) this.renderer.domElement.style.cursor = "move";
524
+ };
525
+ async ensureGizmo() {
526
+ await this.ensureOrbit();
527
+ if (this.transform) return;
528
+ const { TransformControls } = await import("three/addons/controls/TransformControls.js");
529
+ if (this.transform) return;
530
+ this.transform = new TransformControls(this.camera, this.renderer.domElement);
531
+ this.transform.setMode("translate");
532
+ this.transform.addEventListener("dragging-changed", (e) => {
533
+ if (this.orbit) this.orbit.enabled = !e.value;
534
+ });
535
+ this.transform.addEventListener("objectChange", this.onGizmoMoved);
536
+ this.transform.addEventListener("change", this.onControlsChange);
537
+ const tc = this.transform;
538
+ this.overlay.add(tc.getHelper ? tc.getHelper() : this.transform);
539
+ this.renderer.domElement.addEventListener("pointerdown", this.onPointerDown);
540
+ this.renderer.domElement.addEventListener("pointermove", this.onPointerMove);
541
+ this.renderer.domElement.addEventListener("pointerup", this.onPointerUp);
542
+ this.renderer.domElement.addEventListener("pointercancel", this.onPointerUp);
543
+ }
544
+ onControlsChange = () => {
545
+ if (this.orbit && this.orbit.enabled && this.editMode !== "light" && !this.suppressCameraChange) {
546
+ this.writeCameraToConfig();
547
+ this.onCameraChanged?.();
548
+ }
549
+ if (!this.running) this.renderOnce();
550
+ };
551
+ /** Persist the live camera (position/target/distance) into the config. */
552
+ writeCameraToConfig() {
553
+ const p = this.camera.position;
554
+ this.config.cameraPosition = {
555
+ x: roundTo(p.x, 3),
556
+ y: roundTo(p.y, 3),
557
+ z: roundTo(p.z, 3)
558
+ };
559
+ const cover = Math.max((this.camera.right - this.camera.left) / FRAME_W, (this.camera.top - this.camera.bottom) / 750);
560
+ if (cover > 0) this.config.cameraZoom = roundTo(this.camera.zoom / cover, 3);
561
+ if (this.orbit) {
562
+ const t = this.orbit.target;
563
+ this.config.cameraTarget = {
564
+ x: roundTo(t.x, 3),
565
+ y: roundTo(t.y, 3),
566
+ z: roundTo(t.z, 3)
567
+ };
568
+ this.config.cameraDistance = roundTo(p.distanceTo(this.orbit.target), 3);
569
+ }
570
+ }
571
+ /** Pointer position in normalized device coords (-1..1), from a canvas-relative event. */
572
+ pointerNdc(ev) {
573
+ const rect = this.renderer.domElement.getBoundingClientRect();
574
+ return new THREE.Vector2((ev.clientX - rect.left) / rect.width * 2 - 1, -((ev.clientY - rect.top) / rect.height) * 2 + 1);
575
+ }
576
+ onPointerDown = (ev) => {
577
+ if (!this.editing || !this.transform) return;
578
+ if (ev.button !== 0) return;
579
+ if (this.transform.dragging || this.transform.axis) return;
580
+ this.raycaster.setFromCamera(this.pointerNdc(ev), this.camera);
581
+ const helpers = this.editMode === "wave" ? this.waveHelpers : this.lightHelpers;
582
+ const hit = this.raycaster.intersectObjects(helpers, false)[0];
583
+ if (!hit) {
584
+ if (this.orbit) {
585
+ this.panState = { lastNdc: this.pointerNdc(ev) };
586
+ this.renderer.domElement.setPointerCapture?.(ev.pointerId);
587
+ }
588
+ return;
589
+ }
590
+ const idx = helpers.indexOf(hit.object);
591
+ if (idx < 0) return;
592
+ if (this.editMode === "wave") this.selectWaveHandle(idx);
593
+ else this.selectLight(idx);
594
+ if (this.gizmoMode !== "translate") return;
595
+ const helper = helpers[idx];
596
+ const normal = this.camera.getWorldDirection(new THREE.Vector3());
597
+ this.dragPlane.setFromNormalAndCoplanarPoint(normal, helper.position);
598
+ const grab = new THREE.Vector3();
599
+ if (!this.raycaster.ray.intersectPlane(this.dragPlane, grab)) return;
600
+ this.dragState = {
601
+ helper,
602
+ offset: helper.position.clone().sub(grab)
603
+ };
604
+ if (this.orbit) this.orbit.enabled = false;
605
+ this.renderer.domElement.setPointerCapture?.(ev.pointerId);
606
+ };
607
+ onPointerMove = (ev) => {
608
+ if (this.panState) {
609
+ const ndc = this.pointerNdc(ev);
610
+ const before = new THREE.Vector3(this.panState.lastNdc.x, this.panState.lastNdc.y, 0);
611
+ const after = new THREE.Vector3(ndc.x, ndc.y, 0);
612
+ const delta = before.unproject(this.camera).sub(after.unproject(this.camera));
613
+ this.camera.position.add(delta);
614
+ this.panState.lastNdc = ndc;
615
+ if (this.orbit) {
616
+ this.orbit.target.add(delta);
617
+ this.orbit.update();
618
+ } else {
619
+ this.camera.updateProjectionMatrix();
620
+ if (!this.running) this.renderOnce();
621
+ }
622
+ return;
623
+ }
624
+ if (!this.dragState) return;
625
+ this.raycaster.setFromCamera(this.pointerNdc(ev), this.camera);
626
+ const p = new THREE.Vector3();
627
+ if (!this.raycaster.ray.intersectPlane(this.dragPlane, p)) return;
628
+ this.dragState.helper.position.copy(p.add(this.dragState.offset));
629
+ this.onGizmoMoved();
630
+ if (!this.running) this.renderOnce();
631
+ };
632
+ onPointerUp = (ev) => {
633
+ if (this.panState) {
634
+ this.panState = void 0;
635
+ this.renderer.domElement.releasePointerCapture?.(ev.pointerId);
636
+ return;
637
+ }
638
+ if (!this.dragState) return;
639
+ this.dragState = void 0;
640
+ if (this.orbit) this.orbit.enabled = true;
641
+ this.renderer.domElement.releasePointerCapture?.(ev.pointerId);
642
+ };
643
+ selectLight(i) {
644
+ this.selectedLight = i;
645
+ const h = this.lightHelpers[i];
646
+ if (h && this.transform) this.transform.attach(h);
647
+ else this.transform?.detach();
648
+ this.onLightsChanged?.(i);
649
+ if (!this.running) this.renderOnce();
650
+ }
651
+ selectWaveHandle(i) {
652
+ this.selectedWave = i;
653
+ const h = this.waveHelpers[i];
654
+ if (h && this.transform) this.transform.attach(h);
655
+ else this.transform?.detach();
656
+ this.onWaveChanged?.();
657
+ if (!this.running) this.renderOnce();
658
+ }
659
+ /** Gizmo drag → route to the active mode's writer. */
660
+ onGizmoMoved = () => {
661
+ if (this.editMode === "wave") this.onWaveGizmoMoved();
662
+ else this.onLightGizmoMoved();
663
+ };
664
+ /** Light gizmo drag → write the moved handle back into the config + uniforms. */
665
+ onLightGizmoMoved() {
666
+ const h = this.lightHelpers[this.selectedLight];
667
+ const light = this.config.lights?.[this.selectedLight];
668
+ if (!h || !light) return;
669
+ light.position.x = roundTo(h.position.x, 2);
670
+ light.position.y = roundTo(h.position.y, 2);
671
+ light.position.z = roundTo(h.position.z, 2);
672
+ this.pushLightUniforms();
673
+ this.onLightsChanged?.(this.selectedLight);
674
+ }
675
+ /** Wave gizmo drag → the whole-wave box writes config.position (and the wave handles
676
+ * follow it); a per-wave sphere writes that layer's offset (relative to config.position). */
677
+ onWaveGizmoMoved() {
678
+ const h = this.waveHelpers[this.selectedWave];
679
+ if (!h) return;
680
+ const wave = this.config.waves[h.userData.index];
681
+ if (!wave) return;
682
+ if (this.gizmoMode === "rotate") {
683
+ wave.rotation.x = roundTo(THREE.MathUtils.radToDeg(h.rotation.x), 2);
684
+ wave.rotation.y = roundTo(THREE.MathUtils.radToDeg(h.rotation.y), 2);
685
+ wave.rotation.z = roundTo(THREE.MathUtils.radToDeg(h.rotation.z), 2);
686
+ } else {
687
+ wave.position.x = roundTo(h.position.x, 2);
688
+ wave.position.y = roundTo(h.position.y, 2);
689
+ wave.position.z = roundTo(h.position.z, 2);
690
+ }
691
+ this.pushWaveTransforms();
692
+ this.onWaveChanged?.();
693
+ }
694
+ /** Reconcile the helper spheres with config.lights (count, position, colour). */
695
+ syncLightHelpers() {
696
+ const lights = this.config.lights ?? [];
697
+ while (this.lightHelpers.length < lights.length) {
698
+ const mesh = new THREE.Mesh(new THREE.SphereGeometry(.28, 16, 12), new THREE.MeshBasicMaterial({
699
+ color: 16777215,
700
+ depthTest: false,
701
+ transparent: true
702
+ }));
703
+ mesh.renderOrder = 999;
704
+ this.overlay.add(mesh);
705
+ this.lightHelpers.push(mesh);
706
+ }
707
+ while (this.lightHelpers.length > lights.length) {
708
+ const mesh = this.lightHelpers.pop();
709
+ if (!mesh) break;
710
+ this.overlay.remove(mesh);
711
+ mesh.geometry.dispose();
712
+ mesh.material.dispose();
713
+ }
714
+ lights.forEach((l, i) => {
715
+ const h = this.lightHelpers[i];
716
+ h.position.set(l.position.x, l.position.y, l.position.z);
717
+ h.material.color.set(l.color);
718
+ });
719
+ if (this.selectedLight >= this.lightHelpers.length) this.selectedLight = Math.max(0, this.lightHelpers.length - 1);
720
+ const sel = this.lightHelpers[this.selectedLight];
721
+ if (sel && this.transform && this.transform.object !== sel) this.transform.attach(sel);
722
+ if (!sel) this.transform?.detach();
723
+ }
724
+ clearLightHelpers() {
725
+ for (const mesh of this.lightHelpers) {
726
+ this.overlay.remove(mesh);
727
+ mesh.geometry.dispose();
728
+ mesh.material.dispose();
729
+ }
730
+ this.lightHelpers = [];
731
+ }
732
+ /** Reconcile the wave drag handles with config: one box handle per wave, sitting at that
733
+ * wave's absolute position (and oriented to its rotation so the rotate gizmo starts there). */
734
+ syncWaveHelpers() {
735
+ if (this.transform?.dragging) return;
736
+ const waves = this.config.waves ?? [];
737
+ const wantTotal = waves.length;
738
+ if (this.waveHelpers.length !== wantTotal) {
739
+ this.clearWaveHelpers();
740
+ for (let i = 0; i < wantTotal; i++) {
741
+ const box = new THREE.Mesh(new THREE.BoxGeometry(.55, .55, .55), new THREE.MeshBasicMaterial({
742
+ color: 3789055,
743
+ depthTest: false,
744
+ transparent: true
745
+ }));
746
+ box.renderOrder = 999;
747
+ box.userData = {
748
+ kind: "wave",
749
+ index: i
750
+ };
751
+ this.overlay.add(box);
752
+ this.waveHelpers.push(box);
753
+ }
754
+ }
755
+ for (const h of this.waveHelpers) {
756
+ const sc = waves[h.userData.index];
757
+ if (!sc) continue;
758
+ h.position.set(sc.position.x, sc.position.y, sc.position.z);
759
+ h.rotation.set(THREE.MathUtils.degToRad(sc.rotation.x), THREE.MathUtils.degToRad(sc.rotation.y), THREE.MathUtils.degToRad(sc.rotation.z));
760
+ }
761
+ if (this.selectedWave >= this.waveHelpers.length) this.selectedWave = 0;
762
+ const sel = this.waveHelpers[this.selectedWave];
763
+ if (sel && this.transform && this.transform.object !== sel) this.transform.attach(sel);
764
+ if (!sel) this.transform?.detach();
765
+ }
766
+ clearWaveHelpers() {
767
+ for (const mesh of this.waveHelpers) {
768
+ this.overlay.remove(mesh);
769
+ mesh.geometry.dispose();
770
+ mesh.material.dispose();
771
+ }
772
+ this.waveHelpers = [];
773
+ }
774
+ /** Reposition just the wave MESHES from each wave's absolute transform — the transform
775
+ * subset of refresh(), used live during a wave gizmo drag so the ribbon follows without a
776
+ * full uniform re-push or a helper resync. */
777
+ pushWaveTransforms() {
778
+ this.waves.forEach((wave, i) => {
779
+ const sc = this.config.waves[i] ?? this.config.waves[this.config.waves.length - 1];
780
+ wave.mesh.scale.set(sc.scale.x, sc.scale.y, sc.scale.z);
781
+ wave.mesh.rotation.set(THREE.MathUtils.degToRad(sc.rotation.x), THREE.MathUtils.degToRad(sc.rotation.y), THREE.MathUtils.degToRad(sc.rotation.z));
782
+ wave.mesh.position.set(sc.position.x, sc.position.y, sc.position.z);
783
+ });
784
+ if (!this.running) this.renderOnce();
785
+ }
786
+ /** Pull the camera back to a 3/4 angle that frames the edit target: the origin wave + all
787
+ * lights (light mode), or a region around config.position + the handles (wave mode). */
788
+ frameEditCamera() {
789
+ const box = new THREE.Box3();
790
+ if (this.editMode === "wave") {
791
+ const target = this.config.waves[this.selectedWave] ?? this.config.waves[0];
792
+ const c = new THREE.Vector3(target.position.x, target.position.y, target.position.z);
793
+ box.expandByPoint(c.clone().addScalar(200));
794
+ box.expandByPoint(c.clone().addScalar(-200));
795
+ for (const h of this.waveHelpers) box.expandByPoint(h.position);
796
+ } else {
797
+ box.expandByPoint(new THREE.Vector3(25, 25, 25));
798
+ box.expandByPoint(new THREE.Vector3(-25, -25, -25));
799
+ for (const l of this.config.lights ?? []) box.expandByPoint(new THREE.Vector3(l.position.x, l.position.y, l.position.z));
800
+ }
801
+ const sphere = box.getBoundingSphere(new THREE.Sphere());
802
+ const radius = Math.max(sphere.radius, 2);
803
+ const dir = new THREE.Vector3(.45, .35, 1).normalize();
804
+ this.camera.position.copy(sphere.center).addScaledVector(dir, radius * 3 + 200);
805
+ this.camera.up.set(0, 1, 0);
806
+ this.camera.lookAt(sphere.center);
807
+ this.camera.zoom = (this.camera.right - this.camera.left) / Math.max(1, radius * 2.6);
808
+ this.camera.updateProjectionMatrix();
809
+ if (this.orbit) {
810
+ this.orbit.target.copy(sphere.center);
811
+ this.orbit.update();
812
+ }
813
+ }
814
+ /** Restore the authored camera (from config) — used when leaving light-edit. */
815
+ restoreHeroCamera() {
816
+ const p = this.config.cameraPosition;
817
+ const t = this.config.cameraTarget;
818
+ this.camera.position.set(p.x, p.y, p.z);
819
+ this.camera.up.set(0, 1, 0);
820
+ this.camera.lookAt(t.x, t.y, t.z);
821
+ if (this.orbit) {
822
+ this.orbit.target.set(t.x, t.y, t.z);
823
+ this.orbit.update();
824
+ }
825
+ }
826
+ /** Push only the light uniforms (used live during a gizmo drag). */
827
+ pushLightUniforms() {
828
+ const lights = this.config.lights ?? [];
829
+ for (const wave of this.waves) {
830
+ const u = wave.material.uniforms;
831
+ u.uNumLights.value = Math.min(lights.length, 8);
832
+ const lPos = u.uLightPos.value;
833
+ const lCol = u.uLightColor.value;
834
+ const lInt = u.uLightIntensity.value;
835
+ for (let li = 0; li < 8; li++) {
836
+ const light = lights[li];
837
+ if (light) {
838
+ lPos[li].set(light.position.x, light.position.y, light.position.z);
839
+ hexToLinearVec3(light.color, lCol[li]);
840
+ lInt[li] = light.intensity;
841
+ } else lInt[li] = 0;
842
+ }
843
+ }
844
+ if (!this.running) this.renderOnce();
845
+ }
846
+ isCameraExternallyDriven() {
847
+ return !!this.orbit || this.editing;
848
+ }
849
+ onAfterRefresh() {
850
+ if (this.editMode === "light") this.syncLightHelpers();
851
+ else if (this.editMode === "wave") this.syncWaveHelpers();
852
+ }
853
+ onAfterRenderFrame() {
854
+ if (this.overlay && this.editing && !this.capturing && this.overlay.children.length > 0) {
855
+ const helpers = this.editMode === "wave" ? this.waveHelpers : this.lightHelpers;
856
+ for (const h of helpers) h.scale.setScalar(Math.max(.1, this.camera.position.distanceTo(h.position) * .09));
857
+ this.renderer.autoClear = false;
858
+ this.renderer.setRenderTarget(null);
859
+ this.renderer.render(this.overlay, this.camera);
860
+ this.renderer.autoClear = true;
861
+ }
862
+ this.renderMinimap();
863
+ }
864
+ onAfterResize() {
865
+ if (this.cameraRigOn) this.positionMinimapBtn();
866
+ }
867
+ applyCameraFromConfig() {
868
+ if (this.editing) return;
869
+ const p = this.config.cameraPosition;
870
+ const tg = this.config.cameraTarget;
871
+ this.suppressCameraChange = true;
872
+ this.camera.position.set(p.x, p.y, p.z);
873
+ this.camera.up.set(0, 1, 0);
874
+ this.camera.lookAt(tg.x, tg.y, tg.z);
875
+ if (this.orbit) {
876
+ this.orbit.target.set(tg.x, tg.y, tg.z);
877
+ this.orbit.update();
878
+ }
879
+ this.applyZoom();
880
+ this.suppressCameraChange = false;
881
+ this.onCameraChanged?.();
882
+ if (!this.running) this.renderOnce();
883
+ }
884
+ dispose() {
885
+ window.removeEventListener("keydown", this.onKeyDown);
886
+ this.renderer.domElement.removeEventListener("pointerdown", this.onPointerDown);
887
+ this.renderer.domElement.removeEventListener("pointerdown", this.onCursorDown);
888
+ window.removeEventListener("pointerup", this.onCursorUp);
889
+ this.transform?.detach();
890
+ this.transform?.dispose();
891
+ this.orbit?.dispose();
892
+ this.minimapBtn?.remove();
893
+ this.clearLightHelpers();
894
+ this.clearWaveHelpers();
895
+ for (const m of this.minimapLights) {
896
+ m.geometry.dispose();
897
+ m.material.dispose();
898
+ }
899
+ super.dispose();
900
+ }
901
+ };
902
+ //#endregion
903
+ export { StudioWaveRenderer };
904
+
905
+ //# sourceMappingURL=StudioWaveRenderer.js.map