@heliguy-xyz/splat-viewer 1.0.0-rc.24 → 1.0.0-rc.25

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 (148) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/dist/web-component/splat-viewer.esm.js +825 -139
  3. package/dist/web-component/splat-viewer.esm.min.js +2 -2
  4. package/dist/web-component/splat-viewer.js +825 -139
  5. package/dist/web-component/splat-viewer.min.js +2 -2
  6. package/dist/web-component/supersplat-core/doc.d.ts.map +1 -1
  7. package/dist/web-component/supersplat-core/file-handler.d.ts.map +1 -1
  8. package/dist/web-component/supersplat-core/index.d.ts +1 -1
  9. package/dist/web-component/supersplat-core/index.d.ts.map +1 -1
  10. package/dist/web-component/supersplat-core/main.d.ts.map +1 -1
  11. package/dist/web-component/supersplat-core/publish.d.ts.map +1 -1
  12. package/dist/web-component/supersplat-core/render.d.ts.map +1 -1
  13. package/dist/web-component/supersplat-core/tools/measure-tool.d.ts.map +1 -1
  14. package/dist/web-component/types/supersplat-core/doc.d.ts.map +1 -1
  15. package/dist/web-component/types/supersplat-core/file-handler.d.ts.map +1 -1
  16. package/dist/web-component/types/supersplat-core/index.d.ts +1 -1
  17. package/dist/web-component/types/supersplat-core/index.d.ts.map +1 -1
  18. package/dist/web-component/types/supersplat-core/main.d.ts.map +1 -1
  19. package/dist/web-component/types/supersplat-core/publish.d.ts.map +1 -1
  20. package/dist/web-component/types/supersplat-core/render.d.ts.map +1 -1
  21. package/dist/web-component/types/supersplat-core/tools/measure-tool.d.ts.map +1 -1
  22. package/dist/web-component/types/web-component/CameraModeManager.d.ts.map +1 -1
  23. package/dist/web-component/types/web-component/FlyCameraController.d.ts +85 -0
  24. package/dist/web-component/types/web-component/FlyCameraController.d.ts.map +1 -0
  25. package/dist/web-component/types/web-component/FlyCameraScript.d.ts.map +1 -1
  26. package/dist/web-component/types/web-component/SplatViewerCore.d.ts.map +1 -1
  27. package/dist/web-component/types/web-component/supersplat/BoxSelectionAPI.d.ts +1 -0
  28. package/dist/web-component/types/web-component/supersplat/BoxSelectionAPI.d.ts.map +1 -1
  29. package/dist/web-component/types/web-component/supersplat/SphereSelectionAPI.d.ts +1 -0
  30. package/dist/web-component/types/web-component/supersplat/SphereSelectionAPI.d.ts.map +1 -1
  31. package/dist/web-component/types/web-component/supersplat/blue-noise.d.ts +3 -0
  32. package/dist/web-component/types/web-component/supersplat/blue-noise.d.ts.map +1 -0
  33. package/dist/web-component/web-component/CameraModeManager.d.ts.map +1 -1
  34. package/dist/web-component/web-component/FlyCameraController.d.ts +85 -0
  35. package/dist/web-component/web-component/FlyCameraController.d.ts.map +1 -0
  36. package/dist/web-component/web-component/FlyCameraScript.d.ts.map +1 -1
  37. package/dist/web-component/web-component/SplatViewerCore.d.ts.map +1 -1
  38. package/dist/web-component/web-component/supersplat/BoxSelectionAPI.d.ts +1 -0
  39. package/dist/web-component/web-component/supersplat/BoxSelectionAPI.d.ts.map +1 -1
  40. package/dist/web-component/web-component/supersplat/SphereSelectionAPI.d.ts +1 -0
  41. package/dist/web-component/web-component/supersplat/SphereSelectionAPI.d.ts.map +1 -1
  42. package/dist/web-component/web-component/supersplat/blue-noise.d.ts +3 -0
  43. package/dist/web-component/web-component/supersplat/blue-noise.d.ts.map +1 -0
  44. package/package.json +1 -1
  45. package/dist/web-component/supersplat-core/ui/bottom-toolbar.d.ts +0 -8
  46. package/dist/web-component/supersplat-core/ui/bottom-toolbar.d.ts.map +0 -1
  47. package/dist/web-component/supersplat-core/ui/color-panel.d.ts +0 -8
  48. package/dist/web-component/supersplat-core/ui/color-panel.d.ts.map +0 -1
  49. package/dist/web-component/supersplat-core/ui/color.d.ts +0 -20
  50. package/dist/web-component/supersplat-core/ui/color.d.ts.map +0 -1
  51. package/dist/web-component/supersplat-core/ui/data-panel.d.ts +0 -7
  52. package/dist/web-component/supersplat-core/ui/data-panel.d.ts.map +0 -1
  53. package/dist/web-component/supersplat-core/ui/editor.d.ts +0 -14
  54. package/dist/web-component/supersplat-core/ui/editor.d.ts.map +0 -1
  55. package/dist/web-component/supersplat-core/ui/export-popup.d.ts +0 -11
  56. package/dist/web-component/supersplat-core/ui/export-popup.d.ts.map +0 -1
  57. package/dist/web-component/supersplat-core/ui/histogram.d.ts +0 -32
  58. package/dist/web-component/supersplat-core/ui/histogram.d.ts.map +0 -1
  59. package/dist/web-component/supersplat-core/ui/image-settings-dialog.d.ts +0 -11
  60. package/dist/web-component/supersplat-core/ui/image-settings-dialog.d.ts.map +0 -1
  61. package/dist/web-component/supersplat-core/ui/localization.d.ts +0 -9
  62. package/dist/web-component/supersplat-core/ui/localization.d.ts.map +0 -1
  63. package/dist/web-component/supersplat-core/ui/menu-panel.d.ts +0 -21
  64. package/dist/web-component/supersplat-core/ui/menu-panel.d.ts.map +0 -1
  65. package/dist/web-component/supersplat-core/ui/menu.d.ts +0 -7
  66. package/dist/web-component/supersplat-core/ui/menu.d.ts.map +0 -1
  67. package/dist/web-component/supersplat-core/ui/mode-toggle.d.ts +0 -8
  68. package/dist/web-component/supersplat-core/ui/mode-toggle.d.ts.map +0 -1
  69. package/dist/web-component/supersplat-core/ui/popup.d.ts +0 -16
  70. package/dist/web-component/supersplat-core/ui/popup.d.ts.map +0 -1
  71. package/dist/web-component/supersplat-core/ui/progress.d.ts +0 -9
  72. package/dist/web-component/supersplat-core/ui/progress.d.ts.map +0 -1
  73. package/dist/web-component/supersplat-core/ui/publish-settings-dialog.d.ts +0 -11
  74. package/dist/web-component/supersplat-core/ui/publish-settings-dialog.d.ts.map +0 -1
  75. package/dist/web-component/supersplat-core/ui/right-toolbar.d.ts +0 -8
  76. package/dist/web-component/supersplat-core/ui/right-toolbar.d.ts.map +0 -1
  77. package/dist/web-component/supersplat-core/ui/scene-panel.d.ts +0 -8
  78. package/dist/web-component/supersplat-core/ui/scene-panel.d.ts.map +0 -1
  79. package/dist/web-component/supersplat-core/ui/shortcuts-popup.d.ts +0 -6
  80. package/dist/web-component/supersplat-core/ui/shortcuts-popup.d.ts.map +0 -1
  81. package/dist/web-component/supersplat-core/ui/spinner.d.ts +0 -6
  82. package/dist/web-component/supersplat-core/ui/spinner.d.ts.map +0 -1
  83. package/dist/web-component/supersplat-core/ui/splat-list.d.ts +0 -25
  84. package/dist/web-component/supersplat-core/ui/splat-list.d.ts.map +0 -1
  85. package/dist/web-component/supersplat-core/ui/timeline-panel.d.ts +0 -8
  86. package/dist/web-component/supersplat-core/ui/timeline-panel.d.ts.map +0 -1
  87. package/dist/web-component/supersplat-core/ui/tooltips.d.ts +0 -10
  88. package/dist/web-component/supersplat-core/ui/tooltips.d.ts.map +0 -1
  89. package/dist/web-component/supersplat-core/ui/transform.d.ts +0 -7
  90. package/dist/web-component/supersplat-core/ui/transform.d.ts.map +0 -1
  91. package/dist/web-component/supersplat-core/ui/video-settings-dialog.d.ts +0 -11
  92. package/dist/web-component/supersplat-core/ui/video-settings-dialog.d.ts.map +0 -1
  93. package/dist/web-component/supersplat-core/ui/view-cube.d.ts +0 -9
  94. package/dist/web-component/supersplat-core/ui/view-cube.d.ts.map +0 -1
  95. package/dist/web-component/supersplat-core/ui/view-panel.d.ts +0 -8
  96. package/dist/web-component/supersplat-core/ui/view-panel.d.ts.map +0 -1
  97. package/dist/web-component/types/supersplat-core/ui/bottom-toolbar.d.ts +0 -8
  98. package/dist/web-component/types/supersplat-core/ui/bottom-toolbar.d.ts.map +0 -1
  99. package/dist/web-component/types/supersplat-core/ui/color-panel.d.ts +0 -8
  100. package/dist/web-component/types/supersplat-core/ui/color-panel.d.ts.map +0 -1
  101. package/dist/web-component/types/supersplat-core/ui/color.d.ts +0 -20
  102. package/dist/web-component/types/supersplat-core/ui/color.d.ts.map +0 -1
  103. package/dist/web-component/types/supersplat-core/ui/data-panel.d.ts +0 -7
  104. package/dist/web-component/types/supersplat-core/ui/data-panel.d.ts.map +0 -1
  105. package/dist/web-component/types/supersplat-core/ui/editor.d.ts +0 -14
  106. package/dist/web-component/types/supersplat-core/ui/editor.d.ts.map +0 -1
  107. package/dist/web-component/types/supersplat-core/ui/export-popup.d.ts +0 -11
  108. package/dist/web-component/types/supersplat-core/ui/export-popup.d.ts.map +0 -1
  109. package/dist/web-component/types/supersplat-core/ui/histogram.d.ts +0 -32
  110. package/dist/web-component/types/supersplat-core/ui/histogram.d.ts.map +0 -1
  111. package/dist/web-component/types/supersplat-core/ui/image-settings-dialog.d.ts +0 -11
  112. package/dist/web-component/types/supersplat-core/ui/image-settings-dialog.d.ts.map +0 -1
  113. package/dist/web-component/types/supersplat-core/ui/localization.d.ts +0 -9
  114. package/dist/web-component/types/supersplat-core/ui/localization.d.ts.map +0 -1
  115. package/dist/web-component/types/supersplat-core/ui/menu-panel.d.ts +0 -21
  116. package/dist/web-component/types/supersplat-core/ui/menu-panel.d.ts.map +0 -1
  117. package/dist/web-component/types/supersplat-core/ui/menu.d.ts +0 -7
  118. package/dist/web-component/types/supersplat-core/ui/menu.d.ts.map +0 -1
  119. package/dist/web-component/types/supersplat-core/ui/mode-toggle.d.ts +0 -8
  120. package/dist/web-component/types/supersplat-core/ui/mode-toggle.d.ts.map +0 -1
  121. package/dist/web-component/types/supersplat-core/ui/popup.d.ts +0 -16
  122. package/dist/web-component/types/supersplat-core/ui/popup.d.ts.map +0 -1
  123. package/dist/web-component/types/supersplat-core/ui/progress.d.ts +0 -9
  124. package/dist/web-component/types/supersplat-core/ui/progress.d.ts.map +0 -1
  125. package/dist/web-component/types/supersplat-core/ui/publish-settings-dialog.d.ts +0 -11
  126. package/dist/web-component/types/supersplat-core/ui/publish-settings-dialog.d.ts.map +0 -1
  127. package/dist/web-component/types/supersplat-core/ui/right-toolbar.d.ts +0 -8
  128. package/dist/web-component/types/supersplat-core/ui/right-toolbar.d.ts.map +0 -1
  129. package/dist/web-component/types/supersplat-core/ui/scene-panel.d.ts +0 -8
  130. package/dist/web-component/types/supersplat-core/ui/scene-panel.d.ts.map +0 -1
  131. package/dist/web-component/types/supersplat-core/ui/shortcuts-popup.d.ts +0 -6
  132. package/dist/web-component/types/supersplat-core/ui/shortcuts-popup.d.ts.map +0 -1
  133. package/dist/web-component/types/supersplat-core/ui/spinner.d.ts +0 -6
  134. package/dist/web-component/types/supersplat-core/ui/spinner.d.ts.map +0 -1
  135. package/dist/web-component/types/supersplat-core/ui/splat-list.d.ts +0 -25
  136. package/dist/web-component/types/supersplat-core/ui/splat-list.d.ts.map +0 -1
  137. package/dist/web-component/types/supersplat-core/ui/timeline-panel.d.ts +0 -8
  138. package/dist/web-component/types/supersplat-core/ui/timeline-panel.d.ts.map +0 -1
  139. package/dist/web-component/types/supersplat-core/ui/tooltips.d.ts +0 -10
  140. package/dist/web-component/types/supersplat-core/ui/tooltips.d.ts.map +0 -1
  141. package/dist/web-component/types/supersplat-core/ui/transform.d.ts +0 -7
  142. package/dist/web-component/types/supersplat-core/ui/transform.d.ts.map +0 -1
  143. package/dist/web-component/types/supersplat-core/ui/video-settings-dialog.d.ts +0 -11
  144. package/dist/web-component/types/supersplat-core/ui/video-settings-dialog.d.ts.map +0 -1
  145. package/dist/web-component/types/supersplat-core/ui/view-cube.d.ts +0 -9
  146. package/dist/web-component/types/supersplat-core/ui/view-cube.d.ts.map +0 -1
  147. package/dist/web-component/types/supersplat-core/ui/view-panel.d.ts +0 -8
  148. package/dist/web-component/types/supersplat-core/ui/view-panel.d.ts.map +0 -1
@@ -105228,14 +105228,24 @@ class CameraModeManager {
105228
105228
  activateFlyMode() {
105229
105229
  if (!this.fly)
105230
105230
  return;
105231
- if (typeof this.fly.activate === 'function')
105232
- this.fly.activate();
105233
- // Align fly camera internal orientation with the current camera rotation so that
105234
- // switching from orbit -> fly does not snap the view back to the initial direction.
105231
+ // Preserve camera position and rotation when switching to fly mode
105235
105232
  try {
105233
+ // Preserve position
105234
+ const pos = this.camera.getPosition
105235
+ ? this.camera.getPosition().clone()
105236
+ : this.camera.getLocalPosition
105237
+ ? this.camera.getLocalPosition().clone()
105238
+ : null;
105239
+ if (pos && this.camera.setPosition) {
105240
+ ;
105241
+ this.camera.setPosition(pos);
105242
+ }
105243
+ // Preserve rotation (convert Euler to pitch/yaw)
105236
105244
  const euler = this.camera.getEulerAngles
105237
105245
  ? this.camera.getEulerAngles()
105238
- : null;
105246
+ : this.camera.getLocalEulerAngles
105247
+ ? this.camera.getLocalEulerAngles()
105248
+ : null;
105239
105249
  if (euler) {
105240
105250
  // These properties are part of the FlyCamera runtime state
105241
105251
  ;
@@ -105246,6 +105256,8 @@ class CameraModeManager {
105246
105256
  catch {
105247
105257
  // Best-effort sync; ignore if camera or script API differs
105248
105258
  }
105259
+ if (typeof this.fly.activate === 'function')
105260
+ this.fly.activate();
105249
105261
  }
105250
105262
  deactivateFlyMode() {
105251
105263
  if (!this.fly)
@@ -105255,6 +105267,373 @@ class CameraModeManager {
105255
105267
  }
105256
105268
  }
105257
105269
 
105270
+ /**
105271
+ * Fly camera controller for environments where PlayCanvas ScriptComponentSystem is not available
105272
+ * (e.g. supersplat-core's custom PCApp, which omits ScriptComponentSystem).
105273
+ *
105274
+ * This controller attaches DOM input listeners and updates the camera entity via `app.on('update')`.
105275
+ */
105276
+ class FlyCameraController {
105277
+ constructor(app, entity, emitFlyEvent, config) {
105278
+ // Config
105279
+ this.moveSpeed = 5.0;
105280
+ this.fastSpeedMultiplier = 3.0;
105281
+ this.slowSpeedMultiplier = 0.3;
105282
+ this.lookSensitivity = 0.2;
105283
+ this.invertY = false;
105284
+ this.keyBindings = {
105285
+ forward: 'KeyW',
105286
+ backward: 'KeyS',
105287
+ left: 'KeyA',
105288
+ right: 'KeyD',
105289
+ up: 'KeyE',
105290
+ down: 'KeyQ',
105291
+ fastMove: 'ShiftLeft',
105292
+ slowMove: 'ControlLeft',
105293
+ };
105294
+ this.smoothing = 0.8;
105295
+ this.friction = 0.85;
105296
+ this.enableCollision = false;
105297
+ this.minHeight = null;
105298
+ this.maxHeight = null;
105299
+ this._isActive = true;
105300
+ this._isPointerLocked = false;
105301
+ this._isLooking = false;
105302
+ this._pressed = new Set();
105303
+ this._velocity = new Vec3(0, 0, 0);
105304
+ this._targetVelocity = new Vec3(0, 0, 0);
105305
+ this._pitch = 0;
105306
+ this._yaw = 0;
105307
+ this._lastMoveEmitTime = 0;
105308
+ this._lastLookEmitTime = 0;
105309
+ this._updateHandler = null;
105310
+ this.app = app;
105311
+ this.entity = entity;
105312
+ this.emitFlyEvent = emitFlyEvent;
105313
+ if (config) {
105314
+ this.setConfig(config);
105315
+ }
105316
+ // Sync initial yaw/pitch from entity orientation if available
105317
+ try {
105318
+ const euler = this.entity?.getEulerAngles?.();
105319
+ if (euler) {
105320
+ this._pitch = euler.x || 0;
105321
+ this._yaw = euler.y || 0;
105322
+ }
105323
+ }
105324
+ catch {
105325
+ // ignore
105326
+ }
105327
+ this._bindInputListeners();
105328
+ }
105329
+ _bindInputListeners() {
105330
+ // Keyboard (capture phase so we see keys even when other handlers run)
105331
+ this._onKeyDown = this._handleKeyDown.bind(this);
105332
+ this._onKeyUp = this._handleKeyUp.bind(this);
105333
+ document.addEventListener('keydown', this._onKeyDown, true);
105334
+ document.addEventListener('keyup', this._onKeyUp, true);
105335
+ // Look: pointer events primary, mouse fallback
105336
+ this._onMouseMove = this._handleMouseMove.bind(this);
105337
+ this._onPointerMove = this._handlePointerMove.bind(this);
105338
+ document.addEventListener('mousemove', this._onMouseMove);
105339
+ document.addEventListener('pointermove', this._onPointerMove, true);
105340
+ const canvas = this.app?.graphicsDevice?.canvas;
105341
+ this._onMouseDown = (e) => {
105342
+ if (e.button === 0 && this._isActive)
105343
+ this._isLooking = true;
105344
+ };
105345
+ this._onPointerDown = (e) => {
105346
+ if (e.button === 0 && this._isActive) {
105347
+ this._isLooking = true;
105348
+ }
105349
+ };
105350
+ canvas?.addEventListener('mousedown', this._onMouseDown, true);
105351
+ canvas?.addEventListener('pointerdown', this._onPointerDown, true);
105352
+ this._onMouseUp = (e) => {
105353
+ if (e.button === 0)
105354
+ this._isLooking = false;
105355
+ };
105356
+ this._onPointerUp = (e) => {
105357
+ if (e.button === 0) {
105358
+ this._isLooking = false;
105359
+ }
105360
+ };
105361
+ document.addEventListener('mouseup', this._onMouseUp, true);
105362
+ document.addEventListener('pointerup', this._onPointerUp, true);
105363
+ }
105364
+ _unbindInputListeners() {
105365
+ document.removeEventListener('keydown', this._onKeyDown, true);
105366
+ document.removeEventListener('keyup', this._onKeyUp, true);
105367
+ document.removeEventListener('mousemove', this._onMouseMove);
105368
+ document.removeEventListener('pointermove', this._onPointerMove, true);
105369
+ document.removeEventListener('mouseup', this._onMouseUp, true);
105370
+ document.removeEventListener('pointerup', this._onPointerUp, true);
105371
+ const canvas = this.app?.graphicsDevice?.canvas;
105372
+ if (canvas && this._onMouseDown) {
105373
+ canvas.removeEventListener('mousedown', this._onMouseDown, true);
105374
+ }
105375
+ if (canvas && this._onPointerDown) {
105376
+ canvas.removeEventListener('pointerdown', this._onPointerDown, true);
105377
+ }
105378
+ }
105379
+ /**
105380
+ * Sync position and rotation from the camera entity.
105381
+ * Called when switching from orbit to fly mode to preserve camera state.
105382
+ */
105383
+ syncFromEntity() {
105384
+ try {
105385
+ // Preserve position
105386
+ const pos = this.entity?.getPosition?.() || this.entity?.getLocalPosition?.();
105387
+ if (pos) {
105388
+ const posVec = pos.clone ? pos.clone() : new Vec3(pos.x || 0, pos.y || 0, pos.z || 0);
105389
+ this.entity?.setPosition?.(posVec);
105390
+ this.entity?.setLocalPosition?.(posVec);
105391
+ }
105392
+ // Preserve rotation (convert Euler to pitch/yaw)
105393
+ const euler = this.entity?.getEulerAngles?.() || this.entity?.getLocalEulerAngles?.();
105394
+ if (euler) {
105395
+ // PlayCanvas Euler: x=pitch, y=yaw, z=roll
105396
+ this._pitch = euler.x || 0;
105397
+ this._yaw = euler.y || 0;
105398
+ // Apply rotation immediately so view doesn't snap
105399
+ if (this.entity?.setLocalEulerAngles) {
105400
+ this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
105401
+ }
105402
+ else if (this.entity?.setEulerAngles) {
105403
+ this.entity.setEulerAngles(this._pitch, this._yaw, 0);
105404
+ }
105405
+ }
105406
+ }
105407
+ catch {
105408
+ // ignore
105409
+ }
105410
+ }
105411
+ activate() {
105412
+ if (this._isActive)
105413
+ return;
105414
+ this._isActive = true;
105415
+ // Sync position and rotation from current camera state before activating
105416
+ this.syncFromEntity();
105417
+ if (!this._updateHandler) {
105418
+ this._updateHandler = (dt) => this.update(dt);
105419
+ this.app?.on?.('update', this._updateHandler);
105420
+ }
105421
+ }
105422
+ deactivate() {
105423
+ if (!this._isActive)
105424
+ return;
105425
+ this._isActive = false;
105426
+ this._isLooking = false;
105427
+ this._pressed.clear();
105428
+ if (this._updateHandler) {
105429
+ try {
105430
+ this.app?.off?.('update', this._updateHandler);
105431
+ }
105432
+ catch {
105433
+ // ignore
105434
+ }
105435
+ this._updateHandler = null;
105436
+ }
105437
+ }
105438
+ destroy() {
105439
+ try {
105440
+ this.deactivate();
105441
+ }
105442
+ finally {
105443
+ this._unbindInputListeners();
105444
+ }
105445
+ }
105446
+ setConfig(config) {
105447
+ if (config.moveSpeed !== undefined)
105448
+ this.moveSpeed = config.moveSpeed;
105449
+ if (config.fastSpeedMultiplier !== undefined)
105450
+ this.fastSpeedMultiplier = config.fastSpeedMultiplier;
105451
+ if (config.slowSpeedMultiplier !== undefined)
105452
+ this.slowSpeedMultiplier = config.slowSpeedMultiplier;
105453
+ if (config.lookSensitivity !== undefined)
105454
+ this.lookSensitivity = config.lookSensitivity;
105455
+ if (config.invertY !== undefined)
105456
+ this.invertY = config.invertY;
105457
+ if (config.keyBindings !== undefined)
105458
+ this.keyBindings = config.keyBindings;
105459
+ if (config.smoothing !== undefined)
105460
+ this.smoothing = config.smoothing;
105461
+ if (config.friction !== undefined)
105462
+ this.friction = config.friction;
105463
+ if (config.enableCollision !== undefined)
105464
+ this.enableCollision = config.enableCollision;
105465
+ if (config.minHeight !== undefined)
105466
+ this.minHeight = config.minHeight;
105467
+ if (config.maxHeight !== undefined)
105468
+ this.maxHeight = config.maxHeight;
105469
+ }
105470
+ getState() {
105471
+ const pos = this.entity?.getPosition?.();
105472
+ return {
105473
+ position: { x: pos?.x ?? 0, y: pos?.y ?? 0, z: pos?.z ?? 0 },
105474
+ rotation: { pitch: this._pitch, yaw: this._yaw },
105475
+ velocity: { x: this._velocity.x, y: this._velocity.y, z: this._velocity.z },
105476
+ isMoving: Math.abs(this._velocity.x) +
105477
+ Math.abs(this._velocity.y) +
105478
+ Math.abs(this._velocity.z) >
105479
+ 1e-4,
105480
+ };
105481
+ }
105482
+ _handleKeyDown(e) {
105483
+ const keys = [];
105484
+ if (e.code)
105485
+ keys.push(e.code);
105486
+ if (e.key) {
105487
+ keys.push(e.key);
105488
+ if (e.key.length === 1) {
105489
+ keys.push(`Key${e.key.toUpperCase()}`);
105490
+ keys.push(e.key.toUpperCase());
105491
+ keys.push(e.key.toLowerCase());
105492
+ }
105493
+ }
105494
+ for (const k of keys)
105495
+ this._pressed.add(k);
105496
+ }
105497
+ _handleKeyUp(e) {
105498
+ const keys = [];
105499
+ if (e.code)
105500
+ keys.push(e.code);
105501
+ if (e.key) {
105502
+ keys.push(e.key);
105503
+ if (e.key.length === 1) {
105504
+ keys.push(`Key${e.key.toUpperCase()}`);
105505
+ keys.push(e.key.toUpperCase());
105506
+ keys.push(e.key.toLowerCase());
105507
+ }
105508
+ }
105509
+ for (const k of keys)
105510
+ this._pressed.delete(k);
105511
+ }
105512
+ _handleMouseMove(e) {
105513
+ if (!this._isLooking || !this._isActive)
105514
+ return;
105515
+ const dx = e.movementX * this.lookSensitivity;
105516
+ const dy = e.movementY * this.lookSensitivity * (this.invertY ? 1 : -1);
105517
+ this._yaw = (this._yaw - dx) % 360;
105518
+ this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105519
+ }
105520
+ _handlePointerMove(e) {
105521
+ if (!this._isLooking || !this._isActive)
105522
+ return;
105523
+ const dx = (e.movementX || 0) * this.lookSensitivity;
105524
+ const dy = (e.movementY || 0) * this.lookSensitivity * (this.invertY ? 1 : -1);
105525
+ this._yaw = (this._yaw - dx) % 360;
105526
+ this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105527
+ }
105528
+ _getEffectiveSpeed() {
105529
+ const fast = this._pressed.has(this.keyBindings.fastMove);
105530
+ const slow = this._pressed.has(this.keyBindings.slowMove);
105531
+ let s = this.moveSpeed;
105532
+ if (fast)
105533
+ s *= this.fastSpeedMultiplier;
105534
+ if (slow)
105535
+ s *= this.slowSpeedMultiplier;
105536
+ return s;
105537
+ }
105538
+ _updateVelocity() {
105539
+ const kb = this.keyBindings;
105540
+ const isPressed = (binding, fallbacks) => {
105541
+ const all = [binding, ...fallbacks].filter(Boolean);
105542
+ return all.some(k => this._pressed.has(k));
105543
+ };
105544
+ const forward = isPressed(kb.forward, ['KeyW', 'w', 'W']) ? 1 : 0;
105545
+ const backward = isPressed(kb.backward, ['KeyS', 's', 'S']) ? 1 : 0;
105546
+ const left = isPressed(kb.left, ['KeyA', 'a', 'A']) ? 1 : 0;
105547
+ const right = isPressed(kb.right, ['KeyD', 'd', 'D']) ? 1 : 0;
105548
+ const up = isPressed(kb.up, ['KeyE', 'e', 'E']) ? 1 : 0;
105549
+ const down = isPressed(kb.down, ['KeyQ', 'q', 'Q']) ? 1 : 0;
105550
+ const inputZ = forward - backward;
105551
+ const inputX = right - left;
105552
+ const inputY = up - down;
105553
+ const planarLen = Math.hypot(inputX, inputZ);
105554
+ const nx = planarLen > 0 ? inputX / planarLen : 0;
105555
+ const nz = planarLen > 0 ? inputZ / planarLen : 0;
105556
+ const speed = this._getEffectiveSpeed() * 2;
105557
+ const entity = this.entity;
105558
+ const fwd = entity?.forward && entity.forward.clone
105559
+ ? entity.forward.clone()
105560
+ : entity?.forward
105561
+ ? new Vec3(entity.forward.x, entity.forward.y, entity.forward.z)
105562
+ : new Vec3(0, 0, -1);
105563
+ const rightVec = entity?.right && entity.right.clone
105564
+ ? entity.right.clone()
105565
+ : entity?.right
105566
+ ? new Vec3(entity.right.x, entity.right.y, entity.right.z)
105567
+ : new Vec3(1, 0, 0);
105568
+ const upVec = entity?.up && entity.up.clone
105569
+ ? entity.up.clone()
105570
+ : entity?.up
105571
+ ? new Vec3(entity.up.x, entity.up.y, entity.up.z)
105572
+ : Vec3.UP.clone();
105573
+ const target = new Vec3(0, 0, 0);
105574
+ target.add(fwd.mulScalar(nz * speed));
105575
+ target.add(rightVec.mulScalar(nx * speed));
105576
+ target.add(upVec.mulScalar(inputY * speed));
105577
+ this._targetVelocity.copy(target);
105578
+ this._velocity.lerp(this._velocity, this._targetVelocity, Math.min(1, this.smoothing));
105579
+ if (nx === 0 && nz === 0 && inputY === 0) {
105580
+ this._velocity.mulScalar(this.friction);
105581
+ if (this._velocity.length() < 0.0001)
105582
+ this._velocity.set(0, 0, 0);
105583
+ }
105584
+ }
105585
+ _applyMovement(dt) {
105586
+ if (this._velocity.length() === 0)
105587
+ return;
105588
+ const pos = this.entity?.getPosition?.()?.clone?.() ?? new Vec3(0, 0, 0);
105589
+ pos.add(this._velocity.clone().mulScalar(dt));
105590
+ this.entity?.setPosition?.(pos);
105591
+ }
105592
+ _applyRotation() {
105593
+ if (this.entity?.setLocalEulerAngles) {
105594
+ this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
105595
+ }
105596
+ else if (this.entity?.setEulerAngles) {
105597
+ this.entity.setEulerAngles(this._pitch, this._yaw, 0);
105598
+ }
105599
+ }
105600
+ _applyConstraints() {
105601
+ if (!this.enableCollision &&
105602
+ this.minHeight == null &&
105603
+ this.maxHeight == null) {
105604
+ return;
105605
+ }
105606
+ const pos = this.entity?.getPosition?.()?.clone?.() ?? new Vec3(0, 0, 0);
105607
+ if (this.minHeight != null)
105608
+ pos.y = Math.max(pos.y, this.minHeight);
105609
+ if (this.maxHeight != null)
105610
+ pos.y = Math.min(pos.y, this.maxHeight);
105611
+ this.entity?.setPosition?.(pos);
105612
+ }
105613
+ update(dt) {
105614
+ if (!this._isActive)
105615
+ return;
105616
+ this._updateVelocity();
105617
+ this._applyMovement(dt);
105618
+ this._applyRotation();
105619
+ this._applyConstraints();
105620
+ // Emit throttled movement/look events (100ms)
105621
+ const now = performance.now();
105622
+ if (!this._lastMoveEmitTime || now - this._lastMoveEmitTime >= 100) {
105623
+ const pos = this.entity?.getPosition?.();
105624
+ this.emitFlyEvent?.('fly-camera-move', {
105625
+ position: { x: pos?.x ?? 0, y: pos?.y ?? 0, z: pos?.z ?? 0 },
105626
+ velocity: { x: this._velocity.x, y: this._velocity.y, z: this._velocity.z },
105627
+ });
105628
+ this._lastMoveEmitTime = now;
105629
+ }
105630
+ if (!this._lastLookEmitTime || now - this._lastLookEmitTime >= 100) {
105631
+ this.emitFlyEvent?.('fly-camera-look', { pitch: this._pitch, yaw: this._yaw });
105632
+ this._lastLookEmitTime = now;
105633
+ }
105634
+ }
105635
+ }
105636
+
105258
105637
  // FlyCamera PlayCanvas script: first-person WASD movement with mouse-look
105259
105638
  function registerFlyCameraScript() {
105260
105639
  if (typeof pc === 'undefined') {
@@ -105340,10 +105719,18 @@ function registerFlyCameraScript() {
105340
105719
  this._onKeyUp = this._handleKeyUp.bind(this);
105341
105720
  document.addEventListener('keydown', this._onKeyDown, true);
105342
105721
  document.addEventListener('keyup', this._onKeyUp, true);
105343
- // Mouse move for look (while mouse button held)
105722
+ // Mouse/pointer move for look (while primary button held).
105723
+ //
105724
+ // Important: SuperSplat camera controls are pointer-event based and may call
105725
+ // preventDefault() on pointer events. When that happens, browsers often
105726
+ // suppress the corresponding legacy mouse events (mousedown/mousemove).
105727
+ // So we listen to *pointer* events as the primary path, with mouse as a
105728
+ // fallback for older environments.
105344
105729
  this._onMouseMove = this._handleMouseMove.bind(this);
105730
+ this._onPointerMove = this._handlePointerMove.bind(this);
105345
105731
  document.addEventListener('mousemove', this._onMouseMove);
105346
- // Mouse button handling: click + hold to look, release to stop
105732
+ document.addEventListener('pointermove', this._onPointerMove, true);
105733
+ // Button handling: click + hold to look, release to stop
105347
105734
  const canvas = this.app.graphicsDevice.canvas;
105348
105735
  this._onClickToLock = (e) => {
105349
105736
  // Left button enables look while held (no pointer lock)
@@ -105351,13 +105738,17 @@ function registerFlyCameraScript() {
105351
105738
  this._isLooking = true;
105352
105739
  }
105353
105740
  };
105354
- canvas.addEventListener('mousedown', this._onClickToLock);
105741
+ canvas.addEventListener('mousedown', this._onClickToLock, true);
105742
+ this._onPointerDownToLook = this._handlePointerDown.bind(this);
105743
+ canvas.addEventListener('pointerdown', this._onPointerDownToLook, true);
105355
105744
  this._onMouseUp = (e) => {
105356
105745
  if (e.button === 0) {
105357
105746
  this._isLooking = false;
105358
105747
  }
105359
105748
  };
105360
- document.addEventListener('mouseup', this._onMouseUp);
105749
+ document.addEventListener('mouseup', this._onMouseUp, true);
105750
+ this._onPointerUp = this._handlePointerUp.bind(this);
105751
+ document.addEventListener('pointerup', this._onPointerUp, true);
105361
105752
  };
105362
105753
  FlyCamera.prototype.update = function (dt) {
105363
105754
  if (!this._isActive)
@@ -105453,9 +105844,11 @@ function registerFlyCameraScript() {
105453
105844
  this._onKeyDown = this._onKeyDown || this._handleKeyDown.bind(this);
105454
105845
  this._onKeyUp = this._onKeyUp || this._handleKeyUp.bind(this);
105455
105846
  this._onMouseMove = this._onMouseMove || this._handleMouseMove.bind(this);
105847
+ this._onPointerMove = this._onPointerMove || this._handlePointerMove.bind(this);
105456
105848
  document.addEventListener('keydown', this._onKeyDown, true);
105457
105849
  document.addEventListener('keyup', this._onKeyUp, true);
105458
105850
  document.addEventListener('mousemove', this._onMouseMove);
105851
+ document.addEventListener('pointermove', this._onPointerMove, true);
105459
105852
  const canvas = this.app.graphicsDevice.canvas;
105460
105853
  this._onClickToLock =
105461
105854
  this._onClickToLock ||
@@ -105464,7 +105857,10 @@ function registerFlyCameraScript() {
105464
105857
  this._isLooking = true;
105465
105858
  }
105466
105859
  });
105467
- canvas.addEventListener('mousedown', this._onClickToLock);
105860
+ canvas.addEventListener('mousedown', this._onClickToLock, true);
105861
+ this._onPointerDownToLook =
105862
+ this._onPointerDownToLook || this._handlePointerDown.bind(this);
105863
+ canvas.addEventListener('pointerdown', this._onPointerDownToLook, true);
105468
105864
  this._onMouseUp =
105469
105865
  this._onMouseUp ||
105470
105866
  ((e) => {
@@ -105472,22 +105868,36 @@ function registerFlyCameraScript() {
105472
105868
  this._isLooking = false;
105473
105869
  }
105474
105870
  });
105475
- document.addEventListener('mouseup', this._onMouseUp);
105871
+ document.addEventListener('mouseup', this._onMouseUp, true);
105872
+ this._onPointerUp = this._onPointerUp || this._handlePointerUp.bind(this);
105873
+ document.addEventListener('pointerup', this._onPointerUp, true);
105476
105874
  };
105477
105875
  FlyCamera.prototype.deactivate = function () {
105478
105876
  if (!this._isActive)
105479
105877
  return;
105480
105878
  this._isActive = false;
105879
+ this._isLooking = false;
105880
+ try {
105881
+ this._pressed?.clear?.();
105882
+ }
105883
+ catch {
105884
+ // ignore
105885
+ }
105481
105886
  // Exit pointer lock when deactivating
105482
105887
  this._exitPointerLock();
105483
105888
  // Remove listeners
105484
105889
  document.removeEventListener('keydown', this._onKeyDown, true);
105485
105890
  document.removeEventListener('keyup', this._onKeyUp, true);
105486
105891
  document.removeEventListener('mousemove', this._onMouseMove);
105487
- document.removeEventListener('mouseup', this._onMouseUp);
105892
+ document.removeEventListener('pointermove', this._onPointerMove, true);
105893
+ document.removeEventListener('mouseup', this._onMouseUp, true);
105894
+ document.removeEventListener('pointerup', this._onPointerUp, true);
105488
105895
  const canvas = this.app?.graphicsDevice?.canvas;
105489
105896
  if (canvas && this._onClickToLock) {
105490
- canvas.removeEventListener('mousedown', this._onClickToLock);
105897
+ canvas.removeEventListener('mousedown', this._onClickToLock, true);
105898
+ }
105899
+ if (canvas && this._onPointerDownToLook) {
105900
+ canvas.removeEventListener('pointerdown', this._onPointerDownToLook, true);
105491
105901
  }
105492
105902
  };
105493
105903
  FlyCamera.prototype.setConfig = function (config) {
@@ -105576,6 +105986,27 @@ function registerFlyCameraScript() {
105576
105986
  this._yaw = (this._yaw - dx) % 360;
105577
105987
  this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105578
105988
  };
105989
+ FlyCamera.prototype._handlePointerMove = function (e) {
105990
+ if (!this._isLooking || !this._isActive)
105991
+ return;
105992
+ const dx = (e.movementX || 0) * this.lookSensitivity;
105993
+ const dy = (e.movementY || 0) * this.lookSensitivity * (this.invertY ? 1 : -1);
105994
+ this._yaw = (this._yaw - dx) % 360;
105995
+ this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105996
+ };
105997
+ FlyCamera.prototype._handlePointerDown = function (e) {
105998
+ if (!this._isActive)
105999
+ return;
106000
+ // Primary button enables look while held (no pointer lock)
106001
+ if (e.button === 0) {
106002
+ this._isLooking = true;
106003
+ }
106004
+ };
106005
+ FlyCamera.prototype._handlePointerUp = function (e) {
106006
+ if (e.button === 0) {
106007
+ this._isLooking = false;
106008
+ }
106009
+ };
105579
106010
  FlyCamera.prototype._handlePointerLockChange = function () {
105580
106011
  const canvas = this.app.graphicsDevice.canvas;
105581
106012
  this._isPointerLocked = document.pointerLockElement === canvas;
@@ -105872,7 +106303,7 @@ const resolveUniforms = (scope, values) => {
105872
106303
  scope.resolve(key).setValue(values[key]);
105873
106304
  }
105874
106305
  };
105875
- const createBlueNoiseTexture = (device) => {
106306
+ const createBlueNoiseTexture$1 = (device) => {
105876
106307
  const size = 32;
105877
106308
  const texture = new Texture$1(device, {
105878
106309
  width: size,
@@ -105951,7 +106382,7 @@ let InfiniteGrid$1 = class InfiniteGrid {
105951
106382
  throw new Error('InfiniteGrid: QuadRender is not available in this PlayCanvas version.');
105952
106383
  }
105953
106384
  this.quadRender = new QuadRender$1(this.shader);
105954
- this.blueNoiseTexture = createBlueNoiseTexture(device);
106385
+ this.blueNoiseTexture = createBlueNoiseTexture$1(device);
105955
106386
  this._createBlendState();
105956
106387
  this._registerRenderHook();
105957
106388
  }
@@ -140317,51 +140748,161 @@ function toPcVec3(value, fallback) {
140317
140748
  return fallback.clone();
140318
140749
  }
140319
140750
 
140320
- const THICK_WIREFRAME_LINE_WIDTH_UV$1 = 0.06;
140321
- const THICK_WIREFRAME_OPACITY$1 = 1.0;
140322
- const THICK_WIREFRAME_BOX_VS = /* glsl */ `
140323
- attribute vec3 vertex_position;
140324
- attribute vec2 vertex_texCoord0;
140751
+ const cache = new WeakMap();
140752
+ function createBlueNoiseTexture(device) {
140753
+ const size = 32;
140754
+ const texture = new Texture$1(device, {
140755
+ width: size,
140756
+ height: size,
140757
+ format: PIXELFORMAT_R8_G8_B8_A8,
140758
+ mipmaps: false,
140759
+ });
140760
+ texture.addressU = ADDRESS_REPEAT;
140761
+ texture.addressV = ADDRESS_REPEAT;
140762
+ texture.minFilter = FILTER_NEAREST;
140763
+ texture.magFilter = FILTER_NEAREST;
140764
+ const pixels = texture.lock();
140765
+ const seed = 1337;
140766
+ let value = seed;
140767
+ const random = () => {
140768
+ value ^= value << 13;
140769
+ value ^= value >>> 17;
140770
+ value ^= value << 5;
140771
+ return ((value >>> 0) % 256) / 255;
140772
+ };
140773
+ for (let i = 0; i < size * size; i++) {
140774
+ const noise = Math.floor(random() * 255);
140775
+ const idx = i * 4;
140776
+ pixels[idx + 0] = noise;
140777
+ pixels[idx + 1] = noise;
140778
+ pixels[idx + 2] = noise;
140779
+ pixels[idx + 3] = 255;
140780
+ }
140781
+ texture.unlock();
140782
+ texture.name = 'supersplat-blue-noise';
140783
+ return texture;
140784
+ }
140785
+ function getBlueNoiseTex32(device) {
140786
+ const existing = cache.get(device);
140787
+ if (existing)
140788
+ return existing;
140789
+ const tex = createBlueNoiseTexture(device);
140790
+ cache.set(device, tex);
140791
+ return tex;
140792
+ }
140325
140793
 
140326
- uniform mat4 matrix_model;
140327
- uniform mat4 matrix_viewProjection;
140794
+ // SuperSplat-like selection box shader: screen-space ray/box intersection that draws a thick
140795
+ // white wire/grid pattern (not dependent on WebGL line width).
140796
+ const BOX_SELECT_VS = /* glsl */ `
140797
+ attribute vec3 vertex_position;
140328
140798
 
140329
- varying vec2 vUv;
140799
+ uniform mat4 matrix_model;
140800
+ uniform mat4 matrix_viewProjection;
140330
140801
 
140331
- void main(void) {
140332
- vUv = vertex_texCoord0;
140333
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140334
- }
140802
+ void main() {
140803
+ gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140804
+ }
140335
140805
  `;
140336
- const THICK_WIREFRAME_BOX_FS = /* glsl */ `
140337
- #ifdef GL_OES_standard_derivatives
140338
- #extension GL_OES_standard_derivatives : enable
140339
- #endif
140806
+ const BOX_SELECT_FS = /* glsl */ `
140807
+ // ray-box intersection in box space
140808
+ bool intersectBox(out float t0, out float t1, out int axis0, out int axis1, vec3 pos, vec3 dir, vec3 boxCen, vec3 boxLen)
140809
+ {
140810
+ bvec3 validDir = notEqual(dir, vec3(0.0));
140811
+ vec3 absDir = abs(dir);
140812
+ vec3 signDir = sign(dir);
140813
+ vec3 m = vec3(
140814
+ validDir.x ? 1.0 / absDir.x : 0.0,
140815
+ validDir.y ? 1.0 / absDir.y : 0.0,
140816
+ validDir.z ? 1.0 / absDir.z : 0.0
140817
+ ) * signDir;
140340
140818
 
140341
- precision mediump float;
140819
+ vec3 n = m * (pos - boxCen);
140820
+ vec3 k = abs(m) * boxLen;
140342
140821
 
140343
- uniform vec3 lineColor;
140344
- uniform float lineWidth;
140345
- uniform float opacity;
140822
+ vec3 v0 = -n - k;
140823
+ vec3 v1 = -n + k;
140346
140824
 
140347
- varying vec2 vUv;
140825
+ // replace invalid axes with -inf and +inf so the tests below ignore them
140826
+ v0 = mix(vec3(-1.0 / 0.0000001), v0, validDir);
140827
+ v1 = mix(vec3(1.0 / 0.0000001), v1, validDir);
140348
140828
 
140349
- void main(void) {
140350
- // Distance to nearest face border in UV space (0 at border, 0.5 at center).
140351
- float d = min(min(vUv.x, 1.0 - vUv.x), min(vUv.y, 1.0 - vUv.y));
140829
+ axis0 = (v0.x > v0.y) ? ((v0.x > v0.z) ? 0 : 2) : ((v0.y > v0.z) ? 1 : 2);
140830
+ axis1 = (v1.x < v1.y) ? ((v1.x < v1.z) ? 0 : 2) : ((v1.y < v1.z) ? 1 : 2);
140352
140831
 
140353
- // Anti-aliased edge mask (1 near border, 0 in face interior).
140354
- float aa = 0.002;
140355
- #ifdef GL_OES_standard_derivatives
140356
- aa = max(aa, fwidth(d) * 1.5);
140357
- #endif
140358
- float a = 1.0 - smoothstep(lineWidth, lineWidth + aa, d);
140359
- a *= opacity;
140832
+ t0 = v0[axis0];
140833
+ t1 = v1[axis1];
140360
140834
 
140361
- if (a <= 0.001) discard;
140362
- gl_FragColor = vec4(lineColor, a);
140363
- }
140835
+ if (t0 > t1 || t1 < 0.0) {
140836
+ return false;
140837
+ }
140838
+
140839
+ return true;
140840
+ }
140841
+
140842
+ float calcDepth(in vec3 pos, in mat4 viewProjection) {
140843
+ vec4 v = viewProjection * vec4(pos, 1.0);
140844
+ return (v.z / v.w) * 0.5 + 0.5;
140845
+ }
140846
+
140847
+ uniform sampler2D blueNoiseTex32;
140848
+ uniform mat4 matrix_viewProjection;
140849
+ uniform vec3 boxCen;
140850
+ uniform vec3 boxLen;
140851
+
140852
+ uniform vec3 near_origin;
140853
+ uniform vec3 near_x;
140854
+ uniform vec3 near_y;
140855
+
140856
+ uniform vec3 far_origin;
140857
+ uniform vec3 far_x;
140858
+ uniform vec3 far_y;
140859
+
140860
+ uniform vec2 targetSize;
140861
+ uniform vec3 lineColor;
140862
+
140863
+ bool writeDepth(float alpha) {
140864
+ ivec2 uv = ivec2(gl_FragCoord.xy);
140865
+ ivec2 size = textureSize(blueNoiseTex32, 0);
140866
+ return alpha > texelFetch(blueNoiseTex32, uv % size, 0).y;
140867
+ }
140868
+
140869
+ bool strips(vec3 pos, int axis) {
140870
+ // Thickness tuned to match SuperSplat viewer "thick wire"
140871
+ bvec3 b = lessThan(fract(pos * 2.0 + vec3(0.015)), vec3(0.06));
140872
+ b[axis] = false;
140873
+ return any(b);
140874
+ }
140875
+
140876
+ void main() {
140877
+ vec2 clip = gl_FragCoord.xy / targetSize;
140878
+ vec3 worldNear = near_origin + near_x * clip.x + near_y * clip.y;
140879
+ vec3 worldFar = far_origin + far_x * clip.x + far_y * clip.y;
140880
+ vec3 rayDir = normalize(worldFar - worldNear);
140881
+
140882
+ float t0, t1;
140883
+ int axis0, axis1;
140884
+ if (!intersectBox(t0, t1, axis0, axis1, worldNear, rayDir, boxCen, boxLen)) {
140885
+ discard;
140886
+ }
140887
+
140888
+ vec3 frontPos = worldNear + rayDir * t0;
140889
+ bool front = t0 > 0.0 && strips(frontPos - boxCen, axis0);
140890
+
140891
+ vec3 backPos = worldNear + rayDir * t1;
140892
+ bool back = strips(backPos - boxCen, axis1);
140893
+
140894
+ if (front) {
140895
+ gl_FragColor = vec4(lineColor, 0.6);
140896
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(frontPos, matrix_viewProjection) : 1.0;
140897
+ } else if (back) {
140898
+ gl_FragColor = vec4(lineColor, 0.6);
140899
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(backPos, matrix_viewProjection) : 1.0;
140900
+ } else {
140901
+ discard;
140902
+ }
140903
+ }
140364
140904
  `;
140905
+ const tmpPos$1 = new Vec3();
140365
140906
  class BoxSelectionAPI {
140366
140907
  constructor() {
140367
140908
  this.boxes = new Map();
@@ -140398,6 +140939,7 @@ class BoxSelectionAPI {
140398
140939
  this.ensureTranslateGizmo();
140399
140940
  this.updateGizmoSize();
140400
140941
  this.updateGizmoAttachment();
140942
+ this.updateBoxShaderUniforms();
140401
140943
  }
140402
140944
  createBox(options = {}) {
140403
140945
  if (!this.app || !this.parent) {
@@ -140420,7 +140962,7 @@ class BoxSelectionAPI {
140420
140962
  entity.render.castShadows = false;
140421
140963
  entity.render.receiveShadows = false;
140422
140964
  entity.render.enabled = visible;
140423
- // Render as a thick wireframe (shader-based) so line width is consistent across browsers.
140965
+ // Render as a thick wireframe (shader-based) to match SuperSplat viewer.
140424
140966
  this.parent.addChild(entity);
140425
140967
  const record = {
140426
140968
  id,
@@ -140507,8 +141049,8 @@ class BoxSelectionAPI {
140507
141049
  return false;
140508
141050
  }
140509
141051
  record.color.set(r, g, b);
140510
- const mat = this.buildWireframeMaterial(record.color);
140511
- record.entity.render.material = mat;
141052
+ const mat = record.entity.render?.material;
141053
+ mat?.setParameter?.('lineColor', [record.color.x, record.color.y, record.color.z]);
140512
141054
  this.requestRender();
140513
141055
  return true;
140514
141056
  }
@@ -140703,19 +141245,34 @@ class BoxSelectionAPI {
140703
141245
  }
140704
141246
  buildWireframeMaterial(color) {
140705
141247
  const material = new ShaderMaterial$1({
140706
- uniqueName: 'boxSelectionThickWireframe',
140707
- vertexGLSL: THICK_WIREFRAME_BOX_VS,
140708
- fragmentGLSL: THICK_WIREFRAME_BOX_FS,
141248
+ uniqueName: 'boxSelectionSupersplatWire',
141249
+ vertexGLSL: BOX_SELECT_VS,
141250
+ fragmentGLSL: BOX_SELECT_FS,
140709
141251
  });
140710
- material.cull = CULLFACE_NONE;
140711
- material.depthWrite = false;
141252
+ material.cull = CULLFACE_FRONT;
140712
141253
  material.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE_MINUS_SRC_ALPHA);
140713
- material.setParameter('lineColor', [color.x, color.y, color.z]);
140714
- material.setParameter('lineWidth', THICK_WIREFRAME_LINE_WIDTH_UV$1);
140715
- material.setParameter('opacity', THICK_WIREFRAME_OPACITY$1);
140716
141254
  material.update();
141255
+ material.setParameter('lineColor', [color.x, color.y, color.z]);
140717
141256
  return material;
140718
141257
  }
141258
+ updateBoxShaderUniforms() {
141259
+ if (!this.app)
141260
+ return;
141261
+ const device = this.app.graphicsDevice;
141262
+ // Ensure required global uniforms for selection shaders.
141263
+ device.scope.resolve('targetSize').setValue([device.width, device.height]);
141264
+ device.scope.resolve('blueNoiseTex32').setValue(getBlueNoiseTex32(device));
141265
+ for (const record of this.boxes.values()) {
141266
+ const mat = record.entity.render?.material;
141267
+ if (!mat?.setParameter)
141268
+ continue;
141269
+ // World-space center
141270
+ record.entity.getWorldTransform().getTranslation(tmpPos$1);
141271
+ mat.setParameter('boxCen', [tmpPos$1.x, tmpPos$1.y, tmpPos$1.z]);
141272
+ // Half extents
141273
+ mat.setParameter('boxLen', [record.lenX * 0.5, record.lenY * 0.5, record.lenZ * 0.5]);
141274
+ }
141275
+ }
140719
141276
  emitEvent(event, detail) {
140720
141277
  if (this.onEvent) {
140721
141278
  this.onEvent(event, detail);
@@ -140727,59 +141284,110 @@ class BoxSelectionAPI {
140727
141284
  }
140728
141285
  }
140729
141286
 
140730
- const THICK_WIREFRAME_LINE_WIDTH_UV = 0.035;
140731
- const THICK_WIREFRAME_OPACITY = 1.0;
140732
- const SPHERE_GRID_U = 14.0;
140733
- const SPHERE_GRID_V = 10.0;
140734
- const THICK_WIREFRAME_SPHERE_VS = /* glsl */ `
140735
- attribute vec3 vertex_position;
140736
- attribute vec2 vertex_texCoord0;
140737
-
140738
- uniform mat4 matrix_model;
140739
- uniform mat4 matrix_viewProjection;
141287
+ // SuperSplat-like selection sphere shader: screen-space ray/sphere intersection that draws a thick
141288
+ // white wire/grid pattern (not dependent on WebGL line width).
141289
+ const SPHERE_SELECT_VS = /* glsl */ `
141290
+ attribute vec3 vertex_position;
140740
141291
 
140741
- varying vec2 vUv;
141292
+ uniform mat4 matrix_model;
141293
+ uniform mat4 matrix_viewProjection;
140742
141294
 
140743
- void main(void) {
140744
- vUv = vertex_texCoord0;
140745
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140746
- }
141295
+ void main() {
141296
+ gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
141297
+ }
140747
141298
  `;
140748
- const THICK_WIREFRAME_SPHERE_FS = /* glsl */ `
140749
- #ifdef GL_OES_standard_derivatives
140750
- #extension GL_OES_standard_derivatives : enable
140751
- #endif
141299
+ const SPHERE_SELECT_FS = /* glsl */ `
141300
+ bool intersectSphere(out float t0, out float t1, vec3 pos, vec3 dir, vec4 sphere) {
141301
+ vec3 L = sphere.xyz - pos;
141302
+ float tca = dot(L, dir);
141303
+
141304
+ float d2 = sphere.w * sphere.w - (dot(L, L) - tca * tca);
141305
+ if (d2 <= 0.0) {
141306
+ return false;
141307
+ }
141308
+
141309
+ float thc = sqrt(d2);
141310
+ t0 = tca - thc;
141311
+ t1 = tca + thc;
141312
+ if (t1 <= 0.0) {
141313
+ return false;
141314
+ }
140752
141315
 
140753
- precision mediump float;
141316
+ return true;
141317
+ }
140754
141318
 
140755
- uniform vec3 lineColor;
140756
- uniform float lineWidth;
140757
- uniform float opacity;
141319
+ float calcDepth(in vec3 pos, in mat4 viewProjection) {
141320
+ vec4 v = viewProjection * vec4(pos, 1.0);
141321
+ return (v.z / v.w) * 0.5 + 0.5;
141322
+ }
140758
141323
 
140759
- varying vec2 vUv;
141324
+ vec2 calcAzimuthElev(in vec3 dir) {
141325
+ float azimuth = atan(dir.z, dir.x);
141326
+ float elev = asin(dir.y);
141327
+ return vec2(azimuth, elev) * 180.0 / 3.14159;
141328
+ }
140760
141329
 
140761
- float gridLineDist(float v, float freq) {
140762
- float f = fract(v * freq);
140763
- return min(f, 1.0 - f);
140764
- }
141330
+ uniform sampler2D blueNoiseTex32;
141331
+ uniform mat4 matrix_viewProjection;
141332
+ uniform vec4 sphere;
141333
+ uniform vec3 lineColor;
140765
141334
 
140766
- void main(void) {
140767
- // "Wireframe-like" lat/long grid in UV space.
140768
- float du = gridLineDist(vUv.x, ${SPHERE_GRID_U});
140769
- float dv = gridLineDist(vUv.y, ${SPHERE_GRID_V});
140770
- float d = min(du, dv);
140771
-
140772
- float aa = 0.002;
140773
- #ifdef GL_OES_standard_derivatives
140774
- aa = max(aa, fwidth(d) * 1.5);
140775
- #endif
140776
- float a = 1.0 - smoothstep(lineWidth, lineWidth + aa, d);
140777
- a *= opacity;
141335
+ uniform vec3 near_origin;
141336
+ uniform vec3 near_x;
141337
+ uniform vec3 near_y;
140778
141338
 
140779
- if (a <= 0.001) discard;
140780
- gl_FragColor = vec4(lineColor, a);
140781
- }
141339
+ uniform vec3 far_origin;
141340
+ uniform vec3 far_x;
141341
+ uniform vec3 far_y;
141342
+
141343
+ uniform vec2 targetSize;
141344
+
141345
+ bool writeDepth(float alpha) {
141346
+ vec2 uv = fract(gl_FragCoord.xy / 32.0);
141347
+ float noise = texture2DLod(blueNoiseTex32, uv, 0.0).y;
141348
+ return alpha > noise;
141349
+ }
141350
+
141351
+ bool strips(vec3 lp) {
141352
+ vec2 ae = calcAzimuthElev(normalize(lp));
141353
+
141354
+ float spacing = 180.0 / (2.0 * 3.14159 * sphere.w);
141355
+ // Thickness tuned to match SuperSplat viewer "thick wire"
141356
+ float size = 0.06;
141357
+ return fract(ae.x / spacing) < size ||
141358
+ fract(ae.y / spacing) < size;
141359
+ }
141360
+
141361
+ void main() {
141362
+ vec2 clip = gl_FragCoord.xy / targetSize;
141363
+ vec3 worldNear = near_origin + near_x * clip.x + near_y * clip.y;
141364
+ vec3 worldFar = far_origin + far_x * clip.x + far_y * clip.y;
141365
+
141366
+ vec3 rayDir = normalize(worldFar - worldNear);
141367
+
141368
+ float t0, t1;
141369
+ if (!intersectSphere(t0, t1, worldNear, rayDir, sphere)) {
141370
+ discard;
141371
+ }
141372
+
141373
+ vec3 frontPos = worldNear + rayDir * t0;
141374
+ bool front = t0 > 0.0 && strips(frontPos - sphere.xyz);
141375
+
141376
+ vec3 backPos = worldNear + rayDir * t1;
141377
+ bool back = strips(backPos - sphere.xyz);
141378
+
141379
+ if (front) {
141380
+ gl_FragColor = vec4(lineColor, 0.6);
141381
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(frontPos, matrix_viewProjection) : 1.0;
141382
+ } else if (back) {
141383
+ gl_FragColor = vec4(lineColor, 0.6);
141384
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(backPos, matrix_viewProjection) : 1.0;
141385
+ } else {
141386
+ discard;
141387
+ }
141388
+ }
140782
141389
  `;
141390
+ const tmpPos = new Vec3();
140783
141391
  class SphereSelectionAPI {
140784
141392
  constructor() {
140785
141393
  this.spheres = new Map();
@@ -140815,6 +141423,7 @@ class SphereSelectionAPI {
140815
141423
  this.ensureTranslateGizmo();
140816
141424
  this.updateGizmoSize();
140817
141425
  this.updateGizmoAttachment();
141426
+ this.updateSphereShaderUniforms();
140818
141427
  }
140819
141428
  createSphere(options = {}) {
140820
141429
  if (!this.app || !this.parent) {
@@ -140827,7 +141436,8 @@ class SphereSelectionAPI {
140827
141436
  const visible = options.visible ?? true;
140828
141437
  const entity = new Entity(id);
140829
141438
  entity.addComponent('render', {
140830
- type: 'sphere',
141439
+ // Use a box proxy like SuperSplat; shader ray-marches a true sphere.
141440
+ type: 'box',
140831
141441
  material: this.buildWireframeMaterial(color),
140832
141442
  });
140833
141443
  entity.setLocalScale(radius * 2, radius * 2, radius * 2); // sphere diameter = radius * 2
@@ -140835,7 +141445,7 @@ class SphereSelectionAPI {
140835
141445
  entity.render.castShadows = false;
140836
141446
  entity.render.receiveShadows = false;
140837
141447
  entity.render.enabled = visible;
140838
- // Render as a thick wireframe (shader-based) so line width is consistent across browsers.
141448
+ // Render as a thick wireframe (shader-based) to match SuperSplat viewer.
140839
141449
  this.parent.addChild(entity);
140840
141450
  const record = {
140841
141451
  id,
@@ -140918,8 +141528,12 @@ class SphereSelectionAPI {
140918
141528
  return false;
140919
141529
  }
140920
141530
  record.color.set(r, g, b);
140921
- const mat = this.buildWireframeMaterial(record.color);
140922
- record.entity.render.material = mat;
141531
+ const mat = record.entity.render?.material;
141532
+ mat?.setParameter?.('lineColor', [
141533
+ record.color.x,
141534
+ record.color.y,
141535
+ record.color.z,
141536
+ ]);
140923
141537
  this.requestRender();
140924
141538
  return true;
140925
141539
  }
@@ -141117,19 +141731,30 @@ class SphereSelectionAPI {
141117
141731
  }
141118
141732
  buildWireframeMaterial(color) {
141119
141733
  const material = new ShaderMaterial$1({
141120
- uniqueName: 'sphereSelectionThickWireframe',
141121
- vertexGLSL: THICK_WIREFRAME_SPHERE_VS,
141122
- fragmentGLSL: THICK_WIREFRAME_SPHERE_FS,
141734
+ uniqueName: 'sphereSelectionSupersplatWire',
141735
+ vertexGLSL: SPHERE_SELECT_VS,
141736
+ fragmentGLSL: SPHERE_SELECT_FS,
141123
141737
  });
141124
- material.cull = CULLFACE_NONE;
141125
- material.depthWrite = false;
141738
+ material.cull = CULLFACE_FRONT;
141126
141739
  material.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE_MINUS_SRC_ALPHA);
141127
- material.setParameter('lineColor', [color.x, color.y, color.z]);
141128
- material.setParameter('lineWidth', THICK_WIREFRAME_LINE_WIDTH_UV);
141129
- material.setParameter('opacity', THICK_WIREFRAME_OPACITY);
141130
141740
  material.update();
141741
+ material.setParameter('lineColor', [color.x, color.y, color.z]);
141131
141742
  return material;
141132
141743
  }
141744
+ updateSphereShaderUniforms() {
141745
+ if (!this.app)
141746
+ return;
141747
+ const device = this.app.graphicsDevice;
141748
+ device.scope.resolve('targetSize').setValue([device.width, device.height]);
141749
+ device.scope.resolve('blueNoiseTex32').setValue(getBlueNoiseTex32(device));
141750
+ for (const record of this.spheres.values()) {
141751
+ const mat = record.entity.render?.material;
141752
+ if (!mat?.setParameter)
141753
+ continue;
141754
+ record.entity.getWorldTransform().getTranslation(tmpPos);
141755
+ mat.setParameter('sphere', [tmpPos.x, tmpPos.y, tmpPos.z, record.radius]);
141756
+ }
141757
+ }
141133
141758
  emitEvent(event, detail) {
141134
141759
  if (this.onEvent) {
141135
141760
  this.onEvent(event, detail);
@@ -146756,7 +147381,7 @@ class SupersplatAdapter {
146756
147381
  const { config } = this.scene;
146757
147382
  const state = this.viewerEventState;
146758
147383
  // Colors and view settings from scene config
146759
- const selectedClr = config.selectedClr ?? { r: 1, g: 1, b: 0, a: 1 };
147384
+ const selectedClr = config.selectedClr ?? { r: 1, g: 0.5, b: 0, a: 1 };
146760
147385
  const unselectedClr = config.unselectedClr ?? { r: 0, g: 0, b: 1, a: 0.5 };
146761
147386
  const lockedClr = config.lockedClr ?? { r: 0, g: 0, b: 0, a: 0.05 };
146762
147387
  const bgClr = config.bgClr ?? { r: 0, g: 0, b: 0, a: 1 };
@@ -147190,9 +147815,10 @@ class SplatViewerCore {
147190
147815
  this.app = ctx.app;
147191
147816
  this.entities.camera = ctx.camera;
147192
147817
  const cameraAny = this.entities.camera;
147193
- if (cameraAny &&
147194
- !cameraAny.script &&
147195
- typeof cameraAny.addComponent === 'function') {
147818
+ // SuperSplat's PCApp omits ScriptComponentSystem, so `addComponent('script')`
147819
+ // will not produce `camera.script.create()`. We keep the attempt (in case
147820
+ // the underlying app changes), but also support a controller-based fallback.
147821
+ if (cameraAny && !cameraAny.script && typeof cameraAny.addComponent === 'function') {
147196
147822
  try {
147197
147823
  cameraAny.addComponent('script');
147198
147824
  }
@@ -147215,16 +147841,23 @@ class SplatViewerCore {
147215
147841
  minHeight: DEFAULT_FLY_CAMERA_CONFIG.minHeight,
147216
147842
  maxHeight: DEFAULT_FLY_CAMERA_CONFIG.maxHeight,
147217
147843
  };
147218
- this._fly = cameraAny?.script?.create?.('flyCamera', {
147219
- attributes: flyAttributes,
147220
- });
147221
- if (this._fly) {
147222
- ;
147223
- this._fly.emitFlyEvent = (type, detail) => {
147224
- this.emit({ type: type, detail });
147225
- };
147226
- // Deactivate by default; orbit is the initial mode
147227
- this._fly.deactivate?.();
147844
+ // Prefer script-based fly when available; fallback to controller otherwise.
147845
+ const canCreateScript = typeof cameraAny?.script?.create === 'function';
147846
+ if (canCreateScript) {
147847
+ const created = cameraAny.script.create('flyCamera', { attributes: flyAttributes });
147848
+ this._fly = created;
147849
+ if (this._fly) {
147850
+ ;
147851
+ this._fly.emitFlyEvent = (type, detail) => {
147852
+ this.emit({ type: type, detail });
147853
+ };
147854
+ this._fly.deactivate?.();
147855
+ }
147856
+ }
147857
+ else {
147858
+ const controller = new FlyCameraController(ctx.app, cameraAny, (type, detail) => this.emit({ type: type, detail }), flyAttributes);
147859
+ controller.deactivate();
147860
+ this._fly = controller;
147228
147861
  }
147229
147862
  this._cameraMode = 'orbit';
147230
147863
  }
@@ -149093,17 +149726,31 @@ class SplatViewerCore {
149093
149726
  // Stop supersplat orbit updates + input
149094
149727
  this._supersplat.setCameraControlsEnabled(false);
149095
149728
  this._supersplat.setCameraManualControl(true);
149096
- // Align fly yaw/pitch with current camera rotation to avoid snapping
149097
- try {
149098
- const euler = this.entities.camera?.getEulerAngles?.();
149099
- if (euler && this._fly) {
149100
- ;
149101
- this._fly._pitch = euler.x || 0;
149102
- this._fly._yaw = euler.y || 0;
149729
+ // Preserve camera position and rotation when switching to fly mode
149730
+ if (this._fly) {
149731
+ // For FlyCameraController (fallback path)
149732
+ if (typeof this._fly.syncFromEntity === 'function') {
149733
+ this._fly.syncFromEntity();
149734
+ }
149735
+ else {
149736
+ // For FlyCameraScript (legacy path)
149737
+ try {
149738
+ const pos = this.entities.camera?.getPosition?.();
149739
+ if (pos) {
149740
+ const posVec = pos.clone ? pos.clone() : new Vec3(pos.x || 0, pos.y || 0, pos.z || 0);
149741
+ this.entities.camera?.setPosition?.(posVec);
149742
+ }
149743
+ const euler = this.entities.camera?.getEulerAngles?.();
149744
+ if (euler) {
149745
+ ;
149746
+ this._fly._pitch = euler.x || 0;
149747
+ this._fly._yaw = euler.y || 0;
149748
+ }
149749
+ }
149750
+ catch {
149751
+ // ignore
149752
+ }
149103
149753
  }
149104
- }
149105
- catch {
149106
- // ignore
149107
149754
  }
149108
149755
  this._fly?.activate?.();
149109
149756
  this._cameraMode = 'fly';
@@ -149220,9 +149867,48 @@ class SplatViewerCore {
149220
149867
  this._cameraModeManager?.setFlyConfig(config);
149221
149868
  }
149222
149869
  getFlyCameraConfig() {
149870
+ // SuperSplat path: fly is either a script instance or controller fallback
149871
+ if (this._supersplat && this._fly) {
149872
+ try {
149873
+ const flyAny = this._fly;
149874
+ const cfg = {
149875
+ moveSpeed: flyAny.moveSpeed,
149876
+ fastSpeedMultiplier: flyAny.fastSpeedMultiplier,
149877
+ slowSpeedMultiplier: flyAny.slowSpeedMultiplier,
149878
+ lookSensitivity: flyAny.lookSensitivity,
149879
+ invertY: !!flyAny.invertY,
149880
+ keyBindings: { ...(flyAny.keyBindings || {}) },
149881
+ smoothing: flyAny.smoothing,
149882
+ friction: flyAny.friction,
149883
+ enableCollision: !!flyAny.enableCollision,
149884
+ minHeight: flyAny.minHeight ?? null,
149885
+ maxHeight: flyAny.maxHeight ?? null,
149886
+ };
149887
+ return cfg;
149888
+ }
149889
+ catch {
149890
+ return null;
149891
+ }
149892
+ }
149223
149893
  return this._cameraModeManager?.getFlyConfig() || null;
149224
149894
  }
149225
149895
  getFlyCameraState() {
149896
+ // SuperSplat path: fly is either a script instance or controller fallback
149897
+ if (this._supersplat && this._fly?.getState) {
149898
+ try {
149899
+ const state = this._fly.getState();
149900
+ return {
149901
+ mode: this._cameraMode,
149902
+ position: state.position,
149903
+ rotation: state.rotation,
149904
+ velocity: state.velocity,
149905
+ isMoving: state.isMoving,
149906
+ };
149907
+ }
149908
+ catch {
149909
+ return null;
149910
+ }
149911
+ }
149226
149912
  return this._cameraModeManager?.getFlyState() || null;
149227
149913
  }
149228
149914
  _setupScene() {