@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
@@ -105235,14 +105235,24 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105235
105235
  activateFlyMode() {
105236
105236
  if (!this.fly)
105237
105237
  return;
105238
- if (typeof this.fly.activate === 'function')
105239
- this.fly.activate();
105240
- // Align fly camera internal orientation with the current camera rotation so that
105241
- // switching from orbit -> fly does not snap the view back to the initial direction.
105238
+ // Preserve camera position and rotation when switching to fly mode
105242
105239
  try {
105240
+ // Preserve position
105241
+ const pos = this.camera.getPosition
105242
+ ? this.camera.getPosition().clone()
105243
+ : this.camera.getLocalPosition
105244
+ ? this.camera.getLocalPosition().clone()
105245
+ : null;
105246
+ if (pos && this.camera.setPosition) {
105247
+ ;
105248
+ this.camera.setPosition(pos);
105249
+ }
105250
+ // Preserve rotation (convert Euler to pitch/yaw)
105243
105251
  const euler = this.camera.getEulerAngles
105244
105252
  ? this.camera.getEulerAngles()
105245
- : null;
105253
+ : this.camera.getLocalEulerAngles
105254
+ ? this.camera.getLocalEulerAngles()
105255
+ : null;
105246
105256
  if (euler) {
105247
105257
  // These properties are part of the FlyCamera runtime state
105248
105258
  ;
@@ -105253,6 +105263,8 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105253
105263
  catch {
105254
105264
  // Best-effort sync; ignore if camera or script API differs
105255
105265
  }
105266
+ if (typeof this.fly.activate === 'function')
105267
+ this.fly.activate();
105256
105268
  }
105257
105269
  deactivateFlyMode() {
105258
105270
  if (!this.fly)
@@ -105262,6 +105274,373 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105262
105274
  }
105263
105275
  }
105264
105276
 
105277
+ /**
105278
+ * Fly camera controller for environments where PlayCanvas ScriptComponentSystem is not available
105279
+ * (e.g. supersplat-core's custom PCApp, which omits ScriptComponentSystem).
105280
+ *
105281
+ * This controller attaches DOM input listeners and updates the camera entity via `app.on('update')`.
105282
+ */
105283
+ class FlyCameraController {
105284
+ constructor(app, entity, emitFlyEvent, config) {
105285
+ // Config
105286
+ this.moveSpeed = 5.0;
105287
+ this.fastSpeedMultiplier = 3.0;
105288
+ this.slowSpeedMultiplier = 0.3;
105289
+ this.lookSensitivity = 0.2;
105290
+ this.invertY = false;
105291
+ this.keyBindings = {
105292
+ forward: 'KeyW',
105293
+ backward: 'KeyS',
105294
+ left: 'KeyA',
105295
+ right: 'KeyD',
105296
+ up: 'KeyE',
105297
+ down: 'KeyQ',
105298
+ fastMove: 'ShiftLeft',
105299
+ slowMove: 'ControlLeft',
105300
+ };
105301
+ this.smoothing = 0.8;
105302
+ this.friction = 0.85;
105303
+ this.enableCollision = false;
105304
+ this.minHeight = null;
105305
+ this.maxHeight = null;
105306
+ this._isActive = true;
105307
+ this._isPointerLocked = false;
105308
+ this._isLooking = false;
105309
+ this._pressed = new Set();
105310
+ this._velocity = new Vec3(0, 0, 0);
105311
+ this._targetVelocity = new Vec3(0, 0, 0);
105312
+ this._pitch = 0;
105313
+ this._yaw = 0;
105314
+ this._lastMoveEmitTime = 0;
105315
+ this._lastLookEmitTime = 0;
105316
+ this._updateHandler = null;
105317
+ this.app = app;
105318
+ this.entity = entity;
105319
+ this.emitFlyEvent = emitFlyEvent;
105320
+ if (config) {
105321
+ this.setConfig(config);
105322
+ }
105323
+ // Sync initial yaw/pitch from entity orientation if available
105324
+ try {
105325
+ const euler = this.entity?.getEulerAngles?.();
105326
+ if (euler) {
105327
+ this._pitch = euler.x || 0;
105328
+ this._yaw = euler.y || 0;
105329
+ }
105330
+ }
105331
+ catch {
105332
+ // ignore
105333
+ }
105334
+ this._bindInputListeners();
105335
+ }
105336
+ _bindInputListeners() {
105337
+ // Keyboard (capture phase so we see keys even when other handlers run)
105338
+ this._onKeyDown = this._handleKeyDown.bind(this);
105339
+ this._onKeyUp = this._handleKeyUp.bind(this);
105340
+ document.addEventListener('keydown', this._onKeyDown, true);
105341
+ document.addEventListener('keyup', this._onKeyUp, true);
105342
+ // Look: pointer events primary, mouse fallback
105343
+ this._onMouseMove = this._handleMouseMove.bind(this);
105344
+ this._onPointerMove = this._handlePointerMove.bind(this);
105345
+ document.addEventListener('mousemove', this._onMouseMove);
105346
+ document.addEventListener('pointermove', this._onPointerMove, true);
105347
+ const canvas = this.app?.graphicsDevice?.canvas;
105348
+ this._onMouseDown = (e) => {
105349
+ if (e.button === 0 && this._isActive)
105350
+ this._isLooking = true;
105351
+ };
105352
+ this._onPointerDown = (e) => {
105353
+ if (e.button === 0 && this._isActive) {
105354
+ this._isLooking = true;
105355
+ }
105356
+ };
105357
+ canvas?.addEventListener('mousedown', this._onMouseDown, true);
105358
+ canvas?.addEventListener('pointerdown', this._onPointerDown, true);
105359
+ this._onMouseUp = (e) => {
105360
+ if (e.button === 0)
105361
+ this._isLooking = false;
105362
+ };
105363
+ this._onPointerUp = (e) => {
105364
+ if (e.button === 0) {
105365
+ this._isLooking = false;
105366
+ }
105367
+ };
105368
+ document.addEventListener('mouseup', this._onMouseUp, true);
105369
+ document.addEventListener('pointerup', this._onPointerUp, true);
105370
+ }
105371
+ _unbindInputListeners() {
105372
+ document.removeEventListener('keydown', this._onKeyDown, true);
105373
+ document.removeEventListener('keyup', this._onKeyUp, true);
105374
+ document.removeEventListener('mousemove', this._onMouseMove);
105375
+ document.removeEventListener('pointermove', this._onPointerMove, true);
105376
+ document.removeEventListener('mouseup', this._onMouseUp, true);
105377
+ document.removeEventListener('pointerup', this._onPointerUp, true);
105378
+ const canvas = this.app?.graphicsDevice?.canvas;
105379
+ if (canvas && this._onMouseDown) {
105380
+ canvas.removeEventListener('mousedown', this._onMouseDown, true);
105381
+ }
105382
+ if (canvas && this._onPointerDown) {
105383
+ canvas.removeEventListener('pointerdown', this._onPointerDown, true);
105384
+ }
105385
+ }
105386
+ /**
105387
+ * Sync position and rotation from the camera entity.
105388
+ * Called when switching from orbit to fly mode to preserve camera state.
105389
+ */
105390
+ syncFromEntity() {
105391
+ try {
105392
+ // Preserve position
105393
+ const pos = this.entity?.getPosition?.() || this.entity?.getLocalPosition?.();
105394
+ if (pos) {
105395
+ const posVec = pos.clone ? pos.clone() : new Vec3(pos.x || 0, pos.y || 0, pos.z || 0);
105396
+ this.entity?.setPosition?.(posVec);
105397
+ this.entity?.setLocalPosition?.(posVec);
105398
+ }
105399
+ // Preserve rotation (convert Euler to pitch/yaw)
105400
+ const euler = this.entity?.getEulerAngles?.() || this.entity?.getLocalEulerAngles?.();
105401
+ if (euler) {
105402
+ // PlayCanvas Euler: x=pitch, y=yaw, z=roll
105403
+ this._pitch = euler.x || 0;
105404
+ this._yaw = euler.y || 0;
105405
+ // Apply rotation immediately so view doesn't snap
105406
+ if (this.entity?.setLocalEulerAngles) {
105407
+ this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
105408
+ }
105409
+ else if (this.entity?.setEulerAngles) {
105410
+ this.entity.setEulerAngles(this._pitch, this._yaw, 0);
105411
+ }
105412
+ }
105413
+ }
105414
+ catch {
105415
+ // ignore
105416
+ }
105417
+ }
105418
+ activate() {
105419
+ if (this._isActive)
105420
+ return;
105421
+ this._isActive = true;
105422
+ // Sync position and rotation from current camera state before activating
105423
+ this.syncFromEntity();
105424
+ if (!this._updateHandler) {
105425
+ this._updateHandler = (dt) => this.update(dt);
105426
+ this.app?.on?.('update', this._updateHandler);
105427
+ }
105428
+ }
105429
+ deactivate() {
105430
+ if (!this._isActive)
105431
+ return;
105432
+ this._isActive = false;
105433
+ this._isLooking = false;
105434
+ this._pressed.clear();
105435
+ if (this._updateHandler) {
105436
+ try {
105437
+ this.app?.off?.('update', this._updateHandler);
105438
+ }
105439
+ catch {
105440
+ // ignore
105441
+ }
105442
+ this._updateHandler = null;
105443
+ }
105444
+ }
105445
+ destroy() {
105446
+ try {
105447
+ this.deactivate();
105448
+ }
105449
+ finally {
105450
+ this._unbindInputListeners();
105451
+ }
105452
+ }
105453
+ setConfig(config) {
105454
+ if (config.moveSpeed !== undefined)
105455
+ this.moveSpeed = config.moveSpeed;
105456
+ if (config.fastSpeedMultiplier !== undefined)
105457
+ this.fastSpeedMultiplier = config.fastSpeedMultiplier;
105458
+ if (config.slowSpeedMultiplier !== undefined)
105459
+ this.slowSpeedMultiplier = config.slowSpeedMultiplier;
105460
+ if (config.lookSensitivity !== undefined)
105461
+ this.lookSensitivity = config.lookSensitivity;
105462
+ if (config.invertY !== undefined)
105463
+ this.invertY = config.invertY;
105464
+ if (config.keyBindings !== undefined)
105465
+ this.keyBindings = config.keyBindings;
105466
+ if (config.smoothing !== undefined)
105467
+ this.smoothing = config.smoothing;
105468
+ if (config.friction !== undefined)
105469
+ this.friction = config.friction;
105470
+ if (config.enableCollision !== undefined)
105471
+ this.enableCollision = config.enableCollision;
105472
+ if (config.minHeight !== undefined)
105473
+ this.minHeight = config.minHeight;
105474
+ if (config.maxHeight !== undefined)
105475
+ this.maxHeight = config.maxHeight;
105476
+ }
105477
+ getState() {
105478
+ const pos = this.entity?.getPosition?.();
105479
+ return {
105480
+ position: { x: pos?.x ?? 0, y: pos?.y ?? 0, z: pos?.z ?? 0 },
105481
+ rotation: { pitch: this._pitch, yaw: this._yaw },
105482
+ velocity: { x: this._velocity.x, y: this._velocity.y, z: this._velocity.z },
105483
+ isMoving: Math.abs(this._velocity.x) +
105484
+ Math.abs(this._velocity.y) +
105485
+ Math.abs(this._velocity.z) >
105486
+ 1e-4,
105487
+ };
105488
+ }
105489
+ _handleKeyDown(e) {
105490
+ const keys = [];
105491
+ if (e.code)
105492
+ keys.push(e.code);
105493
+ if (e.key) {
105494
+ keys.push(e.key);
105495
+ if (e.key.length === 1) {
105496
+ keys.push(`Key${e.key.toUpperCase()}`);
105497
+ keys.push(e.key.toUpperCase());
105498
+ keys.push(e.key.toLowerCase());
105499
+ }
105500
+ }
105501
+ for (const k of keys)
105502
+ this._pressed.add(k);
105503
+ }
105504
+ _handleKeyUp(e) {
105505
+ const keys = [];
105506
+ if (e.code)
105507
+ keys.push(e.code);
105508
+ if (e.key) {
105509
+ keys.push(e.key);
105510
+ if (e.key.length === 1) {
105511
+ keys.push(`Key${e.key.toUpperCase()}`);
105512
+ keys.push(e.key.toUpperCase());
105513
+ keys.push(e.key.toLowerCase());
105514
+ }
105515
+ }
105516
+ for (const k of keys)
105517
+ this._pressed.delete(k);
105518
+ }
105519
+ _handleMouseMove(e) {
105520
+ if (!this._isLooking || !this._isActive)
105521
+ return;
105522
+ const dx = e.movementX * this.lookSensitivity;
105523
+ const dy = e.movementY * this.lookSensitivity * (this.invertY ? 1 : -1);
105524
+ this._yaw = (this._yaw - dx) % 360;
105525
+ this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105526
+ }
105527
+ _handlePointerMove(e) {
105528
+ if (!this._isLooking || !this._isActive)
105529
+ return;
105530
+ const dx = (e.movementX || 0) * this.lookSensitivity;
105531
+ const dy = (e.movementY || 0) * this.lookSensitivity * (this.invertY ? 1 : -1);
105532
+ this._yaw = (this._yaw - dx) % 360;
105533
+ this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105534
+ }
105535
+ _getEffectiveSpeed() {
105536
+ const fast = this._pressed.has(this.keyBindings.fastMove);
105537
+ const slow = this._pressed.has(this.keyBindings.slowMove);
105538
+ let s = this.moveSpeed;
105539
+ if (fast)
105540
+ s *= this.fastSpeedMultiplier;
105541
+ if (slow)
105542
+ s *= this.slowSpeedMultiplier;
105543
+ return s;
105544
+ }
105545
+ _updateVelocity() {
105546
+ const kb = this.keyBindings;
105547
+ const isPressed = (binding, fallbacks) => {
105548
+ const all = [binding, ...fallbacks].filter(Boolean);
105549
+ return all.some(k => this._pressed.has(k));
105550
+ };
105551
+ const forward = isPressed(kb.forward, ['KeyW', 'w', 'W']) ? 1 : 0;
105552
+ const backward = isPressed(kb.backward, ['KeyS', 's', 'S']) ? 1 : 0;
105553
+ const left = isPressed(kb.left, ['KeyA', 'a', 'A']) ? 1 : 0;
105554
+ const right = isPressed(kb.right, ['KeyD', 'd', 'D']) ? 1 : 0;
105555
+ const up = isPressed(kb.up, ['KeyE', 'e', 'E']) ? 1 : 0;
105556
+ const down = isPressed(kb.down, ['KeyQ', 'q', 'Q']) ? 1 : 0;
105557
+ const inputZ = forward - backward;
105558
+ const inputX = right - left;
105559
+ const inputY = up - down;
105560
+ const planarLen = Math.hypot(inputX, inputZ);
105561
+ const nx = planarLen > 0 ? inputX / planarLen : 0;
105562
+ const nz = planarLen > 0 ? inputZ / planarLen : 0;
105563
+ const speed = this._getEffectiveSpeed() * 2;
105564
+ const entity = this.entity;
105565
+ const fwd = entity?.forward && entity.forward.clone
105566
+ ? entity.forward.clone()
105567
+ : entity?.forward
105568
+ ? new Vec3(entity.forward.x, entity.forward.y, entity.forward.z)
105569
+ : new Vec3(0, 0, -1);
105570
+ const rightVec = entity?.right && entity.right.clone
105571
+ ? entity.right.clone()
105572
+ : entity?.right
105573
+ ? new Vec3(entity.right.x, entity.right.y, entity.right.z)
105574
+ : new Vec3(1, 0, 0);
105575
+ const upVec = entity?.up && entity.up.clone
105576
+ ? entity.up.clone()
105577
+ : entity?.up
105578
+ ? new Vec3(entity.up.x, entity.up.y, entity.up.z)
105579
+ : Vec3.UP.clone();
105580
+ const target = new Vec3(0, 0, 0);
105581
+ target.add(fwd.mulScalar(nz * speed));
105582
+ target.add(rightVec.mulScalar(nx * speed));
105583
+ target.add(upVec.mulScalar(inputY * speed));
105584
+ this._targetVelocity.copy(target);
105585
+ this._velocity.lerp(this._velocity, this._targetVelocity, Math.min(1, this.smoothing));
105586
+ if (nx === 0 && nz === 0 && inputY === 0) {
105587
+ this._velocity.mulScalar(this.friction);
105588
+ if (this._velocity.length() < 0.0001)
105589
+ this._velocity.set(0, 0, 0);
105590
+ }
105591
+ }
105592
+ _applyMovement(dt) {
105593
+ if (this._velocity.length() === 0)
105594
+ return;
105595
+ const pos = this.entity?.getPosition?.()?.clone?.() ?? new Vec3(0, 0, 0);
105596
+ pos.add(this._velocity.clone().mulScalar(dt));
105597
+ this.entity?.setPosition?.(pos);
105598
+ }
105599
+ _applyRotation() {
105600
+ if (this.entity?.setLocalEulerAngles) {
105601
+ this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
105602
+ }
105603
+ else if (this.entity?.setEulerAngles) {
105604
+ this.entity.setEulerAngles(this._pitch, this._yaw, 0);
105605
+ }
105606
+ }
105607
+ _applyConstraints() {
105608
+ if (!this.enableCollision &&
105609
+ this.minHeight == null &&
105610
+ this.maxHeight == null) {
105611
+ return;
105612
+ }
105613
+ const pos = this.entity?.getPosition?.()?.clone?.() ?? new Vec3(0, 0, 0);
105614
+ if (this.minHeight != null)
105615
+ pos.y = Math.max(pos.y, this.minHeight);
105616
+ if (this.maxHeight != null)
105617
+ pos.y = Math.min(pos.y, this.maxHeight);
105618
+ this.entity?.setPosition?.(pos);
105619
+ }
105620
+ update(dt) {
105621
+ if (!this._isActive)
105622
+ return;
105623
+ this._updateVelocity();
105624
+ this._applyMovement(dt);
105625
+ this._applyRotation();
105626
+ this._applyConstraints();
105627
+ // Emit throttled movement/look events (100ms)
105628
+ const now = performance.now();
105629
+ if (!this._lastMoveEmitTime || now - this._lastMoveEmitTime >= 100) {
105630
+ const pos = this.entity?.getPosition?.();
105631
+ this.emitFlyEvent?.('fly-camera-move', {
105632
+ position: { x: pos?.x ?? 0, y: pos?.y ?? 0, z: pos?.z ?? 0 },
105633
+ velocity: { x: this._velocity.x, y: this._velocity.y, z: this._velocity.z },
105634
+ });
105635
+ this._lastMoveEmitTime = now;
105636
+ }
105637
+ if (!this._lastLookEmitTime || now - this._lastLookEmitTime >= 100) {
105638
+ this.emitFlyEvent?.('fly-camera-look', { pitch: this._pitch, yaw: this._yaw });
105639
+ this._lastLookEmitTime = now;
105640
+ }
105641
+ }
105642
+ }
105643
+
105265
105644
  // FlyCamera PlayCanvas script: first-person WASD movement with mouse-look
105266
105645
  function registerFlyCameraScript() {
105267
105646
  if (typeof pc === 'undefined') {
@@ -105347,10 +105726,18 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105347
105726
  this._onKeyUp = this._handleKeyUp.bind(this);
105348
105727
  document.addEventListener('keydown', this._onKeyDown, true);
105349
105728
  document.addEventListener('keyup', this._onKeyUp, true);
105350
- // Mouse move for look (while mouse button held)
105729
+ // Mouse/pointer move for look (while primary button held).
105730
+ //
105731
+ // Important: SuperSplat camera controls are pointer-event based and may call
105732
+ // preventDefault() on pointer events. When that happens, browsers often
105733
+ // suppress the corresponding legacy mouse events (mousedown/mousemove).
105734
+ // So we listen to *pointer* events as the primary path, with mouse as a
105735
+ // fallback for older environments.
105351
105736
  this._onMouseMove = this._handleMouseMove.bind(this);
105737
+ this._onPointerMove = this._handlePointerMove.bind(this);
105352
105738
  document.addEventListener('mousemove', this._onMouseMove);
105353
- // Mouse button handling: click + hold to look, release to stop
105739
+ document.addEventListener('pointermove', this._onPointerMove, true);
105740
+ // Button handling: click + hold to look, release to stop
105354
105741
  const canvas = this.app.graphicsDevice.canvas;
105355
105742
  this._onClickToLock = (e) => {
105356
105743
  // Left button enables look while held (no pointer lock)
@@ -105358,13 +105745,17 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105358
105745
  this._isLooking = true;
105359
105746
  }
105360
105747
  };
105361
- canvas.addEventListener('mousedown', this._onClickToLock);
105748
+ canvas.addEventListener('mousedown', this._onClickToLock, true);
105749
+ this._onPointerDownToLook = this._handlePointerDown.bind(this);
105750
+ canvas.addEventListener('pointerdown', this._onPointerDownToLook, true);
105362
105751
  this._onMouseUp = (e) => {
105363
105752
  if (e.button === 0) {
105364
105753
  this._isLooking = false;
105365
105754
  }
105366
105755
  };
105367
- document.addEventListener('mouseup', this._onMouseUp);
105756
+ document.addEventListener('mouseup', this._onMouseUp, true);
105757
+ this._onPointerUp = this._handlePointerUp.bind(this);
105758
+ document.addEventListener('pointerup', this._onPointerUp, true);
105368
105759
  };
105369
105760
  FlyCamera.prototype.update = function (dt) {
105370
105761
  if (!this._isActive)
@@ -105460,9 +105851,11 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105460
105851
  this._onKeyDown = this._onKeyDown || this._handleKeyDown.bind(this);
105461
105852
  this._onKeyUp = this._onKeyUp || this._handleKeyUp.bind(this);
105462
105853
  this._onMouseMove = this._onMouseMove || this._handleMouseMove.bind(this);
105854
+ this._onPointerMove = this._onPointerMove || this._handlePointerMove.bind(this);
105463
105855
  document.addEventListener('keydown', this._onKeyDown, true);
105464
105856
  document.addEventListener('keyup', this._onKeyUp, true);
105465
105857
  document.addEventListener('mousemove', this._onMouseMove);
105858
+ document.addEventListener('pointermove', this._onPointerMove, true);
105466
105859
  const canvas = this.app.graphicsDevice.canvas;
105467
105860
  this._onClickToLock =
105468
105861
  this._onClickToLock ||
@@ -105471,7 +105864,10 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105471
105864
  this._isLooking = true;
105472
105865
  }
105473
105866
  });
105474
- canvas.addEventListener('mousedown', this._onClickToLock);
105867
+ canvas.addEventListener('mousedown', this._onClickToLock, true);
105868
+ this._onPointerDownToLook =
105869
+ this._onPointerDownToLook || this._handlePointerDown.bind(this);
105870
+ canvas.addEventListener('pointerdown', this._onPointerDownToLook, true);
105475
105871
  this._onMouseUp =
105476
105872
  this._onMouseUp ||
105477
105873
  ((e) => {
@@ -105479,22 +105875,36 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105479
105875
  this._isLooking = false;
105480
105876
  }
105481
105877
  });
105482
- document.addEventListener('mouseup', this._onMouseUp);
105878
+ document.addEventListener('mouseup', this._onMouseUp, true);
105879
+ this._onPointerUp = this._onPointerUp || this._handlePointerUp.bind(this);
105880
+ document.addEventListener('pointerup', this._onPointerUp, true);
105483
105881
  };
105484
105882
  FlyCamera.prototype.deactivate = function () {
105485
105883
  if (!this._isActive)
105486
105884
  return;
105487
105885
  this._isActive = false;
105886
+ this._isLooking = false;
105887
+ try {
105888
+ this._pressed?.clear?.();
105889
+ }
105890
+ catch {
105891
+ // ignore
105892
+ }
105488
105893
  // Exit pointer lock when deactivating
105489
105894
  this._exitPointerLock();
105490
105895
  // Remove listeners
105491
105896
  document.removeEventListener('keydown', this._onKeyDown, true);
105492
105897
  document.removeEventListener('keyup', this._onKeyUp, true);
105493
105898
  document.removeEventListener('mousemove', this._onMouseMove);
105494
- document.removeEventListener('mouseup', this._onMouseUp);
105899
+ document.removeEventListener('pointermove', this._onPointerMove, true);
105900
+ document.removeEventListener('mouseup', this._onMouseUp, true);
105901
+ document.removeEventListener('pointerup', this._onPointerUp, true);
105495
105902
  const canvas = this.app?.graphicsDevice?.canvas;
105496
105903
  if (canvas && this._onClickToLock) {
105497
- canvas.removeEventListener('mousedown', this._onClickToLock);
105904
+ canvas.removeEventListener('mousedown', this._onClickToLock, true);
105905
+ }
105906
+ if (canvas && this._onPointerDownToLook) {
105907
+ canvas.removeEventListener('pointerdown', this._onPointerDownToLook, true);
105498
105908
  }
105499
105909
  };
105500
105910
  FlyCamera.prototype.setConfig = function (config) {
@@ -105583,6 +105993,27 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105583
105993
  this._yaw = (this._yaw - dx) % 360;
105584
105994
  this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105585
105995
  };
105996
+ FlyCamera.prototype._handlePointerMove = function (e) {
105997
+ if (!this._isLooking || !this._isActive)
105998
+ return;
105999
+ const dx = (e.movementX || 0) * this.lookSensitivity;
106000
+ const dy = (e.movementY || 0) * this.lookSensitivity * (this.invertY ? 1 : -1);
106001
+ this._yaw = (this._yaw - dx) % 360;
106002
+ this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
106003
+ };
106004
+ FlyCamera.prototype._handlePointerDown = function (e) {
106005
+ if (!this._isActive)
106006
+ return;
106007
+ // Primary button enables look while held (no pointer lock)
106008
+ if (e.button === 0) {
106009
+ this._isLooking = true;
106010
+ }
106011
+ };
106012
+ FlyCamera.prototype._handlePointerUp = function (e) {
106013
+ if (e.button === 0) {
106014
+ this._isLooking = false;
106015
+ }
106016
+ };
105586
106017
  FlyCamera.prototype._handlePointerLockChange = function () {
105587
106018
  const canvas = this.app.graphicsDevice.canvas;
105588
106019
  this._isPointerLocked = document.pointerLockElement === canvas;
@@ -105879,7 +106310,7 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105879
106310
  scope.resolve(key).setValue(values[key]);
105880
106311
  }
105881
106312
  };
105882
- const createBlueNoiseTexture = (device) => {
106313
+ const createBlueNoiseTexture$1 = (device) => {
105883
106314
  const size = 32;
105884
106315
  const texture = new Texture$1(device, {
105885
106316
  width: size,
@@ -105958,7 +106389,7 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105958
106389
  throw new Error('InfiniteGrid: QuadRender is not available in this PlayCanvas version.');
105959
106390
  }
105960
106391
  this.quadRender = new QuadRender$1(this.shader);
105961
- this.blueNoiseTexture = createBlueNoiseTexture(device);
106392
+ this.blueNoiseTexture = createBlueNoiseTexture$1(device);
105962
106393
  this._createBlendState();
105963
106394
  this._registerRenderHook();
105964
106395
  }
@@ -140324,51 +140755,161 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
140324
140755
  return fallback.clone();
140325
140756
  }
140326
140757
 
140327
- const THICK_WIREFRAME_LINE_WIDTH_UV$1 = 0.06;
140328
- const THICK_WIREFRAME_OPACITY$1 = 1.0;
140329
- const THICK_WIREFRAME_BOX_VS = /* glsl */ `
140330
- attribute vec3 vertex_position;
140331
- attribute vec2 vertex_texCoord0;
140758
+ const cache = new WeakMap();
140759
+ function createBlueNoiseTexture(device) {
140760
+ const size = 32;
140761
+ const texture = new Texture$1(device, {
140762
+ width: size,
140763
+ height: size,
140764
+ format: PIXELFORMAT_R8_G8_B8_A8,
140765
+ mipmaps: false,
140766
+ });
140767
+ texture.addressU = ADDRESS_REPEAT;
140768
+ texture.addressV = ADDRESS_REPEAT;
140769
+ texture.minFilter = FILTER_NEAREST;
140770
+ texture.magFilter = FILTER_NEAREST;
140771
+ const pixels = texture.lock();
140772
+ const seed = 1337;
140773
+ let value = seed;
140774
+ const random = () => {
140775
+ value ^= value << 13;
140776
+ value ^= value >>> 17;
140777
+ value ^= value << 5;
140778
+ return ((value >>> 0) % 256) / 255;
140779
+ };
140780
+ for (let i = 0; i < size * size; i++) {
140781
+ const noise = Math.floor(random() * 255);
140782
+ const idx = i * 4;
140783
+ pixels[idx + 0] = noise;
140784
+ pixels[idx + 1] = noise;
140785
+ pixels[idx + 2] = noise;
140786
+ pixels[idx + 3] = 255;
140787
+ }
140788
+ texture.unlock();
140789
+ texture.name = 'supersplat-blue-noise';
140790
+ return texture;
140791
+ }
140792
+ function getBlueNoiseTex32(device) {
140793
+ const existing = cache.get(device);
140794
+ if (existing)
140795
+ return existing;
140796
+ const tex = createBlueNoiseTexture(device);
140797
+ cache.set(device, tex);
140798
+ return tex;
140799
+ }
140332
140800
 
140333
- uniform mat4 matrix_model;
140334
- uniform mat4 matrix_viewProjection;
140801
+ // SuperSplat-like selection box shader: screen-space ray/box intersection that draws a thick
140802
+ // white wire/grid pattern (not dependent on WebGL line width).
140803
+ const BOX_SELECT_VS = /* glsl */ `
140804
+ attribute vec3 vertex_position;
140335
140805
 
140336
- varying vec2 vUv;
140806
+ uniform mat4 matrix_model;
140807
+ uniform mat4 matrix_viewProjection;
140337
140808
 
140338
- void main(void) {
140339
- vUv = vertex_texCoord0;
140340
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140341
- }
140809
+ void main() {
140810
+ gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140811
+ }
140342
140812
  `;
140343
- const THICK_WIREFRAME_BOX_FS = /* glsl */ `
140344
- #ifdef GL_OES_standard_derivatives
140345
- #extension GL_OES_standard_derivatives : enable
140346
- #endif
140813
+ const BOX_SELECT_FS = /* glsl */ `
140814
+ // ray-box intersection in box space
140815
+ bool intersectBox(out float t0, out float t1, out int axis0, out int axis1, vec3 pos, vec3 dir, vec3 boxCen, vec3 boxLen)
140816
+ {
140817
+ bvec3 validDir = notEqual(dir, vec3(0.0));
140818
+ vec3 absDir = abs(dir);
140819
+ vec3 signDir = sign(dir);
140820
+ vec3 m = vec3(
140821
+ validDir.x ? 1.0 / absDir.x : 0.0,
140822
+ validDir.y ? 1.0 / absDir.y : 0.0,
140823
+ validDir.z ? 1.0 / absDir.z : 0.0
140824
+ ) * signDir;
140347
140825
 
140348
- precision mediump float;
140826
+ vec3 n = m * (pos - boxCen);
140827
+ vec3 k = abs(m) * boxLen;
140349
140828
 
140350
- uniform vec3 lineColor;
140351
- uniform float lineWidth;
140352
- uniform float opacity;
140829
+ vec3 v0 = -n - k;
140830
+ vec3 v1 = -n + k;
140353
140831
 
140354
- varying vec2 vUv;
140832
+ // replace invalid axes with -inf and +inf so the tests below ignore them
140833
+ v0 = mix(vec3(-1.0 / 0.0000001), v0, validDir);
140834
+ v1 = mix(vec3(1.0 / 0.0000001), v1, validDir);
140355
140835
 
140356
- void main(void) {
140357
- // Distance to nearest face border in UV space (0 at border, 0.5 at center).
140358
- float d = min(min(vUv.x, 1.0 - vUv.x), min(vUv.y, 1.0 - vUv.y));
140836
+ axis0 = (v0.x > v0.y) ? ((v0.x > v0.z) ? 0 : 2) : ((v0.y > v0.z) ? 1 : 2);
140837
+ axis1 = (v1.x < v1.y) ? ((v1.x < v1.z) ? 0 : 2) : ((v1.y < v1.z) ? 1 : 2);
140359
140838
 
140360
- // Anti-aliased edge mask (1 near border, 0 in face interior).
140361
- float aa = 0.002;
140362
- #ifdef GL_OES_standard_derivatives
140363
- aa = max(aa, fwidth(d) * 1.5);
140364
- #endif
140365
- float a = 1.0 - smoothstep(lineWidth, lineWidth + aa, d);
140366
- a *= opacity;
140839
+ t0 = v0[axis0];
140840
+ t1 = v1[axis1];
140367
140841
 
140368
- if (a <= 0.001) discard;
140369
- gl_FragColor = vec4(lineColor, a);
140370
- }
140842
+ if (t0 > t1 || t1 < 0.0) {
140843
+ return false;
140844
+ }
140845
+
140846
+ return true;
140847
+ }
140848
+
140849
+ float calcDepth(in vec3 pos, in mat4 viewProjection) {
140850
+ vec4 v = viewProjection * vec4(pos, 1.0);
140851
+ return (v.z / v.w) * 0.5 + 0.5;
140852
+ }
140853
+
140854
+ uniform sampler2D blueNoiseTex32;
140855
+ uniform mat4 matrix_viewProjection;
140856
+ uniform vec3 boxCen;
140857
+ uniform vec3 boxLen;
140858
+
140859
+ uniform vec3 near_origin;
140860
+ uniform vec3 near_x;
140861
+ uniform vec3 near_y;
140862
+
140863
+ uniform vec3 far_origin;
140864
+ uniform vec3 far_x;
140865
+ uniform vec3 far_y;
140866
+
140867
+ uniform vec2 targetSize;
140868
+ uniform vec3 lineColor;
140869
+
140870
+ bool writeDepth(float alpha) {
140871
+ ivec2 uv = ivec2(gl_FragCoord.xy);
140872
+ ivec2 size = textureSize(blueNoiseTex32, 0);
140873
+ return alpha > texelFetch(blueNoiseTex32, uv % size, 0).y;
140874
+ }
140875
+
140876
+ bool strips(vec3 pos, int axis) {
140877
+ // Thickness tuned to match SuperSplat viewer "thick wire"
140878
+ bvec3 b = lessThan(fract(pos * 2.0 + vec3(0.015)), vec3(0.06));
140879
+ b[axis] = false;
140880
+ return any(b);
140881
+ }
140882
+
140883
+ void main() {
140884
+ vec2 clip = gl_FragCoord.xy / targetSize;
140885
+ vec3 worldNear = near_origin + near_x * clip.x + near_y * clip.y;
140886
+ vec3 worldFar = far_origin + far_x * clip.x + far_y * clip.y;
140887
+ vec3 rayDir = normalize(worldFar - worldNear);
140888
+
140889
+ float t0, t1;
140890
+ int axis0, axis1;
140891
+ if (!intersectBox(t0, t1, axis0, axis1, worldNear, rayDir, boxCen, boxLen)) {
140892
+ discard;
140893
+ }
140894
+
140895
+ vec3 frontPos = worldNear + rayDir * t0;
140896
+ bool front = t0 > 0.0 && strips(frontPos - boxCen, axis0);
140897
+
140898
+ vec3 backPos = worldNear + rayDir * t1;
140899
+ bool back = strips(backPos - boxCen, axis1);
140900
+
140901
+ if (front) {
140902
+ gl_FragColor = vec4(lineColor, 0.6);
140903
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(frontPos, matrix_viewProjection) : 1.0;
140904
+ } else if (back) {
140905
+ gl_FragColor = vec4(lineColor, 0.6);
140906
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(backPos, matrix_viewProjection) : 1.0;
140907
+ } else {
140908
+ discard;
140909
+ }
140910
+ }
140371
140911
  `;
140912
+ const tmpPos$1 = new Vec3();
140372
140913
  class BoxSelectionAPI {
140373
140914
  constructor() {
140374
140915
  this.boxes = new Map();
@@ -140405,6 +140946,7 @@ void main(void) {
140405
140946
  this.ensureTranslateGizmo();
140406
140947
  this.updateGizmoSize();
140407
140948
  this.updateGizmoAttachment();
140949
+ this.updateBoxShaderUniforms();
140408
140950
  }
140409
140951
  createBox(options = {}) {
140410
140952
  if (!this.app || !this.parent) {
@@ -140427,7 +140969,7 @@ void main(void) {
140427
140969
  entity.render.castShadows = false;
140428
140970
  entity.render.receiveShadows = false;
140429
140971
  entity.render.enabled = visible;
140430
- // Render as a thick wireframe (shader-based) so line width is consistent across browsers.
140972
+ // Render as a thick wireframe (shader-based) to match SuperSplat viewer.
140431
140973
  this.parent.addChild(entity);
140432
140974
  const record = {
140433
140975
  id,
@@ -140514,8 +141056,8 @@ void main(void) {
140514
141056
  return false;
140515
141057
  }
140516
141058
  record.color.set(r, g, b);
140517
- const mat = this.buildWireframeMaterial(record.color);
140518
- record.entity.render.material = mat;
141059
+ const mat = record.entity.render?.material;
141060
+ mat?.setParameter?.('lineColor', [record.color.x, record.color.y, record.color.z]);
140519
141061
  this.requestRender();
140520
141062
  return true;
140521
141063
  }
@@ -140710,19 +141252,34 @@ void main(void) {
140710
141252
  }
140711
141253
  buildWireframeMaterial(color) {
140712
141254
  const material = new ShaderMaterial$1({
140713
- uniqueName: 'boxSelectionThickWireframe',
140714
- vertexGLSL: THICK_WIREFRAME_BOX_VS,
140715
- fragmentGLSL: THICK_WIREFRAME_BOX_FS,
141255
+ uniqueName: 'boxSelectionSupersplatWire',
141256
+ vertexGLSL: BOX_SELECT_VS,
141257
+ fragmentGLSL: BOX_SELECT_FS,
140716
141258
  });
140717
- material.cull = CULLFACE_NONE;
140718
- material.depthWrite = false;
141259
+ material.cull = CULLFACE_FRONT;
140719
141260
  material.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE_MINUS_SRC_ALPHA);
140720
- material.setParameter('lineColor', [color.x, color.y, color.z]);
140721
- material.setParameter('lineWidth', THICK_WIREFRAME_LINE_WIDTH_UV$1);
140722
- material.setParameter('opacity', THICK_WIREFRAME_OPACITY$1);
140723
141261
  material.update();
141262
+ material.setParameter('lineColor', [color.x, color.y, color.z]);
140724
141263
  return material;
140725
141264
  }
141265
+ updateBoxShaderUniforms() {
141266
+ if (!this.app)
141267
+ return;
141268
+ const device = this.app.graphicsDevice;
141269
+ // Ensure required global uniforms for selection shaders.
141270
+ device.scope.resolve('targetSize').setValue([device.width, device.height]);
141271
+ device.scope.resolve('blueNoiseTex32').setValue(getBlueNoiseTex32(device));
141272
+ for (const record of this.boxes.values()) {
141273
+ const mat = record.entity.render?.material;
141274
+ if (!mat?.setParameter)
141275
+ continue;
141276
+ // World-space center
141277
+ record.entity.getWorldTransform().getTranslation(tmpPos$1);
141278
+ mat.setParameter('boxCen', [tmpPos$1.x, tmpPos$1.y, tmpPos$1.z]);
141279
+ // Half extents
141280
+ mat.setParameter('boxLen', [record.lenX * 0.5, record.lenY * 0.5, record.lenZ * 0.5]);
141281
+ }
141282
+ }
140726
141283
  emitEvent(event, detail) {
140727
141284
  if (this.onEvent) {
140728
141285
  this.onEvent(event, detail);
@@ -140734,59 +141291,110 @@ void main(void) {
140734
141291
  }
140735
141292
  }
140736
141293
 
140737
- const THICK_WIREFRAME_LINE_WIDTH_UV = 0.035;
140738
- const THICK_WIREFRAME_OPACITY = 1.0;
140739
- const SPHERE_GRID_U = 14.0;
140740
- const SPHERE_GRID_V = 10.0;
140741
- const THICK_WIREFRAME_SPHERE_VS = /* glsl */ `
140742
- attribute vec3 vertex_position;
140743
- attribute vec2 vertex_texCoord0;
140744
-
140745
- uniform mat4 matrix_model;
140746
- uniform mat4 matrix_viewProjection;
141294
+ // SuperSplat-like selection sphere shader: screen-space ray/sphere intersection that draws a thick
141295
+ // white wire/grid pattern (not dependent on WebGL line width).
141296
+ const SPHERE_SELECT_VS = /* glsl */ `
141297
+ attribute vec3 vertex_position;
140747
141298
 
140748
- varying vec2 vUv;
141299
+ uniform mat4 matrix_model;
141300
+ uniform mat4 matrix_viewProjection;
140749
141301
 
140750
- void main(void) {
140751
- vUv = vertex_texCoord0;
140752
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140753
- }
141302
+ void main() {
141303
+ gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
141304
+ }
140754
141305
  `;
140755
- const THICK_WIREFRAME_SPHERE_FS = /* glsl */ `
140756
- #ifdef GL_OES_standard_derivatives
140757
- #extension GL_OES_standard_derivatives : enable
140758
- #endif
141306
+ const SPHERE_SELECT_FS = /* glsl */ `
141307
+ bool intersectSphere(out float t0, out float t1, vec3 pos, vec3 dir, vec4 sphere) {
141308
+ vec3 L = sphere.xyz - pos;
141309
+ float tca = dot(L, dir);
141310
+
141311
+ float d2 = sphere.w * sphere.w - (dot(L, L) - tca * tca);
141312
+ if (d2 <= 0.0) {
141313
+ return false;
141314
+ }
141315
+
141316
+ float thc = sqrt(d2);
141317
+ t0 = tca - thc;
141318
+ t1 = tca + thc;
141319
+ if (t1 <= 0.0) {
141320
+ return false;
141321
+ }
140759
141322
 
140760
- precision mediump float;
141323
+ return true;
141324
+ }
140761
141325
 
140762
- uniform vec3 lineColor;
140763
- uniform float lineWidth;
140764
- uniform float opacity;
141326
+ float calcDepth(in vec3 pos, in mat4 viewProjection) {
141327
+ vec4 v = viewProjection * vec4(pos, 1.0);
141328
+ return (v.z / v.w) * 0.5 + 0.5;
141329
+ }
140765
141330
 
140766
- varying vec2 vUv;
141331
+ vec2 calcAzimuthElev(in vec3 dir) {
141332
+ float azimuth = atan(dir.z, dir.x);
141333
+ float elev = asin(dir.y);
141334
+ return vec2(azimuth, elev) * 180.0 / 3.14159;
141335
+ }
140767
141336
 
140768
- float gridLineDist(float v, float freq) {
140769
- float f = fract(v * freq);
140770
- return min(f, 1.0 - f);
140771
- }
141337
+ uniform sampler2D blueNoiseTex32;
141338
+ uniform mat4 matrix_viewProjection;
141339
+ uniform vec4 sphere;
141340
+ uniform vec3 lineColor;
140772
141341
 
140773
- void main(void) {
140774
- // "Wireframe-like" lat/long grid in UV space.
140775
- float du = gridLineDist(vUv.x, ${SPHERE_GRID_U});
140776
- float dv = gridLineDist(vUv.y, ${SPHERE_GRID_V});
140777
- float d = min(du, dv);
140778
-
140779
- float aa = 0.002;
140780
- #ifdef GL_OES_standard_derivatives
140781
- aa = max(aa, fwidth(d) * 1.5);
140782
- #endif
140783
- float a = 1.0 - smoothstep(lineWidth, lineWidth + aa, d);
140784
- a *= opacity;
141342
+ uniform vec3 near_origin;
141343
+ uniform vec3 near_x;
141344
+ uniform vec3 near_y;
140785
141345
 
140786
- if (a <= 0.001) discard;
140787
- gl_FragColor = vec4(lineColor, a);
140788
- }
141346
+ uniform vec3 far_origin;
141347
+ uniform vec3 far_x;
141348
+ uniform vec3 far_y;
141349
+
141350
+ uniform vec2 targetSize;
141351
+
141352
+ bool writeDepth(float alpha) {
141353
+ vec2 uv = fract(gl_FragCoord.xy / 32.0);
141354
+ float noise = texture2DLod(blueNoiseTex32, uv, 0.0).y;
141355
+ return alpha > noise;
141356
+ }
141357
+
141358
+ bool strips(vec3 lp) {
141359
+ vec2 ae = calcAzimuthElev(normalize(lp));
141360
+
141361
+ float spacing = 180.0 / (2.0 * 3.14159 * sphere.w);
141362
+ // Thickness tuned to match SuperSplat viewer "thick wire"
141363
+ float size = 0.06;
141364
+ return fract(ae.x / spacing) < size ||
141365
+ fract(ae.y / spacing) < size;
141366
+ }
141367
+
141368
+ void main() {
141369
+ vec2 clip = gl_FragCoord.xy / targetSize;
141370
+ vec3 worldNear = near_origin + near_x * clip.x + near_y * clip.y;
141371
+ vec3 worldFar = far_origin + far_x * clip.x + far_y * clip.y;
141372
+
141373
+ vec3 rayDir = normalize(worldFar - worldNear);
141374
+
141375
+ float t0, t1;
141376
+ if (!intersectSphere(t0, t1, worldNear, rayDir, sphere)) {
141377
+ discard;
141378
+ }
141379
+
141380
+ vec3 frontPos = worldNear + rayDir * t0;
141381
+ bool front = t0 > 0.0 && strips(frontPos - sphere.xyz);
141382
+
141383
+ vec3 backPos = worldNear + rayDir * t1;
141384
+ bool back = strips(backPos - sphere.xyz);
141385
+
141386
+ if (front) {
141387
+ gl_FragColor = vec4(lineColor, 0.6);
141388
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(frontPos, matrix_viewProjection) : 1.0;
141389
+ } else if (back) {
141390
+ gl_FragColor = vec4(lineColor, 0.6);
141391
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(backPos, matrix_viewProjection) : 1.0;
141392
+ } else {
141393
+ discard;
141394
+ }
141395
+ }
140789
141396
  `;
141397
+ const tmpPos = new Vec3();
140790
141398
  class SphereSelectionAPI {
140791
141399
  constructor() {
140792
141400
  this.spheres = new Map();
@@ -140822,6 +141430,7 @@ void main(void) {
140822
141430
  this.ensureTranslateGizmo();
140823
141431
  this.updateGizmoSize();
140824
141432
  this.updateGizmoAttachment();
141433
+ this.updateSphereShaderUniforms();
140825
141434
  }
140826
141435
  createSphere(options = {}) {
140827
141436
  if (!this.app || !this.parent) {
@@ -140834,7 +141443,8 @@ void main(void) {
140834
141443
  const visible = options.visible ?? true;
140835
141444
  const entity = new Entity(id);
140836
141445
  entity.addComponent('render', {
140837
- type: 'sphere',
141446
+ // Use a box proxy like SuperSplat; shader ray-marches a true sphere.
141447
+ type: 'box',
140838
141448
  material: this.buildWireframeMaterial(color),
140839
141449
  });
140840
141450
  entity.setLocalScale(radius * 2, radius * 2, radius * 2); // sphere diameter = radius * 2
@@ -140842,7 +141452,7 @@ void main(void) {
140842
141452
  entity.render.castShadows = false;
140843
141453
  entity.render.receiveShadows = false;
140844
141454
  entity.render.enabled = visible;
140845
- // Render as a thick wireframe (shader-based) so line width is consistent across browsers.
141455
+ // Render as a thick wireframe (shader-based) to match SuperSplat viewer.
140846
141456
  this.parent.addChild(entity);
140847
141457
  const record = {
140848
141458
  id,
@@ -140925,8 +141535,12 @@ void main(void) {
140925
141535
  return false;
140926
141536
  }
140927
141537
  record.color.set(r, g, b);
140928
- const mat = this.buildWireframeMaterial(record.color);
140929
- record.entity.render.material = mat;
141538
+ const mat = record.entity.render?.material;
141539
+ mat?.setParameter?.('lineColor', [
141540
+ record.color.x,
141541
+ record.color.y,
141542
+ record.color.z,
141543
+ ]);
140930
141544
  this.requestRender();
140931
141545
  return true;
140932
141546
  }
@@ -141124,19 +141738,30 @@ void main(void) {
141124
141738
  }
141125
141739
  buildWireframeMaterial(color) {
141126
141740
  const material = new ShaderMaterial$1({
141127
- uniqueName: 'sphereSelectionThickWireframe',
141128
- vertexGLSL: THICK_WIREFRAME_SPHERE_VS,
141129
- fragmentGLSL: THICK_WIREFRAME_SPHERE_FS,
141741
+ uniqueName: 'sphereSelectionSupersplatWire',
141742
+ vertexGLSL: SPHERE_SELECT_VS,
141743
+ fragmentGLSL: SPHERE_SELECT_FS,
141130
141744
  });
141131
- material.cull = CULLFACE_NONE;
141132
- material.depthWrite = false;
141745
+ material.cull = CULLFACE_FRONT;
141133
141746
  material.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE_MINUS_SRC_ALPHA);
141134
- material.setParameter('lineColor', [color.x, color.y, color.z]);
141135
- material.setParameter('lineWidth', THICK_WIREFRAME_LINE_WIDTH_UV);
141136
- material.setParameter('opacity', THICK_WIREFRAME_OPACITY);
141137
141747
  material.update();
141748
+ material.setParameter('lineColor', [color.x, color.y, color.z]);
141138
141749
  return material;
141139
141750
  }
141751
+ updateSphereShaderUniforms() {
141752
+ if (!this.app)
141753
+ return;
141754
+ const device = this.app.graphicsDevice;
141755
+ device.scope.resolve('targetSize').setValue([device.width, device.height]);
141756
+ device.scope.resolve('blueNoiseTex32').setValue(getBlueNoiseTex32(device));
141757
+ for (const record of this.spheres.values()) {
141758
+ const mat = record.entity.render?.material;
141759
+ if (!mat?.setParameter)
141760
+ continue;
141761
+ record.entity.getWorldTransform().getTranslation(tmpPos);
141762
+ mat.setParameter('sphere', [tmpPos.x, tmpPos.y, tmpPos.z, record.radius]);
141763
+ }
141764
+ }
141140
141765
  emitEvent(event, detail) {
141141
141766
  if (this.onEvent) {
141142
141767
  this.onEvent(event, detail);
@@ -146763,7 +147388,7 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
146763
147388
  const { config } = this.scene;
146764
147389
  const state = this.viewerEventState;
146765
147390
  // Colors and view settings from scene config
146766
- const selectedClr = config.selectedClr ?? { r: 1, g: 1, b: 0, a: 1 };
147391
+ const selectedClr = config.selectedClr ?? { r: 1, g: 0.5, b: 0, a: 1 };
146767
147392
  const unselectedClr = config.unselectedClr ?? { r: 0, g: 0, b: 1, a: 0.5 };
146768
147393
  const lockedClr = config.lockedClr ?? { r: 0, g: 0, b: 0, a: 0.05 };
146769
147394
  const bgClr = config.bgClr ?? { r: 0, g: 0, b: 0, a: 1 };
@@ -147197,9 +147822,10 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
147197
147822
  this.app = ctx.app;
147198
147823
  this.entities.camera = ctx.camera;
147199
147824
  const cameraAny = this.entities.camera;
147200
- if (cameraAny &&
147201
- !cameraAny.script &&
147202
- typeof cameraAny.addComponent === 'function') {
147825
+ // SuperSplat's PCApp omits ScriptComponentSystem, so `addComponent('script')`
147826
+ // will not produce `camera.script.create()`. We keep the attempt (in case
147827
+ // the underlying app changes), but also support a controller-based fallback.
147828
+ if (cameraAny && !cameraAny.script && typeof cameraAny.addComponent === 'function') {
147203
147829
  try {
147204
147830
  cameraAny.addComponent('script');
147205
147831
  }
@@ -147222,16 +147848,23 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
147222
147848
  minHeight: DEFAULT_FLY_CAMERA_CONFIG.minHeight,
147223
147849
  maxHeight: DEFAULT_FLY_CAMERA_CONFIG.maxHeight,
147224
147850
  };
147225
- this._fly = cameraAny?.script?.create?.('flyCamera', {
147226
- attributes: flyAttributes,
147227
- });
147228
- if (this._fly) {
147229
- ;
147230
- this._fly.emitFlyEvent = (type, detail) => {
147231
- this.emit({ type: type, detail });
147232
- };
147233
- // Deactivate by default; orbit is the initial mode
147234
- this._fly.deactivate?.();
147851
+ // Prefer script-based fly when available; fallback to controller otherwise.
147852
+ const canCreateScript = typeof cameraAny?.script?.create === 'function';
147853
+ if (canCreateScript) {
147854
+ const created = cameraAny.script.create('flyCamera', { attributes: flyAttributes });
147855
+ this._fly = created;
147856
+ if (this._fly) {
147857
+ ;
147858
+ this._fly.emitFlyEvent = (type, detail) => {
147859
+ this.emit({ type: type, detail });
147860
+ };
147861
+ this._fly.deactivate?.();
147862
+ }
147863
+ }
147864
+ else {
147865
+ const controller = new FlyCameraController(ctx.app, cameraAny, (type, detail) => this.emit({ type: type, detail }), flyAttributes);
147866
+ controller.deactivate();
147867
+ this._fly = controller;
147235
147868
  }
147236
147869
  this._cameraMode = 'orbit';
147237
147870
  }
@@ -149100,17 +149733,31 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149100
149733
  // Stop supersplat orbit updates + input
149101
149734
  this._supersplat.setCameraControlsEnabled(false);
149102
149735
  this._supersplat.setCameraManualControl(true);
149103
- // Align fly yaw/pitch with current camera rotation to avoid snapping
149104
- try {
149105
- const euler = this.entities.camera?.getEulerAngles?.();
149106
- if (euler && this._fly) {
149107
- ;
149108
- this._fly._pitch = euler.x || 0;
149109
- this._fly._yaw = euler.y || 0;
149736
+ // Preserve camera position and rotation when switching to fly mode
149737
+ if (this._fly) {
149738
+ // For FlyCameraController (fallback path)
149739
+ if (typeof this._fly.syncFromEntity === 'function') {
149740
+ this._fly.syncFromEntity();
149741
+ }
149742
+ else {
149743
+ // For FlyCameraScript (legacy path)
149744
+ try {
149745
+ const pos = this.entities.camera?.getPosition?.();
149746
+ if (pos) {
149747
+ const posVec = pos.clone ? pos.clone() : new Vec3(pos.x || 0, pos.y || 0, pos.z || 0);
149748
+ this.entities.camera?.setPosition?.(posVec);
149749
+ }
149750
+ const euler = this.entities.camera?.getEulerAngles?.();
149751
+ if (euler) {
149752
+ ;
149753
+ this._fly._pitch = euler.x || 0;
149754
+ this._fly._yaw = euler.y || 0;
149755
+ }
149756
+ }
149757
+ catch {
149758
+ // ignore
149759
+ }
149110
149760
  }
149111
- }
149112
- catch {
149113
- // ignore
149114
149761
  }
149115
149762
  this._fly?.activate?.();
149116
149763
  this._cameraMode = 'fly';
@@ -149227,9 +149874,48 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149227
149874
  this._cameraModeManager?.setFlyConfig(config);
149228
149875
  }
149229
149876
  getFlyCameraConfig() {
149877
+ // SuperSplat path: fly is either a script instance or controller fallback
149878
+ if (this._supersplat && this._fly) {
149879
+ try {
149880
+ const flyAny = this._fly;
149881
+ const cfg = {
149882
+ moveSpeed: flyAny.moveSpeed,
149883
+ fastSpeedMultiplier: flyAny.fastSpeedMultiplier,
149884
+ slowSpeedMultiplier: flyAny.slowSpeedMultiplier,
149885
+ lookSensitivity: flyAny.lookSensitivity,
149886
+ invertY: !!flyAny.invertY,
149887
+ keyBindings: { ...(flyAny.keyBindings || {}) },
149888
+ smoothing: flyAny.smoothing,
149889
+ friction: flyAny.friction,
149890
+ enableCollision: !!flyAny.enableCollision,
149891
+ minHeight: flyAny.minHeight ?? null,
149892
+ maxHeight: flyAny.maxHeight ?? null,
149893
+ };
149894
+ return cfg;
149895
+ }
149896
+ catch {
149897
+ return null;
149898
+ }
149899
+ }
149230
149900
  return this._cameraModeManager?.getFlyConfig() || null;
149231
149901
  }
149232
149902
  getFlyCameraState() {
149903
+ // SuperSplat path: fly is either a script instance or controller fallback
149904
+ if (this._supersplat && this._fly?.getState) {
149905
+ try {
149906
+ const state = this._fly.getState();
149907
+ return {
149908
+ mode: this._cameraMode,
149909
+ position: state.position,
149910
+ rotation: state.rotation,
149911
+ velocity: state.velocity,
149912
+ isMoving: state.isMoving,
149913
+ };
149914
+ }
149915
+ catch {
149916
+ return null;
149917
+ }
149918
+ }
149233
149919
  return this._cameraModeManager?.getFlyState() || null;
149234
149920
  }
149235
149921
  _setupScene() {