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

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 (165) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/README.md +39 -0
  3. package/dist/web-component/splat-viewer.esm.js +1420 -681
  4. package/dist/web-component/splat-viewer.esm.min.js +2 -2
  5. package/dist/web-component/splat-viewer.js +1420 -681
  6. package/dist/web-component/splat-viewer.min.js +2 -2
  7. package/dist/web-component/supersplat-core/doc.d.ts.map +1 -1
  8. package/dist/web-component/supersplat-core/file-handler.d.ts.map +1 -1
  9. package/dist/web-component/supersplat-core/index.d.ts +1 -1
  10. package/dist/web-component/supersplat-core/index.d.ts.map +1 -1
  11. package/dist/web-component/supersplat-core/main.d.ts.map +1 -1
  12. package/dist/web-component/supersplat-core/publish.d.ts.map +1 -1
  13. package/dist/web-component/supersplat-core/render.d.ts.map +1 -1
  14. package/dist/web-component/supersplat-core/tools/measure-tool.d.ts.map +1 -1
  15. package/dist/web-component/types/supersplat-core/doc.d.ts.map +1 -1
  16. package/dist/web-component/types/supersplat-core/file-handler.d.ts.map +1 -1
  17. package/dist/web-component/types/supersplat-core/index.d.ts +1 -1
  18. package/dist/web-component/types/supersplat-core/index.d.ts.map +1 -1
  19. package/dist/web-component/types/supersplat-core/main.d.ts.map +1 -1
  20. package/dist/web-component/types/supersplat-core/publish.d.ts.map +1 -1
  21. package/dist/web-component/types/supersplat-core/render.d.ts.map +1 -1
  22. package/dist/web-component/types/supersplat-core/tools/measure-tool.d.ts.map +1 -1
  23. package/dist/web-component/types/web-component/CameraModeManager.d.ts.map +1 -1
  24. package/dist/web-component/types/web-component/FlyCameraController.d.ts +85 -0
  25. package/dist/web-component/types/web-component/FlyCameraController.d.ts.map +1 -0
  26. package/dist/web-component/types/web-component/FlyCameraScript.d.ts.map +1 -1
  27. package/dist/web-component/types/web-component/SplatViewerCore.d.ts +1 -0
  28. package/dist/web-component/types/web-component/SplatViewerCore.d.ts.map +1 -1
  29. package/dist/web-component/types/web-component/SplatViewerElement.d.ts +2 -0
  30. package/dist/web-component/types/web-component/SplatViewerElement.d.ts.map +1 -1
  31. package/dist/web-component/types/web-component/supersplat/BoxSelectionAPI.d.ts +1 -0
  32. package/dist/web-component/types/web-component/supersplat/BoxSelectionAPI.d.ts.map +1 -1
  33. package/dist/web-component/types/web-component/supersplat/SphereSelectionAPI.d.ts +1 -0
  34. package/dist/web-component/types/web-component/supersplat/SphereSelectionAPI.d.ts.map +1 -1
  35. package/dist/web-component/types/web-component/supersplat/blue-noise.d.ts +3 -0
  36. package/dist/web-component/types/web-component/supersplat/blue-noise.d.ts.map +1 -0
  37. package/dist/web-component/types/web-component/types/attributes.d.ts +3 -0
  38. package/dist/web-component/types/web-component/types/attributes.d.ts.map +1 -1
  39. package/dist/web-component/types/web-component/types/core.d.ts +2 -0
  40. package/dist/web-component/types/web-component/types/core.d.ts.map +1 -1
  41. package/dist/web-component/types/web-component/utils/config.d.ts.map +1 -1
  42. package/dist/web-component/web-component/CameraModeManager.d.ts.map +1 -1
  43. package/dist/web-component/web-component/FlyCameraController.d.ts +85 -0
  44. package/dist/web-component/web-component/FlyCameraController.d.ts.map +1 -0
  45. package/dist/web-component/web-component/FlyCameraScript.d.ts.map +1 -1
  46. package/dist/web-component/web-component/SplatViewerCore.d.ts +1 -0
  47. package/dist/web-component/web-component/SplatViewerCore.d.ts.map +1 -1
  48. package/dist/web-component/web-component/SplatViewerElement.d.ts +2 -0
  49. package/dist/web-component/web-component/SplatViewerElement.d.ts.map +1 -1
  50. package/dist/web-component/web-component/supersplat/BoxSelectionAPI.d.ts +1 -0
  51. package/dist/web-component/web-component/supersplat/BoxSelectionAPI.d.ts.map +1 -1
  52. package/dist/web-component/web-component/supersplat/SphereSelectionAPI.d.ts +1 -0
  53. package/dist/web-component/web-component/supersplat/SphereSelectionAPI.d.ts.map +1 -1
  54. package/dist/web-component/web-component/supersplat/blue-noise.d.ts +3 -0
  55. package/dist/web-component/web-component/supersplat/blue-noise.d.ts.map +1 -0
  56. package/dist/web-component/web-component/types/attributes.d.ts +3 -0
  57. package/dist/web-component/web-component/types/attributes.d.ts.map +1 -1
  58. package/dist/web-component/web-component/types/core.d.ts +2 -0
  59. package/dist/web-component/web-component/types/core.d.ts.map +1 -1
  60. package/dist/web-component/web-component/utils/config.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/dist/web-component/supersplat-core/ui/bottom-toolbar.d.ts +0 -8
  63. package/dist/web-component/supersplat-core/ui/bottom-toolbar.d.ts.map +0 -1
  64. package/dist/web-component/supersplat-core/ui/color-panel.d.ts +0 -8
  65. package/dist/web-component/supersplat-core/ui/color-panel.d.ts.map +0 -1
  66. package/dist/web-component/supersplat-core/ui/color.d.ts +0 -20
  67. package/dist/web-component/supersplat-core/ui/color.d.ts.map +0 -1
  68. package/dist/web-component/supersplat-core/ui/data-panel.d.ts +0 -7
  69. package/dist/web-component/supersplat-core/ui/data-panel.d.ts.map +0 -1
  70. package/dist/web-component/supersplat-core/ui/editor.d.ts +0 -14
  71. package/dist/web-component/supersplat-core/ui/editor.d.ts.map +0 -1
  72. package/dist/web-component/supersplat-core/ui/export-popup.d.ts +0 -11
  73. package/dist/web-component/supersplat-core/ui/export-popup.d.ts.map +0 -1
  74. package/dist/web-component/supersplat-core/ui/histogram.d.ts +0 -32
  75. package/dist/web-component/supersplat-core/ui/histogram.d.ts.map +0 -1
  76. package/dist/web-component/supersplat-core/ui/image-settings-dialog.d.ts +0 -11
  77. package/dist/web-component/supersplat-core/ui/image-settings-dialog.d.ts.map +0 -1
  78. package/dist/web-component/supersplat-core/ui/localization.d.ts +0 -9
  79. package/dist/web-component/supersplat-core/ui/localization.d.ts.map +0 -1
  80. package/dist/web-component/supersplat-core/ui/menu-panel.d.ts +0 -21
  81. package/dist/web-component/supersplat-core/ui/menu-panel.d.ts.map +0 -1
  82. package/dist/web-component/supersplat-core/ui/menu.d.ts +0 -7
  83. package/dist/web-component/supersplat-core/ui/menu.d.ts.map +0 -1
  84. package/dist/web-component/supersplat-core/ui/mode-toggle.d.ts +0 -8
  85. package/dist/web-component/supersplat-core/ui/mode-toggle.d.ts.map +0 -1
  86. package/dist/web-component/supersplat-core/ui/popup.d.ts +0 -16
  87. package/dist/web-component/supersplat-core/ui/popup.d.ts.map +0 -1
  88. package/dist/web-component/supersplat-core/ui/progress.d.ts +0 -9
  89. package/dist/web-component/supersplat-core/ui/progress.d.ts.map +0 -1
  90. package/dist/web-component/supersplat-core/ui/publish-settings-dialog.d.ts +0 -11
  91. package/dist/web-component/supersplat-core/ui/publish-settings-dialog.d.ts.map +0 -1
  92. package/dist/web-component/supersplat-core/ui/right-toolbar.d.ts +0 -8
  93. package/dist/web-component/supersplat-core/ui/right-toolbar.d.ts.map +0 -1
  94. package/dist/web-component/supersplat-core/ui/scene-panel.d.ts +0 -8
  95. package/dist/web-component/supersplat-core/ui/scene-panel.d.ts.map +0 -1
  96. package/dist/web-component/supersplat-core/ui/shortcuts-popup.d.ts +0 -6
  97. package/dist/web-component/supersplat-core/ui/shortcuts-popup.d.ts.map +0 -1
  98. package/dist/web-component/supersplat-core/ui/spinner.d.ts +0 -6
  99. package/dist/web-component/supersplat-core/ui/spinner.d.ts.map +0 -1
  100. package/dist/web-component/supersplat-core/ui/splat-list.d.ts +0 -25
  101. package/dist/web-component/supersplat-core/ui/splat-list.d.ts.map +0 -1
  102. package/dist/web-component/supersplat-core/ui/timeline-panel.d.ts +0 -8
  103. package/dist/web-component/supersplat-core/ui/timeline-panel.d.ts.map +0 -1
  104. package/dist/web-component/supersplat-core/ui/tooltips.d.ts +0 -10
  105. package/dist/web-component/supersplat-core/ui/tooltips.d.ts.map +0 -1
  106. package/dist/web-component/supersplat-core/ui/transform.d.ts +0 -7
  107. package/dist/web-component/supersplat-core/ui/transform.d.ts.map +0 -1
  108. package/dist/web-component/supersplat-core/ui/video-settings-dialog.d.ts +0 -11
  109. package/dist/web-component/supersplat-core/ui/video-settings-dialog.d.ts.map +0 -1
  110. package/dist/web-component/supersplat-core/ui/view-cube.d.ts +0 -9
  111. package/dist/web-component/supersplat-core/ui/view-cube.d.ts.map +0 -1
  112. package/dist/web-component/supersplat-core/ui/view-panel.d.ts +0 -8
  113. package/dist/web-component/supersplat-core/ui/view-panel.d.ts.map +0 -1
  114. package/dist/web-component/types/supersplat-core/ui/bottom-toolbar.d.ts +0 -8
  115. package/dist/web-component/types/supersplat-core/ui/bottom-toolbar.d.ts.map +0 -1
  116. package/dist/web-component/types/supersplat-core/ui/color-panel.d.ts +0 -8
  117. package/dist/web-component/types/supersplat-core/ui/color-panel.d.ts.map +0 -1
  118. package/dist/web-component/types/supersplat-core/ui/color.d.ts +0 -20
  119. package/dist/web-component/types/supersplat-core/ui/color.d.ts.map +0 -1
  120. package/dist/web-component/types/supersplat-core/ui/data-panel.d.ts +0 -7
  121. package/dist/web-component/types/supersplat-core/ui/data-panel.d.ts.map +0 -1
  122. package/dist/web-component/types/supersplat-core/ui/editor.d.ts +0 -14
  123. package/dist/web-component/types/supersplat-core/ui/editor.d.ts.map +0 -1
  124. package/dist/web-component/types/supersplat-core/ui/export-popup.d.ts +0 -11
  125. package/dist/web-component/types/supersplat-core/ui/export-popup.d.ts.map +0 -1
  126. package/dist/web-component/types/supersplat-core/ui/histogram.d.ts +0 -32
  127. package/dist/web-component/types/supersplat-core/ui/histogram.d.ts.map +0 -1
  128. package/dist/web-component/types/supersplat-core/ui/image-settings-dialog.d.ts +0 -11
  129. package/dist/web-component/types/supersplat-core/ui/image-settings-dialog.d.ts.map +0 -1
  130. package/dist/web-component/types/supersplat-core/ui/localization.d.ts +0 -9
  131. package/dist/web-component/types/supersplat-core/ui/localization.d.ts.map +0 -1
  132. package/dist/web-component/types/supersplat-core/ui/menu-panel.d.ts +0 -21
  133. package/dist/web-component/types/supersplat-core/ui/menu-panel.d.ts.map +0 -1
  134. package/dist/web-component/types/supersplat-core/ui/menu.d.ts +0 -7
  135. package/dist/web-component/types/supersplat-core/ui/menu.d.ts.map +0 -1
  136. package/dist/web-component/types/supersplat-core/ui/mode-toggle.d.ts +0 -8
  137. package/dist/web-component/types/supersplat-core/ui/mode-toggle.d.ts.map +0 -1
  138. package/dist/web-component/types/supersplat-core/ui/popup.d.ts +0 -16
  139. package/dist/web-component/types/supersplat-core/ui/popup.d.ts.map +0 -1
  140. package/dist/web-component/types/supersplat-core/ui/progress.d.ts +0 -9
  141. package/dist/web-component/types/supersplat-core/ui/progress.d.ts.map +0 -1
  142. package/dist/web-component/types/supersplat-core/ui/publish-settings-dialog.d.ts +0 -11
  143. package/dist/web-component/types/supersplat-core/ui/publish-settings-dialog.d.ts.map +0 -1
  144. package/dist/web-component/types/supersplat-core/ui/right-toolbar.d.ts +0 -8
  145. package/dist/web-component/types/supersplat-core/ui/right-toolbar.d.ts.map +0 -1
  146. package/dist/web-component/types/supersplat-core/ui/scene-panel.d.ts +0 -8
  147. package/dist/web-component/types/supersplat-core/ui/scene-panel.d.ts.map +0 -1
  148. package/dist/web-component/types/supersplat-core/ui/shortcuts-popup.d.ts +0 -6
  149. package/dist/web-component/types/supersplat-core/ui/shortcuts-popup.d.ts.map +0 -1
  150. package/dist/web-component/types/supersplat-core/ui/spinner.d.ts +0 -6
  151. package/dist/web-component/types/supersplat-core/ui/spinner.d.ts.map +0 -1
  152. package/dist/web-component/types/supersplat-core/ui/splat-list.d.ts +0 -25
  153. package/dist/web-component/types/supersplat-core/ui/splat-list.d.ts.map +0 -1
  154. package/dist/web-component/types/supersplat-core/ui/timeline-panel.d.ts +0 -8
  155. package/dist/web-component/types/supersplat-core/ui/timeline-panel.d.ts.map +0 -1
  156. package/dist/web-component/types/supersplat-core/ui/tooltips.d.ts +0 -10
  157. package/dist/web-component/types/supersplat-core/ui/tooltips.d.ts.map +0 -1
  158. package/dist/web-component/types/supersplat-core/ui/transform.d.ts +0 -7
  159. package/dist/web-component/types/supersplat-core/ui/transform.d.ts.map +0 -1
  160. package/dist/web-component/types/supersplat-core/ui/video-settings-dialog.d.ts +0 -11
  161. package/dist/web-component/types/supersplat-core/ui/video-settings-dialog.d.ts.map +0 -1
  162. package/dist/web-component/types/supersplat-core/ui/view-cube.d.ts +0 -9
  163. package/dist/web-component/types/supersplat-core/ui/view-cube.d.ts.map +0 -1
  164. package/dist/web-component/types/supersplat-core/ui/view-panel.d.ts +0 -8
  165. 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
  }
@@ -139088,6 +139519,7 @@ const DEFAULT_CONFIG = {
139088
139519
  enableStats: false,
139089
139520
  autoFocus: true,
139090
139521
  maxSplats: 2000000,
139522
+ previewMode: false,
139091
139523
  camera: {
139092
139524
  position: { x: 0, y: 0, z: 10 },
139093
139525
  target: { x: 0, y: 0, z: 0 },
@@ -139177,6 +139609,10 @@ function getConfigFromAttributes(element) {
139177
139609
  if (enableStats !== null) {
139178
139610
  config.enableStats = parseBoolean(enableStats);
139179
139611
  }
139612
+ const previewMode = element.getAttribute('preview-mode');
139613
+ if (previewMode !== null) {
139614
+ config.previewMode = parseBoolean(previewMode);
139615
+ }
139180
139616
  // Parse number attributes
139181
139617
  const maxSplats = element.getAttribute('max-splats');
139182
139618
  if (maxSplats !== null) {
@@ -140317,51 +140753,161 @@ function toPcVec3(value, fallback) {
140317
140753
  return fallback.clone();
140318
140754
  }
140319
140755
 
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;
140756
+ const cache = new WeakMap();
140757
+ function createBlueNoiseTexture(device) {
140758
+ const size = 32;
140759
+ const texture = new Texture$1(device, {
140760
+ width: size,
140761
+ height: size,
140762
+ format: PIXELFORMAT_R8_G8_B8_A8,
140763
+ mipmaps: false,
140764
+ });
140765
+ texture.addressU = ADDRESS_REPEAT;
140766
+ texture.addressV = ADDRESS_REPEAT;
140767
+ texture.minFilter = FILTER_NEAREST;
140768
+ texture.magFilter = FILTER_NEAREST;
140769
+ const pixels = texture.lock();
140770
+ const seed = 1337;
140771
+ let value = seed;
140772
+ const random = () => {
140773
+ value ^= value << 13;
140774
+ value ^= value >>> 17;
140775
+ value ^= value << 5;
140776
+ return ((value >>> 0) % 256) / 255;
140777
+ };
140778
+ for (let i = 0; i < size * size; i++) {
140779
+ const noise = Math.floor(random() * 255);
140780
+ const idx = i * 4;
140781
+ pixels[idx + 0] = noise;
140782
+ pixels[idx + 1] = noise;
140783
+ pixels[idx + 2] = noise;
140784
+ pixels[idx + 3] = 255;
140785
+ }
140786
+ texture.unlock();
140787
+ texture.name = 'supersplat-blue-noise';
140788
+ return texture;
140789
+ }
140790
+ function getBlueNoiseTex32(device) {
140791
+ const existing = cache.get(device);
140792
+ if (existing)
140793
+ return existing;
140794
+ const tex = createBlueNoiseTexture(device);
140795
+ cache.set(device, tex);
140796
+ return tex;
140797
+ }
140325
140798
 
140326
- uniform mat4 matrix_model;
140327
- uniform mat4 matrix_viewProjection;
140799
+ // SuperSplat-like selection box shader: screen-space ray/box intersection that draws a thick
140800
+ // white wire/grid pattern (not dependent on WebGL line width).
140801
+ const BOX_SELECT_VS = /* glsl */ `
140802
+ attribute vec3 vertex_position;
140328
140803
 
140329
- varying vec2 vUv;
140804
+ uniform mat4 matrix_model;
140805
+ uniform mat4 matrix_viewProjection;
140330
140806
 
140331
- void main(void) {
140332
- vUv = vertex_texCoord0;
140333
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140334
- }
140807
+ void main() {
140808
+ gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140809
+ }
140335
140810
  `;
140336
- const THICK_WIREFRAME_BOX_FS = /* glsl */ `
140337
- #ifdef GL_OES_standard_derivatives
140338
- #extension GL_OES_standard_derivatives : enable
140339
- #endif
140811
+ const BOX_SELECT_FS = /* glsl */ `
140812
+ // ray-box intersection in box space
140813
+ bool intersectBox(out float t0, out float t1, out int axis0, out int axis1, vec3 pos, vec3 dir, vec3 boxCen, vec3 boxLen)
140814
+ {
140815
+ bvec3 validDir = notEqual(dir, vec3(0.0));
140816
+ vec3 absDir = abs(dir);
140817
+ vec3 signDir = sign(dir);
140818
+ vec3 m = vec3(
140819
+ validDir.x ? 1.0 / absDir.x : 0.0,
140820
+ validDir.y ? 1.0 / absDir.y : 0.0,
140821
+ validDir.z ? 1.0 / absDir.z : 0.0
140822
+ ) * signDir;
140340
140823
 
140341
- precision mediump float;
140824
+ vec3 n = m * (pos - boxCen);
140825
+ vec3 k = abs(m) * boxLen;
140342
140826
 
140343
- uniform vec3 lineColor;
140344
- uniform float lineWidth;
140345
- uniform float opacity;
140827
+ vec3 v0 = -n - k;
140828
+ vec3 v1 = -n + k;
140346
140829
 
140347
- varying vec2 vUv;
140830
+ // replace invalid axes with -inf and +inf so the tests below ignore them
140831
+ v0 = mix(vec3(-1.0 / 0.0000001), v0, validDir);
140832
+ v1 = mix(vec3(1.0 / 0.0000001), v1, validDir);
140348
140833
 
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));
140834
+ axis0 = (v0.x > v0.y) ? ((v0.x > v0.z) ? 0 : 2) : ((v0.y > v0.z) ? 1 : 2);
140835
+ axis1 = (v1.x < v1.y) ? ((v1.x < v1.z) ? 0 : 2) : ((v1.y < v1.z) ? 1 : 2);
140352
140836
 
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;
140837
+ t0 = v0[axis0];
140838
+ t1 = v1[axis1];
140360
140839
 
140361
- if (a <= 0.001) discard;
140362
- gl_FragColor = vec4(lineColor, a);
140363
- }
140840
+ if (t0 > t1 || t1 < 0.0) {
140841
+ return false;
140842
+ }
140843
+
140844
+ return true;
140845
+ }
140846
+
140847
+ float calcDepth(in vec3 pos, in mat4 viewProjection) {
140848
+ vec4 v = viewProjection * vec4(pos, 1.0);
140849
+ return (v.z / v.w) * 0.5 + 0.5;
140850
+ }
140851
+
140852
+ uniform sampler2D blueNoiseTex32;
140853
+ uniform mat4 matrix_viewProjection;
140854
+ uniform vec3 boxCen;
140855
+ uniform vec3 boxLen;
140856
+
140857
+ uniform vec3 near_origin;
140858
+ uniform vec3 near_x;
140859
+ uniform vec3 near_y;
140860
+
140861
+ uniform vec3 far_origin;
140862
+ uniform vec3 far_x;
140863
+ uniform vec3 far_y;
140864
+
140865
+ uniform vec2 targetSize;
140866
+ uniform vec3 lineColor;
140867
+
140868
+ bool writeDepth(float alpha) {
140869
+ ivec2 uv = ivec2(gl_FragCoord.xy);
140870
+ ivec2 size = textureSize(blueNoiseTex32, 0);
140871
+ return alpha > texelFetch(blueNoiseTex32, uv % size, 0).y;
140872
+ }
140873
+
140874
+ bool strips(vec3 pos, int axis) {
140875
+ // Thickness tuned to match SuperSplat viewer "thick wire"
140876
+ bvec3 b = lessThan(fract(pos * 2.0 + vec3(0.015)), vec3(0.06));
140877
+ b[axis] = false;
140878
+ return any(b);
140879
+ }
140880
+
140881
+ void main() {
140882
+ vec2 clip = gl_FragCoord.xy / targetSize;
140883
+ vec3 worldNear = near_origin + near_x * clip.x + near_y * clip.y;
140884
+ vec3 worldFar = far_origin + far_x * clip.x + far_y * clip.y;
140885
+ vec3 rayDir = normalize(worldFar - worldNear);
140886
+
140887
+ float t0, t1;
140888
+ int axis0, axis1;
140889
+ if (!intersectBox(t0, t1, axis0, axis1, worldNear, rayDir, boxCen, boxLen)) {
140890
+ discard;
140891
+ }
140892
+
140893
+ vec3 frontPos = worldNear + rayDir * t0;
140894
+ bool front = t0 > 0.0 && strips(frontPos - boxCen, axis0);
140895
+
140896
+ vec3 backPos = worldNear + rayDir * t1;
140897
+ bool back = strips(backPos - boxCen, axis1);
140898
+
140899
+ if (front) {
140900
+ gl_FragColor = vec4(lineColor, 0.6);
140901
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(frontPos, matrix_viewProjection) : 1.0;
140902
+ } else if (back) {
140903
+ gl_FragColor = vec4(lineColor, 0.6);
140904
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(backPos, matrix_viewProjection) : 1.0;
140905
+ } else {
140906
+ discard;
140907
+ }
140908
+ }
140364
140909
  `;
140910
+ const tmpPos$1 = new Vec3();
140365
140911
  class BoxSelectionAPI {
140366
140912
  constructor() {
140367
140913
  this.boxes = new Map();
@@ -140398,6 +140944,7 @@ class BoxSelectionAPI {
140398
140944
  this.ensureTranslateGizmo();
140399
140945
  this.updateGizmoSize();
140400
140946
  this.updateGizmoAttachment();
140947
+ this.updateBoxShaderUniforms();
140401
140948
  }
140402
140949
  createBox(options = {}) {
140403
140950
  if (!this.app || !this.parent) {
@@ -140420,7 +140967,7 @@ class BoxSelectionAPI {
140420
140967
  entity.render.castShadows = false;
140421
140968
  entity.render.receiveShadows = false;
140422
140969
  entity.render.enabled = visible;
140423
- // Render as a thick wireframe (shader-based) so line width is consistent across browsers.
140970
+ // Render as a thick wireframe (shader-based) to match SuperSplat viewer.
140424
140971
  this.parent.addChild(entity);
140425
140972
  const record = {
140426
140973
  id,
@@ -140507,8 +141054,8 @@ class BoxSelectionAPI {
140507
141054
  return false;
140508
141055
  }
140509
141056
  record.color.set(r, g, b);
140510
- const mat = this.buildWireframeMaterial(record.color);
140511
- record.entity.render.material = mat;
141057
+ const mat = record.entity.render?.material;
141058
+ mat?.setParameter?.('lineColor', [record.color.x, record.color.y, record.color.z]);
140512
141059
  this.requestRender();
140513
141060
  return true;
140514
141061
  }
@@ -140703,19 +141250,34 @@ class BoxSelectionAPI {
140703
141250
  }
140704
141251
  buildWireframeMaterial(color) {
140705
141252
  const material = new ShaderMaterial$1({
140706
- uniqueName: 'boxSelectionThickWireframe',
140707
- vertexGLSL: THICK_WIREFRAME_BOX_VS,
140708
- fragmentGLSL: THICK_WIREFRAME_BOX_FS,
141253
+ uniqueName: 'boxSelectionSupersplatWire',
141254
+ vertexGLSL: BOX_SELECT_VS,
141255
+ fragmentGLSL: BOX_SELECT_FS,
140709
141256
  });
140710
- material.cull = CULLFACE_NONE;
140711
- material.depthWrite = false;
141257
+ material.cull = CULLFACE_FRONT;
140712
141258
  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
141259
  material.update();
141260
+ material.setParameter('lineColor', [color.x, color.y, color.z]);
140717
141261
  return material;
140718
141262
  }
141263
+ updateBoxShaderUniforms() {
141264
+ if (!this.app)
141265
+ return;
141266
+ const device = this.app.graphicsDevice;
141267
+ // Ensure required global uniforms for selection shaders.
141268
+ device.scope.resolve('targetSize').setValue([device.width, device.height]);
141269
+ device.scope.resolve('blueNoiseTex32').setValue(getBlueNoiseTex32(device));
141270
+ for (const record of this.boxes.values()) {
141271
+ const mat = record.entity.render?.material;
141272
+ if (!mat?.setParameter)
141273
+ continue;
141274
+ // World-space center
141275
+ record.entity.getWorldTransform().getTranslation(tmpPos$1);
141276
+ mat.setParameter('boxCen', [tmpPos$1.x, tmpPos$1.y, tmpPos$1.z]);
141277
+ // Half extents
141278
+ mat.setParameter('boxLen', [record.lenX * 0.5, record.lenY * 0.5, record.lenZ * 0.5]);
141279
+ }
141280
+ }
140719
141281
  emitEvent(event, detail) {
140720
141282
  if (this.onEvent) {
140721
141283
  this.onEvent(event, detail);
@@ -140727,59 +141289,110 @@ class BoxSelectionAPI {
140727
141289
  }
140728
141290
  }
140729
141291
 
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;
141292
+ // SuperSplat-like selection sphere shader: screen-space ray/sphere intersection that draws a thick
141293
+ // white wire/grid pattern (not dependent on WebGL line width).
141294
+ const SPHERE_SELECT_VS = /* glsl */ `
141295
+ attribute vec3 vertex_position;
140740
141296
 
140741
- varying vec2 vUv;
141297
+ uniform mat4 matrix_model;
141298
+ uniform mat4 matrix_viewProjection;
140742
141299
 
140743
- void main(void) {
140744
- vUv = vertex_texCoord0;
140745
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140746
- }
141300
+ void main() {
141301
+ gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
141302
+ }
140747
141303
  `;
140748
- const THICK_WIREFRAME_SPHERE_FS = /* glsl */ `
140749
- #ifdef GL_OES_standard_derivatives
140750
- #extension GL_OES_standard_derivatives : enable
140751
- #endif
141304
+ const SPHERE_SELECT_FS = /* glsl */ `
141305
+ bool intersectSphere(out float t0, out float t1, vec3 pos, vec3 dir, vec4 sphere) {
141306
+ vec3 L = sphere.xyz - pos;
141307
+ float tca = dot(L, dir);
140752
141308
 
140753
- precision mediump float;
141309
+ float d2 = sphere.w * sphere.w - (dot(L, L) - tca * tca);
141310
+ if (d2 <= 0.0) {
141311
+ return false;
141312
+ }
140754
141313
 
140755
- uniform vec3 lineColor;
140756
- uniform float lineWidth;
140757
- uniform float opacity;
141314
+ float thc = sqrt(d2);
141315
+ t0 = tca - thc;
141316
+ t1 = tca + thc;
141317
+ if (t1 <= 0.0) {
141318
+ return false;
141319
+ }
140758
141320
 
140759
- varying vec2 vUv;
141321
+ return true;
141322
+ }
140760
141323
 
140761
- float gridLineDist(float v, float freq) {
140762
- float f = fract(v * freq);
140763
- return min(f, 1.0 - f);
140764
- }
141324
+ float calcDepth(in vec3 pos, in mat4 viewProjection) {
141325
+ vec4 v = viewProjection * vec4(pos, 1.0);
141326
+ return (v.z / v.w) * 0.5 + 0.5;
141327
+ }
140765
141328
 
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;
141329
+ vec2 calcAzimuthElev(in vec3 dir) {
141330
+ float azimuth = atan(dir.z, dir.x);
141331
+ float elev = asin(dir.y);
141332
+ return vec2(azimuth, elev) * 180.0 / 3.14159;
141333
+ }
140778
141334
 
140779
- if (a <= 0.001) discard;
140780
- gl_FragColor = vec4(lineColor, a);
140781
- }
141335
+ uniform sampler2D blueNoiseTex32;
141336
+ uniform mat4 matrix_viewProjection;
141337
+ uniform vec4 sphere;
141338
+ uniform vec3 lineColor;
141339
+
141340
+ uniform vec3 near_origin;
141341
+ uniform vec3 near_x;
141342
+ uniform vec3 near_y;
141343
+
141344
+ uniform vec3 far_origin;
141345
+ uniform vec3 far_x;
141346
+ uniform vec3 far_y;
141347
+
141348
+ uniform vec2 targetSize;
141349
+
141350
+ bool writeDepth(float alpha) {
141351
+ vec2 uv = fract(gl_FragCoord.xy / 32.0);
141352
+ float noise = texture2DLod(blueNoiseTex32, uv, 0.0).y;
141353
+ return alpha > noise;
141354
+ }
141355
+
141356
+ bool strips(vec3 lp) {
141357
+ vec2 ae = calcAzimuthElev(normalize(lp));
141358
+
141359
+ float spacing = 180.0 / (2.0 * 3.14159 * sphere.w);
141360
+ // Thickness tuned to match SuperSplat viewer "thick wire"
141361
+ float size = 0.06;
141362
+ return fract(ae.x / spacing) < size ||
141363
+ fract(ae.y / spacing) < size;
141364
+ }
141365
+
141366
+ void main() {
141367
+ vec2 clip = gl_FragCoord.xy / targetSize;
141368
+ vec3 worldNear = near_origin + near_x * clip.x + near_y * clip.y;
141369
+ vec3 worldFar = far_origin + far_x * clip.x + far_y * clip.y;
141370
+
141371
+ vec3 rayDir = normalize(worldFar - worldNear);
141372
+
141373
+ float t0, t1;
141374
+ if (!intersectSphere(t0, t1, worldNear, rayDir, sphere)) {
141375
+ discard;
141376
+ }
141377
+
141378
+ vec3 frontPos = worldNear + rayDir * t0;
141379
+ bool front = t0 > 0.0 && strips(frontPos - sphere.xyz);
141380
+
141381
+ vec3 backPos = worldNear + rayDir * t1;
141382
+ bool back = strips(backPos - sphere.xyz);
141383
+
141384
+ if (front) {
141385
+ gl_FragColor = vec4(lineColor, 0.6);
141386
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(frontPos, matrix_viewProjection) : 1.0;
141387
+ } else if (back) {
141388
+ gl_FragColor = vec4(lineColor, 0.6);
141389
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(backPos, matrix_viewProjection) : 1.0;
141390
+ } else {
141391
+ discard;
141392
+ }
141393
+ }
140782
141394
  `;
141395
+ const tmpPos = new Vec3();
140783
141396
  class SphereSelectionAPI {
140784
141397
  constructor() {
140785
141398
  this.spheres = new Map();
@@ -140815,6 +141428,7 @@ class SphereSelectionAPI {
140815
141428
  this.ensureTranslateGizmo();
140816
141429
  this.updateGizmoSize();
140817
141430
  this.updateGizmoAttachment();
141431
+ this.updateSphereShaderUniforms();
140818
141432
  }
140819
141433
  createSphere(options = {}) {
140820
141434
  if (!this.app || !this.parent) {
@@ -140827,7 +141441,8 @@ class SphereSelectionAPI {
140827
141441
  const visible = options.visible ?? true;
140828
141442
  const entity = new Entity(id);
140829
141443
  entity.addComponent('render', {
140830
- type: 'sphere',
141444
+ // Use a box proxy like SuperSplat; shader ray-marches a true sphere.
141445
+ type: 'box',
140831
141446
  material: this.buildWireframeMaterial(color),
140832
141447
  });
140833
141448
  entity.setLocalScale(radius * 2, radius * 2, radius * 2); // sphere diameter = radius * 2
@@ -140835,7 +141450,7 @@ class SphereSelectionAPI {
140835
141450
  entity.render.castShadows = false;
140836
141451
  entity.render.receiveShadows = false;
140837
141452
  entity.render.enabled = visible;
140838
- // Render as a thick wireframe (shader-based) so line width is consistent across browsers.
141453
+ // Render as a thick wireframe (shader-based) to match SuperSplat viewer.
140839
141454
  this.parent.addChild(entity);
140840
141455
  const record = {
140841
141456
  id,
@@ -140918,8 +141533,12 @@ class SphereSelectionAPI {
140918
141533
  return false;
140919
141534
  }
140920
141535
  record.color.set(r, g, b);
140921
- const mat = this.buildWireframeMaterial(record.color);
140922
- record.entity.render.material = mat;
141536
+ const mat = record.entity.render?.material;
141537
+ mat?.setParameter?.('lineColor', [
141538
+ record.color.x,
141539
+ record.color.y,
141540
+ record.color.z,
141541
+ ]);
140923
141542
  this.requestRender();
140924
141543
  return true;
140925
141544
  }
@@ -141117,19 +141736,30 @@ class SphereSelectionAPI {
141117
141736
  }
141118
141737
  buildWireframeMaterial(color) {
141119
141738
  const material = new ShaderMaterial$1({
141120
- uniqueName: 'sphereSelectionThickWireframe',
141121
- vertexGLSL: THICK_WIREFRAME_SPHERE_VS,
141122
- fragmentGLSL: THICK_WIREFRAME_SPHERE_FS,
141739
+ uniqueName: 'sphereSelectionSupersplatWire',
141740
+ vertexGLSL: SPHERE_SELECT_VS,
141741
+ fragmentGLSL: SPHERE_SELECT_FS,
141123
141742
  });
141124
- material.cull = CULLFACE_NONE;
141125
- material.depthWrite = false;
141743
+ material.cull = CULLFACE_FRONT;
141126
141744
  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
141745
  material.update();
141746
+ material.setParameter('lineColor', [color.x, color.y, color.z]);
141131
141747
  return material;
141132
141748
  }
141749
+ updateSphereShaderUniforms() {
141750
+ if (!this.app)
141751
+ return;
141752
+ const device = this.app.graphicsDevice;
141753
+ device.scope.resolve('targetSize').setValue([device.width, device.height]);
141754
+ device.scope.resolve('blueNoiseTex32').setValue(getBlueNoiseTex32(device));
141755
+ for (const record of this.spheres.values()) {
141756
+ const mat = record.entity.render?.material;
141757
+ if (!mat?.setParameter)
141758
+ continue;
141759
+ record.entity.getWorldTransform().getTranslation(tmpPos);
141760
+ mat.setParameter('sphere', [tmpPos.x, tmpPos.y, tmpPos.z, record.radius]);
141761
+ }
141762
+ }
141133
141763
  emitEvent(event, detail) {
141134
141764
  if (this.onEvent) {
141135
141765
  this.onEvent(event, detail);
@@ -142536,222 +143166,222 @@ class AssetLoader {
142536
143166
  }
142537
143167
  }
142538
143168
 
142539
- const vertexShader$6 = /* glsl */ `
142540
- attribute vec2 vertex_position;
142541
- void main(void) {
142542
- gl_Position = vec4(vertex_position, 0.0, 1.0);
142543
- }
142544
- `;
142545
- const fragmentShader$6 = /* glsl */ `
142546
- uniform highp usampler2D transformA; // splat center x, y, z
142547
- uniform highp usampler2D splatTransform; // transform palette index
142548
- uniform sampler2D transformPalette; // palette of transforms
142549
- uniform sampler2D splatState; // per-splat state
142550
- uniform highp ivec3 splat_params; // texture width, texture height, num splats
142551
- uniform highp uint mode; // 0: selected, 1: visible
142552
-
142553
- // calculate min and max for a single column of splats
142554
- void main(void) {
142555
-
142556
- vec3 boundMin = vec3(1e6);
142557
- vec3 boundMax = vec3(-1e6);
142558
-
142559
- for (int id = 0; id < splat_params.y; id++) {
142560
- // calculate splatUV
142561
- ivec2 splatUV = ivec2(gl_FragCoord.x, id);
142562
-
142563
- // skip out-of-range splats
142564
- if ((splatUV.x + splatUV.y * splat_params.x) >= splat_params.z) {
142565
- continue;
142566
- }
142567
-
142568
- // read splat state
142569
- uint state = uint(texelFetch(splatState, splatUV, 0).r * 255.0);
142570
-
142571
- // skip deleted or locked splats
142572
- if (((mode == 0u) && (state != 1u)) || ((mode == 1u) && ((state & 4u) != 0u))) {
142573
- continue;
142574
- }
142575
-
142576
- // read splat center
142577
- vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
142578
-
142579
- // apply optional per-splat transform
142580
- uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
142581
- if (transformIndex > 0u) {
142582
- // read transform matrix
142583
- int u = int(transformIndex % 512u) * 3;
142584
- int v = int(transformIndex / 512u);
142585
-
142586
- mat3x4 t;
142587
- t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
142588
- t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
142589
- t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
142590
-
142591
- center = vec4(center, 1.0) * t;
142592
- }
142593
-
142594
- boundMin = min(boundMin, mix(center, boundMin, isinf(center)));
142595
- boundMax = max(boundMax, mix(center, boundMax, isinf(center)));
142596
- }
142597
-
142598
- pcFragColor0 = vec4(boundMin, 0.0);
142599
- pcFragColor1 = vec4(boundMax, 0.0);
142600
- }
142601
- `;
142602
-
142603
- const vertexShader$5 = /* glsl */ `
142604
- attribute vec2 vertex_position;
142605
- void main(void) {
142606
- gl_Position = vec4(vertex_position, 0.0, 1.0);
142607
- }
142608
- `;
142609
- const fragmentShader$5 = /* glsl */ `
142610
- uniform highp usampler2D transformA; // splat center x, y, z
142611
- uniform highp usampler2D splatTransform; // transform palette index
142612
- uniform sampler2D transformPalette; // palette of transforms
142613
- uniform uvec2 splat_params; // splat texture width, num splats
142614
-
142615
- uniform mat4 matrix_model;
142616
- uniform mat4 matrix_viewProjection;
142617
-
142618
- uniform uvec2 output_params; // output width, height
142619
-
142620
- // 0: mask, 1: rect, 2: sphere
142621
- uniform int mode;
142622
-
142623
- // mask params
142624
- uniform sampler2D mask; // mask in alpha channel
142625
- uniform vec2 mask_params; // mask width, height
142626
-
142627
- // rect params
142628
- uniform vec4 rect_params; // rect x, y, width, height
142629
-
142630
- // sphere params
142631
- uniform vec4 sphere_params; // sphere x, y, z, radius
142632
-
142633
- // box params
142634
- uniform vec4 box_params; // box x, y, z
142635
- uniform vec4 aabb_params; // len x, y, z
142636
-
142637
- void main(void) {
142638
- // calculate output id
142639
- uvec2 outputUV = uvec2(gl_FragCoord);
142640
- uint outputId = (outputUV.x + outputUV.y * output_params.x) * 4u;
142641
-
142642
- vec4 clr = vec4(0.0);
142643
-
142644
- for (uint i = 0u; i < 4u; i++) {
142645
- uint id = outputId + i;
142646
-
142647
- if (id >= splat_params.y) {
142648
- continue;
142649
- }
142650
-
142651
- // calculate splatUV
142652
- ivec2 splatUV = ivec2(
142653
- int(id % splat_params.x),
142654
- int(id / splat_params.x)
142655
- );
142656
-
142657
- // read splat center
142658
- vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
142659
-
142660
- // apply optional per-splat transform
142661
- uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
142662
- if (transformIndex > 0u) {
142663
- // read transform matrix
142664
- int u = int(transformIndex % 512u) * 3;
142665
- int v = int(transformIndex / 512u);
142666
-
142667
- mat3x4 t;
142668
- t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
142669
- t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
142670
- t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
142671
-
142672
- center = vec4(center, 1.0) * t;
142673
- }
142674
-
142675
- // transform to clip space and discard if outside
142676
- vec3 world = (matrix_model * vec4(center, 1.0)).xyz;
142677
- vec4 clip = matrix_viewProjection * vec4(world, 1.0);
142678
- vec3 ndc = clip.xyz / clip.w;
142679
-
142680
- // skip offscreen fragments
142681
- if (!any(greaterThan(abs(ndc), vec3(1.0)))) {
142682
- if (mode == 0) {
142683
- // select by mask
142684
- ivec2 maskUV = ivec2((ndc.xy * vec2(0.5, -0.5) + 0.5) * mask_params);
142685
- clr[i] = texelFetch(mask, maskUV, 0).a < 1.0 ? 0.0 : 1.0;
142686
- } else if (mode == 1) {
142687
- // select by rect
142688
- clr[i] = all(greaterThan(ndc.xy * vec2(1.0, -1.0), rect_params.xy)) && all(lessThan(ndc.xy * vec2(1.0, -1.0), rect_params.zw)) ? 1.0 : 0.0;
142689
- } else if (mode == 2) {
142690
- // select by sphere
142691
- clr[i] = length(world - sphere_params.xyz) < sphere_params.w ? 1.0 : 0.0;
142692
- } else if (mode == 3) {
142693
- // select by box
142694
- vec3 relativePosition = world - box_params.xyz;
142695
- bool isInsideCube = true;
142696
- if (relativePosition.x < -aabb_params.x || relativePosition.x > aabb_params.x) {
142697
- isInsideCube = false;
142698
- }
142699
- if (relativePosition.y < -aabb_params.y || relativePosition.y > aabb_params.y) {
142700
- isInsideCube = false;
142701
- }
142702
- if (relativePosition.z < -aabb_params.z || relativePosition.z > aabb_params.z) {
142703
- isInsideCube = false;
142704
- }
142705
- clr[i] = isInsideCube ? 1.0 : 0.0;
142706
- }
142707
- }
142708
- }
142709
-
142710
- gl_FragColor = clr;
142711
- }
142712
- `;
142713
-
142714
- const vertexShader$4 = /* glsl */ `
142715
- attribute vec2 vertex_position;
142716
- void main(void) {
142717
- gl_Position = vec4(vertex_position, 0.0, 1.0);
142718
- }
142719
- `;
142720
- const fragmentShader$4 = /* glsl */ `
142721
- uniform highp usampler2D transformA; // splat center x, y, z
142722
- uniform highp usampler2D splatTransform; // transform palette index
142723
- uniform sampler2D transformPalette; // palette of transforms
142724
- uniform ivec2 splat_params; // splat texture width, num splats
142725
-
142726
- void main(void) {
142727
- // calculate output id
142728
- ivec2 splatUV = ivec2(gl_FragCoord);
142729
-
142730
- // skip if splat index is out of bounds
142731
- if (splatUV.x + splatUV.y * splat_params.x >= splat_params.y) {
142732
- discard;
142733
- }
142734
-
142735
- // read splat center
142736
- vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
142737
-
142738
- // apply optional per-splat transform
142739
- uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
142740
- if (transformIndex > 0u) {
142741
- // read transform matrix
142742
- int u = int(transformIndex % 512u) * 3;
142743
- int v = int(transformIndex / 512u);
142744
-
142745
- mat3x4 t;
142746
- t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
142747
- t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
142748
- t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
142749
-
142750
- center = vec4(center, 1.0) * t;
142751
- }
142752
-
142753
- gl_FragColor = vec4(center, 0.0);
142754
- }
143169
+ const vertexShader$6 = /* glsl */ `
143170
+ attribute vec2 vertex_position;
143171
+ void main(void) {
143172
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
143173
+ }
143174
+ `;
143175
+ const fragmentShader$6 = /* glsl */ `
143176
+ uniform highp usampler2D transformA; // splat center x, y, z
143177
+ uniform highp usampler2D splatTransform; // transform palette index
143178
+ uniform sampler2D transformPalette; // palette of transforms
143179
+ uniform sampler2D splatState; // per-splat state
143180
+ uniform highp ivec3 splat_params; // texture width, texture height, num splats
143181
+ uniform highp uint mode; // 0: selected, 1: visible
143182
+
143183
+ // calculate min and max for a single column of splats
143184
+ void main(void) {
143185
+
143186
+ vec3 boundMin = vec3(1e6);
143187
+ vec3 boundMax = vec3(-1e6);
143188
+
143189
+ for (int id = 0; id < splat_params.y; id++) {
143190
+ // calculate splatUV
143191
+ ivec2 splatUV = ivec2(gl_FragCoord.x, id);
143192
+
143193
+ // skip out-of-range splats
143194
+ if ((splatUV.x + splatUV.y * splat_params.x) >= splat_params.z) {
143195
+ continue;
143196
+ }
143197
+
143198
+ // read splat state
143199
+ uint state = uint(texelFetch(splatState, splatUV, 0).r * 255.0);
143200
+
143201
+ // skip deleted or locked splats
143202
+ if (((mode == 0u) && (state != 1u)) || ((mode == 1u) && ((state & 4u) != 0u))) {
143203
+ continue;
143204
+ }
143205
+
143206
+ // read splat center
143207
+ vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
143208
+
143209
+ // apply optional per-splat transform
143210
+ uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
143211
+ if (transformIndex > 0u) {
143212
+ // read transform matrix
143213
+ int u = int(transformIndex % 512u) * 3;
143214
+ int v = int(transformIndex / 512u);
143215
+
143216
+ mat3x4 t;
143217
+ t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
143218
+ t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
143219
+ t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
143220
+
143221
+ center = vec4(center, 1.0) * t;
143222
+ }
143223
+
143224
+ boundMin = min(boundMin, mix(center, boundMin, isinf(center)));
143225
+ boundMax = max(boundMax, mix(center, boundMax, isinf(center)));
143226
+ }
143227
+
143228
+ pcFragColor0 = vec4(boundMin, 0.0);
143229
+ pcFragColor1 = vec4(boundMax, 0.0);
143230
+ }
143231
+ `;
143232
+
143233
+ const vertexShader$5 = /* glsl */ `
143234
+ attribute vec2 vertex_position;
143235
+ void main(void) {
143236
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
143237
+ }
143238
+ `;
143239
+ const fragmentShader$5 = /* glsl */ `
143240
+ uniform highp usampler2D transformA; // splat center x, y, z
143241
+ uniform highp usampler2D splatTransform; // transform palette index
143242
+ uniform sampler2D transformPalette; // palette of transforms
143243
+ uniform uvec2 splat_params; // splat texture width, num splats
143244
+
143245
+ uniform mat4 matrix_model;
143246
+ uniform mat4 matrix_viewProjection;
143247
+
143248
+ uniform uvec2 output_params; // output width, height
143249
+
143250
+ // 0: mask, 1: rect, 2: sphere
143251
+ uniform int mode;
143252
+
143253
+ // mask params
143254
+ uniform sampler2D mask; // mask in alpha channel
143255
+ uniform vec2 mask_params; // mask width, height
143256
+
143257
+ // rect params
143258
+ uniform vec4 rect_params; // rect x, y, width, height
143259
+
143260
+ // sphere params
143261
+ uniform vec4 sphere_params; // sphere x, y, z, radius
143262
+
143263
+ // box params
143264
+ uniform vec4 box_params; // box x, y, z
143265
+ uniform vec4 aabb_params; // len x, y, z
143266
+
143267
+ void main(void) {
143268
+ // calculate output id
143269
+ uvec2 outputUV = uvec2(gl_FragCoord);
143270
+ uint outputId = (outputUV.x + outputUV.y * output_params.x) * 4u;
143271
+
143272
+ vec4 clr = vec4(0.0);
143273
+
143274
+ for (uint i = 0u; i < 4u; i++) {
143275
+ uint id = outputId + i;
143276
+
143277
+ if (id >= splat_params.y) {
143278
+ continue;
143279
+ }
143280
+
143281
+ // calculate splatUV
143282
+ ivec2 splatUV = ivec2(
143283
+ int(id % splat_params.x),
143284
+ int(id / splat_params.x)
143285
+ );
143286
+
143287
+ // read splat center
143288
+ vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
143289
+
143290
+ // apply optional per-splat transform
143291
+ uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
143292
+ if (transformIndex > 0u) {
143293
+ // read transform matrix
143294
+ int u = int(transformIndex % 512u) * 3;
143295
+ int v = int(transformIndex / 512u);
143296
+
143297
+ mat3x4 t;
143298
+ t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
143299
+ t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
143300
+ t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
143301
+
143302
+ center = vec4(center, 1.0) * t;
143303
+ }
143304
+
143305
+ // transform to clip space and discard if outside
143306
+ vec3 world = (matrix_model * vec4(center, 1.0)).xyz;
143307
+ vec4 clip = matrix_viewProjection * vec4(world, 1.0);
143308
+ vec3 ndc = clip.xyz / clip.w;
143309
+
143310
+ // skip offscreen fragments
143311
+ if (!any(greaterThan(abs(ndc), vec3(1.0)))) {
143312
+ if (mode == 0) {
143313
+ // select by mask
143314
+ ivec2 maskUV = ivec2((ndc.xy * vec2(0.5, -0.5) + 0.5) * mask_params);
143315
+ clr[i] = texelFetch(mask, maskUV, 0).a < 1.0 ? 0.0 : 1.0;
143316
+ } else if (mode == 1) {
143317
+ // select by rect
143318
+ clr[i] = all(greaterThan(ndc.xy * vec2(1.0, -1.0), rect_params.xy)) && all(lessThan(ndc.xy * vec2(1.0, -1.0), rect_params.zw)) ? 1.0 : 0.0;
143319
+ } else if (mode == 2) {
143320
+ // select by sphere
143321
+ clr[i] = length(world - sphere_params.xyz) < sphere_params.w ? 1.0 : 0.0;
143322
+ } else if (mode == 3) {
143323
+ // select by box
143324
+ vec3 relativePosition = world - box_params.xyz;
143325
+ bool isInsideCube = true;
143326
+ if (relativePosition.x < -aabb_params.x || relativePosition.x > aabb_params.x) {
143327
+ isInsideCube = false;
143328
+ }
143329
+ if (relativePosition.y < -aabb_params.y || relativePosition.y > aabb_params.y) {
143330
+ isInsideCube = false;
143331
+ }
143332
+ if (relativePosition.z < -aabb_params.z || relativePosition.z > aabb_params.z) {
143333
+ isInsideCube = false;
143334
+ }
143335
+ clr[i] = isInsideCube ? 1.0 : 0.0;
143336
+ }
143337
+ }
143338
+ }
143339
+
143340
+ gl_FragColor = clr;
143341
+ }
143342
+ `;
143343
+
143344
+ const vertexShader$4 = /* glsl */ `
143345
+ attribute vec2 vertex_position;
143346
+ void main(void) {
143347
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
143348
+ }
143349
+ `;
143350
+ const fragmentShader$4 = /* glsl */ `
143351
+ uniform highp usampler2D transformA; // splat center x, y, z
143352
+ uniform highp usampler2D splatTransform; // transform palette index
143353
+ uniform sampler2D transformPalette; // palette of transforms
143354
+ uniform ivec2 splat_params; // splat texture width, num splats
143355
+
143356
+ void main(void) {
143357
+ // calculate output id
143358
+ ivec2 splatUV = ivec2(gl_FragCoord);
143359
+
143360
+ // skip if splat index is out of bounds
143361
+ if (splatUV.x + splatUV.y * splat_params.x >= splat_params.y) {
143362
+ discard;
143363
+ }
143364
+
143365
+ // read splat center
143366
+ vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
143367
+
143368
+ // apply optional per-splat transform
143369
+ uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
143370
+ if (transformIndex > 0u) {
143371
+ // read transform matrix
143372
+ int u = int(transformIndex % 512u) * 3;
143373
+ int v = int(transformIndex / 512u);
143374
+
143375
+ mat3x4 t;
143376
+ t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
143377
+ t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
143378
+ t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
143379
+
143380
+ center = vec4(center, 1.0) * t;
143381
+ }
143382
+
143383
+ gl_FragColor = vec4(center, 0.0);
143384
+ }
142755
143385
  `;
142756
143386
 
142757
143387
  const v1 = new Vec3();
@@ -142788,18 +143418,18 @@ class DataProcessor {
142788
143418
  attributes: {
142789
143419
  vertex_position: SEMANTIC_POSITION
142790
143420
  },
142791
- vertexGLSL: `
142792
- attribute vec2 vertex_position;
142793
- void main(void) {
142794
- gl_Position = vec4(vertex_position, 0.0, 1.0);
142795
- }
143421
+ vertexGLSL: `
143422
+ attribute vec2 vertex_position;
143423
+ void main(void) {
143424
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
143425
+ }
142796
143426
  `,
142797
- fragmentGLSL: `
142798
- uniform sampler2D colorTex;
142799
- void main(void) {
142800
- ivec2 texel = ivec2(gl_FragCoord.xy);
142801
- gl_FragColor = texelFetch(colorTex, texel, 0);
142802
- }
143427
+ fragmentGLSL: `
143428
+ uniform sampler2D colorTex;
143429
+ void main(void) {
143430
+ ivec2 texel = ivec2(gl_FragCoord.xy);
143431
+ gl_FragColor = texelFetch(colorTex, texel, 0);
143432
+ }
142803
143433
  `
142804
143434
  });
142805
143435
  // intersection test
@@ -144038,176 +144668,176 @@ class Camera extends Element$1 {
144038
144668
  }
144039
144669
  }
144040
144670
 
144041
- const vertexShader$3 = /* glsl*/ `
144042
- uniform vec3 near_origin;
144043
- uniform vec3 near_x;
144044
- uniform vec3 near_y;
144045
-
144046
- uniform vec3 far_origin;
144047
- uniform vec3 far_x;
144048
- uniform vec3 far_y;
144049
-
144050
- attribute vec2 vertex_position;
144051
-
144052
- varying vec3 worldFar;
144053
- varying vec3 worldNear;
144054
-
144055
- void main(void) {
144056
- gl_Position = vec4(vertex_position, 0.0, 1.0);
144057
-
144058
- vec2 p = vertex_position * 0.5 + 0.5;
144059
- worldNear = near_origin + near_x * p.x + near_y * p.y;
144060
- worldFar = far_origin + far_x * p.x + far_y * p.y;
144061
- }
144062
- `;
144063
- const fragmentShader$3 = /* glsl*/ `
144064
- uniform vec3 view_position;
144065
- uniform mat4 matrix_viewProjection;
144066
- uniform sampler2D blueNoiseTex32;
144067
-
144068
- uniform int plane; // 0: x (yz), 1: y (xz), 2: z (xy)
144069
-
144070
- vec4 planes[3] = vec4[3](
144071
- vec4(1.0, 0.0, 0.0, 0.0),
144072
- vec4(0.0, 1.0, 0.0, 0.0),
144073
- vec4(0.0, 0.0, 1.0, 0.0)
144074
- );
144075
-
144076
- vec3 colors[3] = vec3[3](
144077
- vec3(1.0, 0.2, 0.2),
144078
- vec3(0.2, 1.0, 0.2),
144079
- vec3(0.2, 0.2, 1.0)
144080
- );
144081
-
144082
- int axis0[3] = int[3](1, 0, 0);
144083
- int axis1[3] = int[3](2, 2, 1);
144084
-
144085
- varying vec3 worldNear;
144086
- varying vec3 worldFar;
144087
-
144088
- bool intersectPlane(inout float t, vec3 pos, vec3 dir, vec4 plane) {
144089
- float d = dot(dir, plane.xyz);
144090
- if (abs(d) < 1e-06) {
144091
- return false;
144092
- }
144093
-
144094
- float n = -(dot(pos, plane.xyz) + plane.w) / d;
144095
- if (n < 0.0) {
144096
- return false;
144097
- }
144098
-
144099
- t = n;
144100
-
144101
- return true;
144102
- }
144103
-
144104
- // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c
144105
- float pristineGrid(in vec2 uv, in vec2 ddx, in vec2 ddy, vec2 lineWidth) {
144106
- vec2 uvDeriv = vec2(length(vec2(ddx.x, ddy.x)), length(vec2(ddx.y, ddy.y)));
144107
- bvec2 invertLine = bvec2(lineWidth.x > 0.5, lineWidth.y > 0.5);
144108
- vec2 targetWidth = vec2(
144109
- invertLine.x ? 1.0 - lineWidth.x : lineWidth.x,
144110
- invertLine.y ? 1.0 - lineWidth.y : lineWidth.y
144111
- );
144112
- vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5));
144113
- vec2 lineAA = uvDeriv * 1.5;
144114
- vec2 gridUV = abs(fract(uv) * 2.0 - 1.0);
144115
- gridUV.x = invertLine.x ? gridUV.x : 1.0 - gridUV.x;
144116
- gridUV.y = invertLine.y ? gridUV.y : 1.0 - gridUV.y;
144117
- vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
144118
-
144119
- grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0);
144120
- grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0));
144121
- grid2.x = invertLine.x ? 1.0 - grid2.x : grid2.x;
144122
- grid2.y = invertLine.y ? 1.0 - grid2.y : grid2.y;
144123
-
144124
- return mix(grid2.x, 1.0, grid2.y);
144125
- }
144126
-
144127
- float calcDepth(vec3 p) {
144128
- vec4 v = matrix_viewProjection * vec4(p, 1.0);
144129
- return (v.z / v.w) * 0.5 + 0.5;
144130
- }
144131
-
144132
- bool writeDepth(float alpha) {
144133
- vec2 uv = fract(gl_FragCoord.xy / 32.0);
144134
- float noise = texture2DLod(blueNoiseTex32, uv, 0.0).y;
144135
- return alpha > noise;
144136
- }
144137
-
144138
- void main(void) {
144139
- vec3 p = worldNear;
144140
- vec3 v = normalize(worldFar - worldNear);
144141
-
144142
- // intersect ray with the world xz plane
144143
- float t;
144144
- if (!intersectPlane(t, p, v, planes[plane])) {
144145
- discard;
144146
- }
144147
-
144148
- // calculate grid intersection
144149
- vec3 worldPos = p + v * t;
144150
- vec2 pos = plane == 0 ? worldPos.yz : (plane == 1 ? worldPos.xz : worldPos.xy);
144151
- vec2 ddx = dFdx(pos);
144152
- vec2 ddy = dFdy(pos);
144153
-
144154
- float epsilon = 1.0 / 255.0;
144155
-
144156
- // calculate fade
144157
- float fade = 1.0 - smoothstep(400.0, 1000.0, length(worldPos - view_position));
144158
- if (fade < epsilon) {
144159
- discard;
144160
- }
144161
-
144162
- vec2 levelPos;
144163
- float levelSize;
144164
- float levelAlpha;
144165
-
144166
- // 10m grid with colored main axes
144167
- levelPos = pos * 0.1;
144168
- levelSize = 2.0 / 1000.0;
144169
- levelAlpha = pristineGrid(levelPos, ddx * 0.1, ddy * 0.1, vec2(levelSize)) * fade;
144170
- if (levelAlpha > epsilon) {
144171
- vec3 color;
144172
- vec2 loc = abs(levelPos);
144173
- if (loc.x < levelSize) {
144174
- if (loc.y < levelSize) {
144175
- color = vec3(1.0);
144176
- } else {
144177
- color = colors[axis1[plane]];
144178
- }
144179
- } else if (loc.y < levelSize) {
144180
- color = colors[axis0[plane]];
144181
- } else {
144182
- color = vec3(0.9);
144183
- }
144184
- gl_FragColor = vec4(color, levelAlpha);
144185
- gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144186
- return;
144187
- }
144188
-
144189
- // 1m grid
144190
- levelPos = pos;
144191
- levelSize = 1.0 / 100.0;
144192
- levelAlpha = pristineGrid(levelPos, ddx, ddy, vec2(levelSize)) * fade;
144193
- if (levelAlpha > epsilon) {
144194
- gl_FragColor = vec4(vec3(0.7), levelAlpha);
144195
- gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144196
- return;
144197
- }
144198
-
144199
- // 0.1m grid
144200
- levelPos = pos * 10.0;
144201
- levelSize = 1.0 / 100.0;
144202
- levelAlpha = pristineGrid(levelPos, ddx * 10.0, ddy * 10.0, vec2(levelSize)) * fade;
144203
- if (levelAlpha > epsilon) {
144204
- gl_FragColor = vec4(vec3(0.7), levelAlpha);
144205
- gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144206
- return;
144207
- }
144208
-
144209
- discard;
144210
- }
144671
+ const vertexShader$3 = /* glsl*/ `
144672
+ uniform vec3 near_origin;
144673
+ uniform vec3 near_x;
144674
+ uniform vec3 near_y;
144675
+
144676
+ uniform vec3 far_origin;
144677
+ uniform vec3 far_x;
144678
+ uniform vec3 far_y;
144679
+
144680
+ attribute vec2 vertex_position;
144681
+
144682
+ varying vec3 worldFar;
144683
+ varying vec3 worldNear;
144684
+
144685
+ void main(void) {
144686
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
144687
+
144688
+ vec2 p = vertex_position * 0.5 + 0.5;
144689
+ worldNear = near_origin + near_x * p.x + near_y * p.y;
144690
+ worldFar = far_origin + far_x * p.x + far_y * p.y;
144691
+ }
144692
+ `;
144693
+ const fragmentShader$3 = /* glsl*/ `
144694
+ uniform vec3 view_position;
144695
+ uniform mat4 matrix_viewProjection;
144696
+ uniform sampler2D blueNoiseTex32;
144697
+
144698
+ uniform int plane; // 0: x (yz), 1: y (xz), 2: z (xy)
144699
+
144700
+ vec4 planes[3] = vec4[3](
144701
+ vec4(1.0, 0.0, 0.0, 0.0),
144702
+ vec4(0.0, 1.0, 0.0, 0.0),
144703
+ vec4(0.0, 0.0, 1.0, 0.0)
144704
+ );
144705
+
144706
+ vec3 colors[3] = vec3[3](
144707
+ vec3(1.0, 0.2, 0.2),
144708
+ vec3(0.2, 1.0, 0.2),
144709
+ vec3(0.2, 0.2, 1.0)
144710
+ );
144711
+
144712
+ int axis0[3] = int[3](1, 0, 0);
144713
+ int axis1[3] = int[3](2, 2, 1);
144714
+
144715
+ varying vec3 worldNear;
144716
+ varying vec3 worldFar;
144717
+
144718
+ bool intersectPlane(inout float t, vec3 pos, vec3 dir, vec4 plane) {
144719
+ float d = dot(dir, plane.xyz);
144720
+ if (abs(d) < 1e-06) {
144721
+ return false;
144722
+ }
144723
+
144724
+ float n = -(dot(pos, plane.xyz) + plane.w) / d;
144725
+ if (n < 0.0) {
144726
+ return false;
144727
+ }
144728
+
144729
+ t = n;
144730
+
144731
+ return true;
144732
+ }
144733
+
144734
+ // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c
144735
+ float pristineGrid(in vec2 uv, in vec2 ddx, in vec2 ddy, vec2 lineWidth) {
144736
+ vec2 uvDeriv = vec2(length(vec2(ddx.x, ddy.x)), length(vec2(ddx.y, ddy.y)));
144737
+ bvec2 invertLine = bvec2(lineWidth.x > 0.5, lineWidth.y > 0.5);
144738
+ vec2 targetWidth = vec2(
144739
+ invertLine.x ? 1.0 - lineWidth.x : lineWidth.x,
144740
+ invertLine.y ? 1.0 - lineWidth.y : lineWidth.y
144741
+ );
144742
+ vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5));
144743
+ vec2 lineAA = uvDeriv * 1.5;
144744
+ vec2 gridUV = abs(fract(uv) * 2.0 - 1.0);
144745
+ gridUV.x = invertLine.x ? gridUV.x : 1.0 - gridUV.x;
144746
+ gridUV.y = invertLine.y ? gridUV.y : 1.0 - gridUV.y;
144747
+ vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
144748
+
144749
+ grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0);
144750
+ grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0));
144751
+ grid2.x = invertLine.x ? 1.0 - grid2.x : grid2.x;
144752
+ grid2.y = invertLine.y ? 1.0 - grid2.y : grid2.y;
144753
+
144754
+ return mix(grid2.x, 1.0, grid2.y);
144755
+ }
144756
+
144757
+ float calcDepth(vec3 p) {
144758
+ vec4 v = matrix_viewProjection * vec4(p, 1.0);
144759
+ return (v.z / v.w) * 0.5 + 0.5;
144760
+ }
144761
+
144762
+ bool writeDepth(float alpha) {
144763
+ vec2 uv = fract(gl_FragCoord.xy / 32.0);
144764
+ float noise = texture2DLod(blueNoiseTex32, uv, 0.0).y;
144765
+ return alpha > noise;
144766
+ }
144767
+
144768
+ void main(void) {
144769
+ vec3 p = worldNear;
144770
+ vec3 v = normalize(worldFar - worldNear);
144771
+
144772
+ // intersect ray with the world xz plane
144773
+ float t;
144774
+ if (!intersectPlane(t, p, v, planes[plane])) {
144775
+ discard;
144776
+ }
144777
+
144778
+ // calculate grid intersection
144779
+ vec3 worldPos = p + v * t;
144780
+ vec2 pos = plane == 0 ? worldPos.yz : (plane == 1 ? worldPos.xz : worldPos.xy);
144781
+ vec2 ddx = dFdx(pos);
144782
+ vec2 ddy = dFdy(pos);
144783
+
144784
+ float epsilon = 1.0 / 255.0;
144785
+
144786
+ // calculate fade
144787
+ float fade = 1.0 - smoothstep(400.0, 1000.0, length(worldPos - view_position));
144788
+ if (fade < epsilon) {
144789
+ discard;
144790
+ }
144791
+
144792
+ vec2 levelPos;
144793
+ float levelSize;
144794
+ float levelAlpha;
144795
+
144796
+ // 10m grid with colored main axes
144797
+ levelPos = pos * 0.1;
144798
+ levelSize = 2.0 / 1000.0;
144799
+ levelAlpha = pristineGrid(levelPos, ddx * 0.1, ddy * 0.1, vec2(levelSize)) * fade;
144800
+ if (levelAlpha > epsilon) {
144801
+ vec3 color;
144802
+ vec2 loc = abs(levelPos);
144803
+ if (loc.x < levelSize) {
144804
+ if (loc.y < levelSize) {
144805
+ color = vec3(1.0);
144806
+ } else {
144807
+ color = colors[axis1[plane]];
144808
+ }
144809
+ } else if (loc.y < levelSize) {
144810
+ color = colors[axis0[plane]];
144811
+ } else {
144812
+ color = vec3(0.9);
144813
+ }
144814
+ gl_FragColor = vec4(color, levelAlpha);
144815
+ gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144816
+ return;
144817
+ }
144818
+
144819
+ // 1m grid
144820
+ levelPos = pos;
144821
+ levelSize = 1.0 / 100.0;
144822
+ levelAlpha = pristineGrid(levelPos, ddx, ddy, vec2(levelSize)) * fade;
144823
+ if (levelAlpha > epsilon) {
144824
+ gl_FragColor = vec4(vec3(0.7), levelAlpha);
144825
+ gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144826
+ return;
144827
+ }
144828
+
144829
+ // 0.1m grid
144830
+ levelPos = pos * 10.0;
144831
+ levelSize = 1.0 / 100.0;
144832
+ levelAlpha = pristineGrid(levelPos, ddx * 10.0, ddy * 10.0, vec2(levelSize)) * fade;
144833
+ if (levelAlpha > epsilon) {
144834
+ gl_FragColor = vec4(vec3(0.7), levelAlpha);
144835
+ gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144836
+ return;
144837
+ }
144838
+
144839
+ discard;
144840
+ }
144211
144841
  `;
144212
144842
 
144213
144843
  const resolve = (scope, values) => {
@@ -144278,36 +144908,36 @@ class InfiniteGrid extends Element$1 {
144278
144908
  }
144279
144909
  }
144280
144910
 
144281
- const vertexShader$2 = /* glsl*/ `
144282
- attribute vec2 vertex_position;
144283
- void main(void) {
144284
- gl_Position = vec4(vertex_position, 0.0, 1.0);
144285
- }
144286
- `;
144287
- const fragmentShader$2 = /* glsl*/ `
144288
- uniform sampler2D outlineTexture;
144289
- uniform float alphaCutoff;
144290
- uniform vec4 clr;
144291
-
144292
- void main(void) {
144293
- ivec2 texel = ivec2(gl_FragCoord.xy);
144294
-
144295
- // skip solid pixels
144296
- if (texelFetch(outlineTexture, texel, 0).a > alphaCutoff) {
144297
- discard;
144298
- }
144299
-
144300
- for (int x = -2; x <= 2; x++) {
144301
- for (int y = -2; y <= 2; y++) {
144302
- if ((x != 0) && (y != 0) && (texelFetch(outlineTexture, texel + ivec2(x, y), 0).a > alphaCutoff)) {
144303
- gl_FragColor = clr;
144304
- return;
144305
- }
144306
- }
144307
- }
144308
-
144309
- discard;
144310
- }
144911
+ const vertexShader$2 = /* glsl*/ `
144912
+ attribute vec2 vertex_position;
144913
+ void main(void) {
144914
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
144915
+ }
144916
+ `;
144917
+ const fragmentShader$2 = /* glsl*/ `
144918
+ uniform sampler2D outlineTexture;
144919
+ uniform float alphaCutoff;
144920
+ uniform vec4 clr;
144921
+
144922
+ void main(void) {
144923
+ ivec2 texel = ivec2(gl_FragCoord.xy);
144924
+
144925
+ // skip solid pixels
144926
+ if (texelFetch(outlineTexture, texel, 0).a > alphaCutoff) {
144927
+ discard;
144928
+ }
144929
+
144930
+ for (int x = -2; x <= 2; x++) {
144931
+ for (int y = -2; y <= 2; y++) {
144932
+ if ((x != 0) && (y != 0) && (texelFetch(outlineTexture, texel + ivec2(x, y), 0).a > alphaCutoff)) {
144933
+ gl_FragColor = clr;
144934
+ return;
144935
+ }
144936
+ }
144937
+ }
144938
+
144939
+ discard;
144940
+ }
144311
144941
  `;
144312
144942
 
144313
144943
  class Outline extends Element$1 {
@@ -144625,72 +145255,72 @@ class SceneState {
144625
145255
  }
144626
145256
  }
144627
145257
 
144628
- const vertexShader$1 = /* glsl */ `
144629
- attribute uint vertex_id;
144630
-
144631
- uniform mat4 matrix_model;
144632
- uniform mat4 matrix_viewProjection;
144633
-
144634
- uniform sampler2D splatState;
144635
- uniform highp usampler2D splatPosition;
144636
- uniform highp usampler2D splatTransform; // per-splat index into transform palette
144637
- uniform sampler2D transformPalette; // palette of transform matrices
144638
-
144639
- uniform uvec2 texParams;
144640
-
144641
- uniform float splatSize;
144642
- uniform vec4 selectedClr;
144643
- uniform vec4 unselectedClr;
144644
-
144645
- varying vec4 varying_color;
144646
-
144647
- // calculate the current splat index and uv
144648
- ivec2 calcSplatUV(uint index, uint width) {
144649
- return ivec2(int(index % width), int(index / width));
144650
- }
144651
-
144652
- void main(void) {
144653
- ivec2 splatUV = calcSplatUV(vertex_id, texParams.x);
144654
- uint splatState = uint(texelFetch(splatState, splatUV, 0).r * 255.0);
144655
-
144656
- if ((splatState & 6u) != 0u) {
144657
- // deleted or locked (4 or 2)
144658
- gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
144659
- gl_PointSize = 0.0;
144660
- } else {
144661
- mat4 model = matrix_model;
144662
-
144663
- // handle per-splat transform
144664
- uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
144665
- if (transformIndex > 0u) {
144666
- // read transform matrix
144667
- int u = int(transformIndex % 512u) * 3;
144668
- int v = int(transformIndex / 512u);
144669
-
144670
- mat4 t;
144671
- t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
144672
- t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
144673
- t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
144674
- t[3] = vec4(0.0, 0.0, 0.0, 1.0);
144675
-
144676
- model = matrix_model * transpose(t);
144677
- }
144678
-
144679
- varying_color = (splatState == 1u) ? selectedClr : unselectedClr;
144680
-
144681
- vec3 center = uintBitsToFloat(texelFetch(splatPosition, splatUV, 0).xyz);
144682
-
144683
- gl_Position = matrix_viewProjection * model * vec4(center, 1.0);
144684
- gl_PointSize = splatSize;
144685
- }
144686
- }
144687
- `;
144688
- const fragmentShader$1 = /* glsl */ `
144689
- varying vec4 varying_color;
144690
-
144691
- void main(void) {
144692
- gl_FragColor = varying_color;
144693
- }
145258
+ const vertexShader$1 = /* glsl */ `
145259
+ attribute uint vertex_id;
145260
+
145261
+ uniform mat4 matrix_model;
145262
+ uniform mat4 matrix_viewProjection;
145263
+
145264
+ uniform sampler2D splatState;
145265
+ uniform highp usampler2D splatPosition;
145266
+ uniform highp usampler2D splatTransform; // per-splat index into transform palette
145267
+ uniform sampler2D transformPalette; // palette of transform matrices
145268
+
145269
+ uniform uvec2 texParams;
145270
+
145271
+ uniform float splatSize;
145272
+ uniform vec4 selectedClr;
145273
+ uniform vec4 unselectedClr;
145274
+
145275
+ varying vec4 varying_color;
145276
+
145277
+ // calculate the current splat index and uv
145278
+ ivec2 calcSplatUV(uint index, uint width) {
145279
+ return ivec2(int(index % width), int(index / width));
145280
+ }
145281
+
145282
+ void main(void) {
145283
+ ivec2 splatUV = calcSplatUV(vertex_id, texParams.x);
145284
+ uint splatState = uint(texelFetch(splatState, splatUV, 0).r * 255.0);
145285
+
145286
+ if ((splatState & 6u) != 0u) {
145287
+ // deleted or locked (4 or 2)
145288
+ gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
145289
+ gl_PointSize = 0.0;
145290
+ } else {
145291
+ mat4 model = matrix_model;
145292
+
145293
+ // handle per-splat transform
145294
+ uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
145295
+ if (transformIndex > 0u) {
145296
+ // read transform matrix
145297
+ int u = int(transformIndex % 512u) * 3;
145298
+ int v = int(transformIndex / 512u);
145299
+
145300
+ mat4 t;
145301
+ t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
145302
+ t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
145303
+ t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
145304
+ t[3] = vec4(0.0, 0.0, 0.0, 1.0);
145305
+
145306
+ model = matrix_model * transpose(t);
145307
+ }
145308
+
145309
+ varying_color = (splatState == 1u) ? selectedClr : unselectedClr;
145310
+
145311
+ vec3 center = uintBitsToFloat(texelFetch(splatPosition, splatUV, 0).xyz);
145312
+
145313
+ gl_Position = matrix_viewProjection * model * vec4(center, 1.0);
145314
+ gl_PointSize = splatSize;
145315
+ }
145316
+ }
145317
+ `;
145318
+ const fragmentShader$1 = /* glsl */ `
145319
+ varying vec4 varying_color;
145320
+
145321
+ void main(void) {
145322
+ gl_FragColor = varying_color;
145323
+ }
144694
145324
  `;
144695
145325
 
144696
145326
  class SplatOverlay extends Element$1 {
@@ -144779,19 +145409,19 @@ class SplatOverlay extends Element$1 {
144779
145409
  }
144780
145410
  }
144781
145411
 
144782
- const vertexShader = /* glsl*/ `
144783
- attribute vec2 vertex_position;
144784
- void main(void) {
144785
- gl_Position = vec4(vertex_position, 0.0, 1.0);
144786
- }
145412
+ const vertexShader = /* glsl*/ `
145413
+ attribute vec2 vertex_position;
145414
+ void main(void) {
145415
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
145416
+ }
144787
145417
  `;
144788
- const fragmentShader = /* glsl*/ `
144789
- uniform sampler2D blitTexture;
144790
- void main(void) {
144791
- ivec2 texel = ivec2(gl_FragCoord.xy);
144792
-
144793
- gl_FragColor = texelFetch(blitTexture, texel, 0);
144794
- }
145418
+ const fragmentShader = /* glsl*/ `
145419
+ uniform sampler2D blitTexture;
145420
+ void main(void) {
145421
+ ivec2 texel = ivec2(gl_FragCoord.xy);
145422
+
145423
+ gl_FragColor = texelFetch(blitTexture, texel, 0);
145424
+ }
144795
145425
  `;
144796
145426
 
144797
145427
  class Underlay extends Element$1 {
@@ -146756,7 +147386,7 @@ class SupersplatAdapter {
146756
147386
  const { config } = this.scene;
146757
147387
  const state = this.viewerEventState;
146758
147388
  // Colors and view settings from scene config
146759
- const selectedClr = config.selectedClr ?? { r: 1, g: 1, b: 0, a: 1 };
147389
+ const selectedClr = config.selectedClr ?? { r: 1, g: 0.5, b: 0, a: 1 };
146760
147390
  const unselectedClr = config.unselectedClr ?? { r: 0, g: 0, b: 1, a: 0.5 };
146761
147391
  const lockedClr = config.lockedClr ?? { r: 0, g: 0, b: 0, a: 0.05 };
146762
147392
  const bgClr = config.bgClr ?? { r: 0, g: 0, b: 0, a: 1 };
@@ -147068,6 +147698,7 @@ class SplatViewerCore {
147068
147698
  };
147069
147699
  this.enableStats = false;
147070
147700
  this.autoFocus = true;
147701
+ this.previewMode = false;
147071
147702
  this.isLoading = false;
147072
147703
  this.hasModel = false;
147073
147704
  this.error = null;
@@ -147120,6 +147751,11 @@ class SplatViewerCore {
147120
147751
  this.canvas = options.canvas || null;
147121
147752
  this.enableStats = options.enableStats || false;
147122
147753
  this.autoFocus = options.autoFocus !== false;
147754
+ this.previewMode = options.previewMode || false;
147755
+ // In preview mode, always enable auto-focus
147756
+ if (this.previewMode) {
147757
+ this.autoFocus = true;
147758
+ }
147123
147759
  this._navigationCubeConfig = options.navigationCube || null;
147124
147760
  if (options.onStatsUpdate !== undefined) {
147125
147761
  this._onStatsUpdate = options.onStatsUpdate;
@@ -147150,11 +147786,20 @@ class SplatViewerCore {
147150
147786
  const emitEvent = (type, detail) => {
147151
147787
  this.emit({ type, detail });
147152
147788
  };
147153
- this._supersplat = new SupersplatAdapter(this.canvas, this._navigationCubeConfig, emitEvent);
147789
+ this._supersplat = new SupersplatAdapter(this.canvas,
147790
+ // Disable navigation cube in preview mode
147791
+ this.previewMode ? null : this._navigationCubeConfig, emitEvent);
147154
147792
  this._supersplatReady = this._supersplat.init();
147155
147793
  this._supersplatReady
147156
147794
  ?.then(() => {
147157
- this._setupFlyCameraForSupersplat();
147795
+ // Disable camera controls in preview mode
147796
+ if (this.previewMode && this._supersplat) {
147797
+ this._supersplat.setCameraControlsEnabled?.(false);
147798
+ }
147799
+ // Only set up fly camera if not in preview mode
147800
+ if (!this.previewMode) {
147801
+ this._setupFlyCameraForSupersplat();
147802
+ }
147158
147803
  })
147159
147804
  .catch(error => {
147160
147805
  console.error('SplatViewerCore.init: Failed to set up fly camera for supersplat path', error);
@@ -147190,6 +147835,9 @@ class SplatViewerCore {
147190
147835
  this.app = ctx.app;
147191
147836
  this.entities.camera = ctx.camera;
147192
147837
  const cameraAny = this.entities.camera;
147838
+ // SuperSplat's PCApp omits ScriptComponentSystem, so `addComponent('script')`
147839
+ // will not produce `camera.script.create()`. We keep the attempt (in case
147840
+ // the underlying app changes), but also support a controller-based fallback.
147193
147841
  if (cameraAny &&
147194
147842
  !cameraAny.script &&
147195
147843
  typeof cameraAny.addComponent === 'function') {
@@ -147215,16 +147863,25 @@ class SplatViewerCore {
147215
147863
  minHeight: DEFAULT_FLY_CAMERA_CONFIG.minHeight,
147216
147864
  maxHeight: DEFAULT_FLY_CAMERA_CONFIG.maxHeight,
147217
147865
  };
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?.();
147866
+ // Prefer script-based fly when available; fallback to controller otherwise.
147867
+ const canCreateScript = typeof cameraAny?.script?.create === 'function';
147868
+ if (canCreateScript) {
147869
+ const created = cameraAny.script.create('flyCamera', {
147870
+ attributes: flyAttributes,
147871
+ });
147872
+ this._fly = created;
147873
+ if (this._fly) {
147874
+ ;
147875
+ this._fly.emitFlyEvent = (type, detail) => {
147876
+ this.emit({ type: type, detail });
147877
+ };
147878
+ this._fly.deactivate?.();
147879
+ }
147880
+ }
147881
+ else {
147882
+ const controller = new FlyCameraController(ctx.app, cameraAny, (type, detail) => this.emit({ type: type, detail }), flyAttributes);
147883
+ controller.deactivate();
147884
+ this._fly = controller;
147228
147885
  }
147229
147886
  this._cameraMode = 'orbit';
147230
147887
  }
@@ -149075,6 +149732,11 @@ class SplatViewerCore {
149075
149732
  // Camera Mode / Fly Camera API
149076
149733
  // ==========================================
149077
149734
  setCameraMode(mode) {
149735
+ // Prevent camera mode changes in preview mode
149736
+ if (this.previewMode) {
149737
+ console.warn('SplatViewerCore.setCameraMode: Camera controls are disabled in preview mode');
149738
+ return;
149739
+ }
149078
149740
  // supersplat-core path: manage mode switching explicitly (camera entity is updated by supersplat-core each frame)
149079
149741
  if (this._supersplat) {
149080
149742
  const prev = this._cameraMode;
@@ -149093,17 +149755,33 @@ class SplatViewerCore {
149093
149755
  // Stop supersplat orbit updates + input
149094
149756
  this._supersplat.setCameraControlsEnabled(false);
149095
149757
  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;
149758
+ // Preserve camera position and rotation when switching to fly mode
149759
+ if (this._fly) {
149760
+ // For FlyCameraController (fallback path)
149761
+ if (typeof this._fly.syncFromEntity === 'function') {
149762
+ this._fly.syncFromEntity();
149763
+ }
149764
+ else {
149765
+ // For FlyCameraScript (legacy path)
149766
+ try {
149767
+ const pos = this.entities.camera?.getPosition?.();
149768
+ if (pos) {
149769
+ const posVec = pos.clone
149770
+ ? pos.clone()
149771
+ : new Vec3(pos.x || 0, pos.y || 0, pos.z || 0);
149772
+ this.entities.camera?.setPosition?.(posVec);
149773
+ }
149774
+ const euler = this.entities.camera?.getEulerAngles?.();
149775
+ if (euler) {
149776
+ ;
149777
+ this._fly._pitch = euler.x || 0;
149778
+ this._fly._yaw = euler.y || 0;
149779
+ }
149780
+ }
149781
+ catch {
149782
+ // ignore
149783
+ }
149103
149784
  }
149104
- }
149105
- catch {
149106
- // ignore
149107
149785
  }
149108
149786
  this._fly?.activate?.();
149109
149787
  this._cameraMode = 'fly';
@@ -149220,9 +149898,48 @@ class SplatViewerCore {
149220
149898
  this._cameraModeManager?.setFlyConfig(config);
149221
149899
  }
149222
149900
  getFlyCameraConfig() {
149901
+ // SuperSplat path: fly is either a script instance or controller fallback
149902
+ if (this._supersplat && this._fly) {
149903
+ try {
149904
+ const flyAny = this._fly;
149905
+ const cfg = {
149906
+ moveSpeed: flyAny.moveSpeed,
149907
+ fastSpeedMultiplier: flyAny.fastSpeedMultiplier,
149908
+ slowSpeedMultiplier: flyAny.slowSpeedMultiplier,
149909
+ lookSensitivity: flyAny.lookSensitivity,
149910
+ invertY: !!flyAny.invertY,
149911
+ keyBindings: { ...(flyAny.keyBindings || {}) },
149912
+ smoothing: flyAny.smoothing,
149913
+ friction: flyAny.friction,
149914
+ enableCollision: !!flyAny.enableCollision,
149915
+ minHeight: flyAny.minHeight ?? null,
149916
+ maxHeight: flyAny.maxHeight ?? null,
149917
+ };
149918
+ return cfg;
149919
+ }
149920
+ catch {
149921
+ return null;
149922
+ }
149923
+ }
149223
149924
  return this._cameraModeManager?.getFlyConfig() || null;
149224
149925
  }
149225
149926
  getFlyCameraState() {
149927
+ // SuperSplat path: fly is either a script instance or controller fallback
149928
+ if (this._supersplat && this._fly?.getState) {
149929
+ try {
149930
+ const state = this._fly.getState();
149931
+ return {
149932
+ mode: this._cameraMode,
149933
+ position: state.position,
149934
+ rotation: state.rotation,
149935
+ velocity: state.velocity,
149936
+ isMoving: state.isMoving,
149937
+ };
149938
+ }
149939
+ catch {
149940
+ return null;
149941
+ }
149942
+ }
149226
149943
  return this._cameraModeManager?.getFlyState() || null;
149227
149944
  }
149228
149945
  _setupScene() {
@@ -149274,8 +149991,8 @@ class SplatViewerCore {
149274
149991
  panSensitivity: 1.0,
149275
149992
  zoomSensitivity: 0.1,
149276
149993
  };
149277
- // Add navigation cube configuration if available
149278
- if (this._navigationCubeConfig) {
149994
+ // Add navigation cube configuration if available (but not in preview mode)
149995
+ if (this._navigationCubeConfig && !this.previewMode) {
149279
149996
  orbitAttributes.enableNavigationCube =
149280
149997
  this._navigationCubeConfig.enabled || false;
149281
149998
  this.entities.camera._navigationCubeConfig =
@@ -149290,48 +150007,57 @@ class SplatViewerCore {
149290
150007
  detail: { type: interactionType },
149291
150008
  });
149292
150009
  };
150010
+ // Disable camera controls in preview mode
150011
+ if (this.previewMode && this._orbit) {
150012
+ const orbitAny = this._orbit;
150013
+ if (typeof orbitAny.setEnabled === 'function') {
150014
+ orbitAny.setEnabled(false);
150015
+ }
150016
+ }
149293
150017
  this.entities.camera.setPosition(0, 0, 10);
149294
150018
  this.entities.camera.lookAt(Vec3.ZERO);
149295
150019
  // ==============================
149296
- // Setup fly camera (disabled by default)
150020
+ // Setup fly camera (disabled by default, skipped in preview mode)
149297
150021
  // ==============================
149298
- try {
149299
- registerFlyCameraScript();
149300
- // Ensure script component exists (created above)
149301
- const flyAttributes = {
149302
- moveSpeed: DEFAULT_FLY_CAMERA_CONFIG.moveSpeed,
149303
- fastSpeedMultiplier: DEFAULT_FLY_CAMERA_CONFIG.fastSpeedMultiplier,
149304
- slowSpeedMultiplier: DEFAULT_FLY_CAMERA_CONFIG.slowSpeedMultiplier,
149305
- lookSensitivity: DEFAULT_FLY_CAMERA_CONFIG.lookSensitivity,
149306
- invertY: DEFAULT_FLY_CAMERA_CONFIG.invertY,
149307
- keyBindings: DEFAULT_FLY_CAMERA_CONFIG.keyBindings,
149308
- smoothing: DEFAULT_FLY_CAMERA_CONFIG.smoothing,
149309
- friction: DEFAULT_FLY_CAMERA_CONFIG.friction,
149310
- enableCollision: DEFAULT_FLY_CAMERA_CONFIG.enableCollision,
149311
- minHeight: DEFAULT_FLY_CAMERA_CONFIG.minHeight,
149312
- maxHeight: DEFAULT_FLY_CAMERA_CONFIG.maxHeight,
149313
- };
149314
- this._fly = this.entities.camera.script.create('flyCamera', {
149315
- attributes: flyAttributes,
149316
- });
149317
- // Wire event emission to core
149318
- if (this._fly) {
149319
- ;
149320
- this._fly.emitFlyEvent = (type, detail) => {
149321
- this.emit({ type: type, detail });
150022
+ if (!this.previewMode) {
150023
+ try {
150024
+ registerFlyCameraScript();
150025
+ // Ensure script component exists (created above)
150026
+ const flyAttributes = {
150027
+ moveSpeed: DEFAULT_FLY_CAMERA_CONFIG.moveSpeed,
150028
+ fastSpeedMultiplier: DEFAULT_FLY_CAMERA_CONFIG.fastSpeedMultiplier,
150029
+ slowSpeedMultiplier: DEFAULT_FLY_CAMERA_CONFIG.slowSpeedMultiplier,
150030
+ lookSensitivity: DEFAULT_FLY_CAMERA_CONFIG.lookSensitivity,
150031
+ invertY: DEFAULT_FLY_CAMERA_CONFIG.invertY,
150032
+ keyBindings: DEFAULT_FLY_CAMERA_CONFIG.keyBindings,
150033
+ smoothing: DEFAULT_FLY_CAMERA_CONFIG.smoothing,
150034
+ friction: DEFAULT_FLY_CAMERA_CONFIG.friction,
150035
+ enableCollision: DEFAULT_FLY_CAMERA_CONFIG.enableCollision,
150036
+ minHeight: DEFAULT_FLY_CAMERA_CONFIG.minHeight,
150037
+ maxHeight: DEFAULT_FLY_CAMERA_CONFIG.maxHeight,
149322
150038
  };
150039
+ this._fly = this.entities.camera.script.create('flyCamera', {
150040
+ attributes: flyAttributes,
150041
+ });
150042
+ // Wire event emission to core
150043
+ if (this._fly) {
150044
+ ;
150045
+ this._fly.emitFlyEvent = (type, detail) => {
150046
+ this.emit({ type: type, detail });
150047
+ };
150048
+ }
150049
+ // Deactivate fly by default; orbit is the initial mode
150050
+ if (this._fly?.deactivate) {
150051
+ this._fly.deactivate();
150052
+ }
150053
+ // Initialize camera mode manager
150054
+ this._cameraModeManager = new CameraModeManager(this.app, this.entities.camera, this._orbit, this._fly, (eventType, detail) => {
150055
+ this.emit({ type: eventType, detail });
150056
+ }, 'orbit');
149323
150057
  }
149324
- // Deactivate fly by default; orbit is the initial mode
149325
- if (this._fly?.deactivate) {
149326
- this._fly.deactivate();
150058
+ catch (e) {
150059
+ console.warn('Failed to set up fly camera', e);
149327
150060
  }
149328
- // Initialize camera mode manager
149329
- this._cameraModeManager = new CameraModeManager(this.app, this.entities.camera, this._orbit, this._fly, (eventType, detail) => {
149330
- this.emit({ type: eventType, detail });
149331
- }, 'orbit');
149332
- }
149333
- catch (e) {
149334
- console.warn('Failed to set up fly camera', e);
149335
150061
  }
149336
150062
  }
149337
150063
  _setupStats() {
@@ -149576,6 +150302,7 @@ class SplatViewerElement extends HTMLElement {
149576
150302
  'enable-stats',
149577
150303
  'auto-focus',
149578
150304
  'max-splats',
150305
+ 'preview-mode',
149579
150306
  'camera-position',
149580
150307
  'camera-target',
149581
150308
  'orbit-sensitivity',
@@ -149606,6 +150333,9 @@ class SplatViewerElement extends HTMLElement {
149606
150333
  get autoFocus() {
149607
150334
  return this.hasAttribute('auto-focus');
149608
150335
  }
150336
+ get previewMode() {
150337
+ return this.hasAttribute('preview-mode');
150338
+ }
149609
150339
  get maxSplats() {
149610
150340
  return this.getAttribute('max-splats');
149611
150341
  }
@@ -149683,6 +150413,14 @@ class SplatViewerElement extends HTMLElement {
149683
150413
  this.removeAttribute('auto-focus');
149684
150414
  }
149685
150415
  }
150416
+ set previewMode(value) {
150417
+ if (value) {
150418
+ this.setAttribute('preview-mode', '');
150419
+ }
150420
+ else {
150421
+ this.removeAttribute('preview-mode');
150422
+ }
150423
+ }
149686
150424
  set maxSplats(value) {
149687
150425
  if (value === null) {
149688
150426
  this.removeAttribute('max-splats');
@@ -149864,6 +150602,7 @@ class SplatViewerElement extends HTMLElement {
149864
150602
  return value.length > 0;
149865
150603
  case 'enable-stats':
149866
150604
  case 'auto-focus':
150605
+ case 'preview-mode':
149867
150606
  case 'enable-navigation-cube':
149868
150607
  // Boolean attributes - any value is valid
149869
150608
  return true;
@@ -150810,7 +151549,7 @@ class SplatViewerElement extends HTMLElement {
150810
151549
  if (!this._core) {
150811
151550
  throw new Error('SplatViewerElement: Core not initialized. Call connectedCallback first.');
150812
151551
  }
150813
- return this._core.selectSplatsInSphere(center, radius, modelId, addToSelection);
151552
+ return this._core.selectSplatsInSphere(new Vec3(center.x, center.y, center.z), radius, modelId, addToSelection);
150814
151553
  }
150815
151554
  clearSplatSelection() {
150816
151555
  if (!this._core) {