@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
@@ -105235,14 +105235,24 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105235
105235
  activateFlyMode() {
105236
105236
  if (!this.fly)
105237
105237
  return;
105238
- if (typeof this.fly.activate === 'function')
105239
- this.fly.activate();
105240
- // Align fly camera internal orientation with the current camera rotation so that
105241
- // switching from orbit -> fly does not snap the view back to the initial direction.
105238
+ // Preserve camera position and rotation when switching to fly mode
105242
105239
  try {
105240
+ // Preserve position
105241
+ const pos = this.camera.getPosition
105242
+ ? this.camera.getPosition().clone()
105243
+ : this.camera.getLocalPosition
105244
+ ? this.camera.getLocalPosition().clone()
105245
+ : null;
105246
+ if (pos && this.camera.setPosition) {
105247
+ ;
105248
+ this.camera.setPosition(pos);
105249
+ }
105250
+ // Preserve rotation (convert Euler to pitch/yaw)
105243
105251
  const euler = this.camera.getEulerAngles
105244
105252
  ? this.camera.getEulerAngles()
105245
- : null;
105253
+ : this.camera.getLocalEulerAngles
105254
+ ? this.camera.getLocalEulerAngles()
105255
+ : null;
105246
105256
  if (euler) {
105247
105257
  // These properties are part of the FlyCamera runtime state
105248
105258
  ;
@@ -105253,6 +105263,8 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105253
105263
  catch {
105254
105264
  // Best-effort sync; ignore if camera or script API differs
105255
105265
  }
105266
+ if (typeof this.fly.activate === 'function')
105267
+ this.fly.activate();
105256
105268
  }
105257
105269
  deactivateFlyMode() {
105258
105270
  if (!this.fly)
@@ -105262,6 +105274,373 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105262
105274
  }
105263
105275
  }
105264
105276
 
105277
+ /**
105278
+ * Fly camera controller for environments where PlayCanvas ScriptComponentSystem is not available
105279
+ * (e.g. supersplat-core's custom PCApp, which omits ScriptComponentSystem).
105280
+ *
105281
+ * This controller attaches DOM input listeners and updates the camera entity via `app.on('update')`.
105282
+ */
105283
+ class FlyCameraController {
105284
+ constructor(app, entity, emitFlyEvent, config) {
105285
+ // Config
105286
+ this.moveSpeed = 5.0;
105287
+ this.fastSpeedMultiplier = 3.0;
105288
+ this.slowSpeedMultiplier = 0.3;
105289
+ this.lookSensitivity = 0.2;
105290
+ this.invertY = false;
105291
+ this.keyBindings = {
105292
+ forward: 'KeyW',
105293
+ backward: 'KeyS',
105294
+ left: 'KeyA',
105295
+ right: 'KeyD',
105296
+ up: 'KeyE',
105297
+ down: 'KeyQ',
105298
+ fastMove: 'ShiftLeft',
105299
+ slowMove: 'ControlLeft',
105300
+ };
105301
+ this.smoothing = 0.8;
105302
+ this.friction = 0.85;
105303
+ this.enableCollision = false;
105304
+ this.minHeight = null;
105305
+ this.maxHeight = null;
105306
+ this._isActive = true;
105307
+ this._isPointerLocked = false;
105308
+ this._isLooking = false;
105309
+ this._pressed = new Set();
105310
+ this._velocity = new Vec3(0, 0, 0);
105311
+ this._targetVelocity = new Vec3(0, 0, 0);
105312
+ this._pitch = 0;
105313
+ this._yaw = 0;
105314
+ this._lastMoveEmitTime = 0;
105315
+ this._lastLookEmitTime = 0;
105316
+ this._updateHandler = null;
105317
+ this.app = app;
105318
+ this.entity = entity;
105319
+ this.emitFlyEvent = emitFlyEvent;
105320
+ if (config) {
105321
+ this.setConfig(config);
105322
+ }
105323
+ // Sync initial yaw/pitch from entity orientation if available
105324
+ try {
105325
+ const euler = this.entity?.getEulerAngles?.();
105326
+ if (euler) {
105327
+ this._pitch = euler.x || 0;
105328
+ this._yaw = euler.y || 0;
105329
+ }
105330
+ }
105331
+ catch {
105332
+ // ignore
105333
+ }
105334
+ this._bindInputListeners();
105335
+ }
105336
+ _bindInputListeners() {
105337
+ // Keyboard (capture phase so we see keys even when other handlers run)
105338
+ this._onKeyDown = this._handleKeyDown.bind(this);
105339
+ this._onKeyUp = this._handleKeyUp.bind(this);
105340
+ document.addEventListener('keydown', this._onKeyDown, true);
105341
+ document.addEventListener('keyup', this._onKeyUp, true);
105342
+ // Look: pointer events primary, mouse fallback
105343
+ this._onMouseMove = this._handleMouseMove.bind(this);
105344
+ this._onPointerMove = this._handlePointerMove.bind(this);
105345
+ document.addEventListener('mousemove', this._onMouseMove);
105346
+ document.addEventListener('pointermove', this._onPointerMove, true);
105347
+ const canvas = this.app?.graphicsDevice?.canvas;
105348
+ this._onMouseDown = (e) => {
105349
+ if (e.button === 0 && this._isActive)
105350
+ this._isLooking = true;
105351
+ };
105352
+ this._onPointerDown = (e) => {
105353
+ if (e.button === 0 && this._isActive) {
105354
+ this._isLooking = true;
105355
+ }
105356
+ };
105357
+ canvas?.addEventListener('mousedown', this._onMouseDown, true);
105358
+ canvas?.addEventListener('pointerdown', this._onPointerDown, true);
105359
+ this._onMouseUp = (e) => {
105360
+ if (e.button === 0)
105361
+ this._isLooking = false;
105362
+ };
105363
+ this._onPointerUp = (e) => {
105364
+ if (e.button === 0) {
105365
+ this._isLooking = false;
105366
+ }
105367
+ };
105368
+ document.addEventListener('mouseup', this._onMouseUp, true);
105369
+ document.addEventListener('pointerup', this._onPointerUp, true);
105370
+ }
105371
+ _unbindInputListeners() {
105372
+ document.removeEventListener('keydown', this._onKeyDown, true);
105373
+ document.removeEventListener('keyup', this._onKeyUp, true);
105374
+ document.removeEventListener('mousemove', this._onMouseMove);
105375
+ document.removeEventListener('pointermove', this._onPointerMove, true);
105376
+ document.removeEventListener('mouseup', this._onMouseUp, true);
105377
+ document.removeEventListener('pointerup', this._onPointerUp, true);
105378
+ const canvas = this.app?.graphicsDevice?.canvas;
105379
+ if (canvas && this._onMouseDown) {
105380
+ canvas.removeEventListener('mousedown', this._onMouseDown, true);
105381
+ }
105382
+ if (canvas && this._onPointerDown) {
105383
+ canvas.removeEventListener('pointerdown', this._onPointerDown, true);
105384
+ }
105385
+ }
105386
+ /**
105387
+ * Sync position and rotation from the camera entity.
105388
+ * Called when switching from orbit to fly mode to preserve camera state.
105389
+ */
105390
+ syncFromEntity() {
105391
+ try {
105392
+ // Preserve position
105393
+ const pos = this.entity?.getPosition?.() || this.entity?.getLocalPosition?.();
105394
+ if (pos) {
105395
+ const posVec = pos.clone ? pos.clone() : new Vec3(pos.x || 0, pos.y || 0, pos.z || 0);
105396
+ this.entity?.setPosition?.(posVec);
105397
+ this.entity?.setLocalPosition?.(posVec);
105398
+ }
105399
+ // Preserve rotation (convert Euler to pitch/yaw)
105400
+ const euler = this.entity?.getEulerAngles?.() || this.entity?.getLocalEulerAngles?.();
105401
+ if (euler) {
105402
+ // PlayCanvas Euler: x=pitch, y=yaw, z=roll
105403
+ this._pitch = euler.x || 0;
105404
+ this._yaw = euler.y || 0;
105405
+ // Apply rotation immediately so view doesn't snap
105406
+ if (this.entity?.setLocalEulerAngles) {
105407
+ this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
105408
+ }
105409
+ else if (this.entity?.setEulerAngles) {
105410
+ this.entity.setEulerAngles(this._pitch, this._yaw, 0);
105411
+ }
105412
+ }
105413
+ }
105414
+ catch {
105415
+ // ignore
105416
+ }
105417
+ }
105418
+ activate() {
105419
+ if (this._isActive)
105420
+ return;
105421
+ this._isActive = true;
105422
+ // Sync position and rotation from current camera state before activating
105423
+ this.syncFromEntity();
105424
+ if (!this._updateHandler) {
105425
+ this._updateHandler = (dt) => this.update(dt);
105426
+ this.app?.on?.('update', this._updateHandler);
105427
+ }
105428
+ }
105429
+ deactivate() {
105430
+ if (!this._isActive)
105431
+ return;
105432
+ this._isActive = false;
105433
+ this._isLooking = false;
105434
+ this._pressed.clear();
105435
+ if (this._updateHandler) {
105436
+ try {
105437
+ this.app?.off?.('update', this._updateHandler);
105438
+ }
105439
+ catch {
105440
+ // ignore
105441
+ }
105442
+ this._updateHandler = null;
105443
+ }
105444
+ }
105445
+ destroy() {
105446
+ try {
105447
+ this.deactivate();
105448
+ }
105449
+ finally {
105450
+ this._unbindInputListeners();
105451
+ }
105452
+ }
105453
+ setConfig(config) {
105454
+ if (config.moveSpeed !== undefined)
105455
+ this.moveSpeed = config.moveSpeed;
105456
+ if (config.fastSpeedMultiplier !== undefined)
105457
+ this.fastSpeedMultiplier = config.fastSpeedMultiplier;
105458
+ if (config.slowSpeedMultiplier !== undefined)
105459
+ this.slowSpeedMultiplier = config.slowSpeedMultiplier;
105460
+ if (config.lookSensitivity !== undefined)
105461
+ this.lookSensitivity = config.lookSensitivity;
105462
+ if (config.invertY !== undefined)
105463
+ this.invertY = config.invertY;
105464
+ if (config.keyBindings !== undefined)
105465
+ this.keyBindings = config.keyBindings;
105466
+ if (config.smoothing !== undefined)
105467
+ this.smoothing = config.smoothing;
105468
+ if (config.friction !== undefined)
105469
+ this.friction = config.friction;
105470
+ if (config.enableCollision !== undefined)
105471
+ this.enableCollision = config.enableCollision;
105472
+ if (config.minHeight !== undefined)
105473
+ this.minHeight = config.minHeight;
105474
+ if (config.maxHeight !== undefined)
105475
+ this.maxHeight = config.maxHeight;
105476
+ }
105477
+ getState() {
105478
+ const pos = this.entity?.getPosition?.();
105479
+ return {
105480
+ position: { x: pos?.x ?? 0, y: pos?.y ?? 0, z: pos?.z ?? 0 },
105481
+ rotation: { pitch: this._pitch, yaw: this._yaw },
105482
+ velocity: { x: this._velocity.x, y: this._velocity.y, z: this._velocity.z },
105483
+ isMoving: Math.abs(this._velocity.x) +
105484
+ Math.abs(this._velocity.y) +
105485
+ Math.abs(this._velocity.z) >
105486
+ 1e-4,
105487
+ };
105488
+ }
105489
+ _handleKeyDown(e) {
105490
+ const keys = [];
105491
+ if (e.code)
105492
+ keys.push(e.code);
105493
+ if (e.key) {
105494
+ keys.push(e.key);
105495
+ if (e.key.length === 1) {
105496
+ keys.push(`Key${e.key.toUpperCase()}`);
105497
+ keys.push(e.key.toUpperCase());
105498
+ keys.push(e.key.toLowerCase());
105499
+ }
105500
+ }
105501
+ for (const k of keys)
105502
+ this._pressed.add(k);
105503
+ }
105504
+ _handleKeyUp(e) {
105505
+ const keys = [];
105506
+ if (e.code)
105507
+ keys.push(e.code);
105508
+ if (e.key) {
105509
+ keys.push(e.key);
105510
+ if (e.key.length === 1) {
105511
+ keys.push(`Key${e.key.toUpperCase()}`);
105512
+ keys.push(e.key.toUpperCase());
105513
+ keys.push(e.key.toLowerCase());
105514
+ }
105515
+ }
105516
+ for (const k of keys)
105517
+ this._pressed.delete(k);
105518
+ }
105519
+ _handleMouseMove(e) {
105520
+ if (!this._isLooking || !this._isActive)
105521
+ return;
105522
+ const dx = e.movementX * this.lookSensitivity;
105523
+ const dy = e.movementY * this.lookSensitivity * (this.invertY ? 1 : -1);
105524
+ this._yaw = (this._yaw - dx) % 360;
105525
+ this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105526
+ }
105527
+ _handlePointerMove(e) {
105528
+ if (!this._isLooking || !this._isActive)
105529
+ return;
105530
+ const dx = (e.movementX || 0) * this.lookSensitivity;
105531
+ const dy = (e.movementY || 0) * this.lookSensitivity * (this.invertY ? 1 : -1);
105532
+ this._yaw = (this._yaw - dx) % 360;
105533
+ this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105534
+ }
105535
+ _getEffectiveSpeed() {
105536
+ const fast = this._pressed.has(this.keyBindings.fastMove);
105537
+ const slow = this._pressed.has(this.keyBindings.slowMove);
105538
+ let s = this.moveSpeed;
105539
+ if (fast)
105540
+ s *= this.fastSpeedMultiplier;
105541
+ if (slow)
105542
+ s *= this.slowSpeedMultiplier;
105543
+ return s;
105544
+ }
105545
+ _updateVelocity() {
105546
+ const kb = this.keyBindings;
105547
+ const isPressed = (binding, fallbacks) => {
105548
+ const all = [binding, ...fallbacks].filter(Boolean);
105549
+ return all.some(k => this._pressed.has(k));
105550
+ };
105551
+ const forward = isPressed(kb.forward, ['KeyW', 'w', 'W']) ? 1 : 0;
105552
+ const backward = isPressed(kb.backward, ['KeyS', 's', 'S']) ? 1 : 0;
105553
+ const left = isPressed(kb.left, ['KeyA', 'a', 'A']) ? 1 : 0;
105554
+ const right = isPressed(kb.right, ['KeyD', 'd', 'D']) ? 1 : 0;
105555
+ const up = isPressed(kb.up, ['KeyE', 'e', 'E']) ? 1 : 0;
105556
+ const down = isPressed(kb.down, ['KeyQ', 'q', 'Q']) ? 1 : 0;
105557
+ const inputZ = forward - backward;
105558
+ const inputX = right - left;
105559
+ const inputY = up - down;
105560
+ const planarLen = Math.hypot(inputX, inputZ);
105561
+ const nx = planarLen > 0 ? inputX / planarLen : 0;
105562
+ const nz = planarLen > 0 ? inputZ / planarLen : 0;
105563
+ const speed = this._getEffectiveSpeed() * 2;
105564
+ const entity = this.entity;
105565
+ const fwd = entity?.forward && entity.forward.clone
105566
+ ? entity.forward.clone()
105567
+ : entity?.forward
105568
+ ? new Vec3(entity.forward.x, entity.forward.y, entity.forward.z)
105569
+ : new Vec3(0, 0, -1);
105570
+ const rightVec = entity?.right && entity.right.clone
105571
+ ? entity.right.clone()
105572
+ : entity?.right
105573
+ ? new Vec3(entity.right.x, entity.right.y, entity.right.z)
105574
+ : new Vec3(1, 0, 0);
105575
+ const upVec = entity?.up && entity.up.clone
105576
+ ? entity.up.clone()
105577
+ : entity?.up
105578
+ ? new Vec3(entity.up.x, entity.up.y, entity.up.z)
105579
+ : Vec3.UP.clone();
105580
+ const target = new Vec3(0, 0, 0);
105581
+ target.add(fwd.mulScalar(nz * speed));
105582
+ target.add(rightVec.mulScalar(nx * speed));
105583
+ target.add(upVec.mulScalar(inputY * speed));
105584
+ this._targetVelocity.copy(target);
105585
+ this._velocity.lerp(this._velocity, this._targetVelocity, Math.min(1, this.smoothing));
105586
+ if (nx === 0 && nz === 0 && inputY === 0) {
105587
+ this._velocity.mulScalar(this.friction);
105588
+ if (this._velocity.length() < 0.0001)
105589
+ this._velocity.set(0, 0, 0);
105590
+ }
105591
+ }
105592
+ _applyMovement(dt) {
105593
+ if (this._velocity.length() === 0)
105594
+ return;
105595
+ const pos = this.entity?.getPosition?.()?.clone?.() ?? new Vec3(0, 0, 0);
105596
+ pos.add(this._velocity.clone().mulScalar(dt));
105597
+ this.entity?.setPosition?.(pos);
105598
+ }
105599
+ _applyRotation() {
105600
+ if (this.entity?.setLocalEulerAngles) {
105601
+ this.entity.setLocalEulerAngles(this._pitch, this._yaw, 0);
105602
+ }
105603
+ else if (this.entity?.setEulerAngles) {
105604
+ this.entity.setEulerAngles(this._pitch, this._yaw, 0);
105605
+ }
105606
+ }
105607
+ _applyConstraints() {
105608
+ if (!this.enableCollision &&
105609
+ this.minHeight == null &&
105610
+ this.maxHeight == null) {
105611
+ return;
105612
+ }
105613
+ const pos = this.entity?.getPosition?.()?.clone?.() ?? new Vec3(0, 0, 0);
105614
+ if (this.minHeight != null)
105615
+ pos.y = Math.max(pos.y, this.minHeight);
105616
+ if (this.maxHeight != null)
105617
+ pos.y = Math.min(pos.y, this.maxHeight);
105618
+ this.entity?.setPosition?.(pos);
105619
+ }
105620
+ update(dt) {
105621
+ if (!this._isActive)
105622
+ return;
105623
+ this._updateVelocity();
105624
+ this._applyMovement(dt);
105625
+ this._applyRotation();
105626
+ this._applyConstraints();
105627
+ // Emit throttled movement/look events (100ms)
105628
+ const now = performance.now();
105629
+ if (!this._lastMoveEmitTime || now - this._lastMoveEmitTime >= 100) {
105630
+ const pos = this.entity?.getPosition?.();
105631
+ this.emitFlyEvent?.('fly-camera-move', {
105632
+ position: { x: pos?.x ?? 0, y: pos?.y ?? 0, z: pos?.z ?? 0 },
105633
+ velocity: { x: this._velocity.x, y: this._velocity.y, z: this._velocity.z },
105634
+ });
105635
+ this._lastMoveEmitTime = now;
105636
+ }
105637
+ if (!this._lastLookEmitTime || now - this._lastLookEmitTime >= 100) {
105638
+ this.emitFlyEvent?.('fly-camera-look', { pitch: this._pitch, yaw: this._yaw });
105639
+ this._lastLookEmitTime = now;
105640
+ }
105641
+ }
105642
+ }
105643
+
105265
105644
  // FlyCamera PlayCanvas script: first-person WASD movement with mouse-look
105266
105645
  function registerFlyCameraScript() {
105267
105646
  if (typeof pc === 'undefined') {
@@ -105347,10 +105726,18 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105347
105726
  this._onKeyUp = this._handleKeyUp.bind(this);
105348
105727
  document.addEventListener('keydown', this._onKeyDown, true);
105349
105728
  document.addEventListener('keyup', this._onKeyUp, true);
105350
- // Mouse move for look (while mouse button held)
105729
+ // Mouse/pointer move for look (while primary button held).
105730
+ //
105731
+ // Important: SuperSplat camera controls are pointer-event based and may call
105732
+ // preventDefault() on pointer events. When that happens, browsers often
105733
+ // suppress the corresponding legacy mouse events (mousedown/mousemove).
105734
+ // So we listen to *pointer* events as the primary path, with mouse as a
105735
+ // fallback for older environments.
105351
105736
  this._onMouseMove = this._handleMouseMove.bind(this);
105737
+ this._onPointerMove = this._handlePointerMove.bind(this);
105352
105738
  document.addEventListener('mousemove', this._onMouseMove);
105353
- // Mouse button handling: click + hold to look, release to stop
105739
+ document.addEventListener('pointermove', this._onPointerMove, true);
105740
+ // Button handling: click + hold to look, release to stop
105354
105741
  const canvas = this.app.graphicsDevice.canvas;
105355
105742
  this._onClickToLock = (e) => {
105356
105743
  // Left button enables look while held (no pointer lock)
@@ -105358,13 +105745,17 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105358
105745
  this._isLooking = true;
105359
105746
  }
105360
105747
  };
105361
- canvas.addEventListener('mousedown', this._onClickToLock);
105748
+ canvas.addEventListener('mousedown', this._onClickToLock, true);
105749
+ this._onPointerDownToLook = this._handlePointerDown.bind(this);
105750
+ canvas.addEventListener('pointerdown', this._onPointerDownToLook, true);
105362
105751
  this._onMouseUp = (e) => {
105363
105752
  if (e.button === 0) {
105364
105753
  this._isLooking = false;
105365
105754
  }
105366
105755
  };
105367
- document.addEventListener('mouseup', this._onMouseUp);
105756
+ document.addEventListener('mouseup', this._onMouseUp, true);
105757
+ this._onPointerUp = this._handlePointerUp.bind(this);
105758
+ document.addEventListener('pointerup', this._onPointerUp, true);
105368
105759
  };
105369
105760
  FlyCamera.prototype.update = function (dt) {
105370
105761
  if (!this._isActive)
@@ -105460,9 +105851,11 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105460
105851
  this._onKeyDown = this._onKeyDown || this._handleKeyDown.bind(this);
105461
105852
  this._onKeyUp = this._onKeyUp || this._handleKeyUp.bind(this);
105462
105853
  this._onMouseMove = this._onMouseMove || this._handleMouseMove.bind(this);
105854
+ this._onPointerMove = this._onPointerMove || this._handlePointerMove.bind(this);
105463
105855
  document.addEventListener('keydown', this._onKeyDown, true);
105464
105856
  document.addEventListener('keyup', this._onKeyUp, true);
105465
105857
  document.addEventListener('mousemove', this._onMouseMove);
105858
+ document.addEventListener('pointermove', this._onPointerMove, true);
105466
105859
  const canvas = this.app.graphicsDevice.canvas;
105467
105860
  this._onClickToLock =
105468
105861
  this._onClickToLock ||
@@ -105471,7 +105864,10 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105471
105864
  this._isLooking = true;
105472
105865
  }
105473
105866
  });
105474
- canvas.addEventListener('mousedown', this._onClickToLock);
105867
+ canvas.addEventListener('mousedown', this._onClickToLock, true);
105868
+ this._onPointerDownToLook =
105869
+ this._onPointerDownToLook || this._handlePointerDown.bind(this);
105870
+ canvas.addEventListener('pointerdown', this._onPointerDownToLook, true);
105475
105871
  this._onMouseUp =
105476
105872
  this._onMouseUp ||
105477
105873
  ((e) => {
@@ -105479,22 +105875,36 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105479
105875
  this._isLooking = false;
105480
105876
  }
105481
105877
  });
105482
- document.addEventListener('mouseup', this._onMouseUp);
105878
+ document.addEventListener('mouseup', this._onMouseUp, true);
105879
+ this._onPointerUp = this._onPointerUp || this._handlePointerUp.bind(this);
105880
+ document.addEventListener('pointerup', this._onPointerUp, true);
105483
105881
  };
105484
105882
  FlyCamera.prototype.deactivate = function () {
105485
105883
  if (!this._isActive)
105486
105884
  return;
105487
105885
  this._isActive = false;
105886
+ this._isLooking = false;
105887
+ try {
105888
+ this._pressed?.clear?.();
105889
+ }
105890
+ catch {
105891
+ // ignore
105892
+ }
105488
105893
  // Exit pointer lock when deactivating
105489
105894
  this._exitPointerLock();
105490
105895
  // Remove listeners
105491
105896
  document.removeEventListener('keydown', this._onKeyDown, true);
105492
105897
  document.removeEventListener('keyup', this._onKeyUp, true);
105493
105898
  document.removeEventListener('mousemove', this._onMouseMove);
105494
- document.removeEventListener('mouseup', this._onMouseUp);
105899
+ document.removeEventListener('pointermove', this._onPointerMove, true);
105900
+ document.removeEventListener('mouseup', this._onMouseUp, true);
105901
+ document.removeEventListener('pointerup', this._onPointerUp, true);
105495
105902
  const canvas = this.app?.graphicsDevice?.canvas;
105496
105903
  if (canvas && this._onClickToLock) {
105497
- canvas.removeEventListener('mousedown', this._onClickToLock);
105904
+ canvas.removeEventListener('mousedown', this._onClickToLock, true);
105905
+ }
105906
+ if (canvas && this._onPointerDownToLook) {
105907
+ canvas.removeEventListener('pointerdown', this._onPointerDownToLook, true);
105498
105908
  }
105499
105909
  };
105500
105910
  FlyCamera.prototype.setConfig = function (config) {
@@ -105583,6 +105993,27 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105583
105993
  this._yaw = (this._yaw - dx) % 360;
105584
105994
  this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
105585
105995
  };
105996
+ FlyCamera.prototype._handlePointerMove = function (e) {
105997
+ if (!this._isLooking || !this._isActive)
105998
+ return;
105999
+ const dx = (e.movementX || 0) * this.lookSensitivity;
106000
+ const dy = (e.movementY || 0) * this.lookSensitivity * (this.invertY ? 1 : -1);
106001
+ this._yaw = (this._yaw - dx) % 360;
106002
+ this._pitch = Math.max(-89, Math.min(89, this._pitch + dy));
106003
+ };
106004
+ FlyCamera.prototype._handlePointerDown = function (e) {
106005
+ if (!this._isActive)
106006
+ return;
106007
+ // Primary button enables look while held (no pointer lock)
106008
+ if (e.button === 0) {
106009
+ this._isLooking = true;
106010
+ }
106011
+ };
106012
+ FlyCamera.prototype._handlePointerUp = function (e) {
106013
+ if (e.button === 0) {
106014
+ this._isLooking = false;
106015
+ }
106016
+ };
105586
106017
  FlyCamera.prototype._handlePointerLockChange = function () {
105587
106018
  const canvas = this.app.graphicsDevice.canvas;
105588
106019
  this._isPointerLocked = document.pointerLockElement === canvas;
@@ -105879,7 +106310,7 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105879
106310
  scope.resolve(key).setValue(values[key]);
105880
106311
  }
105881
106312
  };
105882
- const createBlueNoiseTexture = (device) => {
106313
+ const createBlueNoiseTexture$1 = (device) => {
105883
106314
  const size = 32;
105884
106315
  const texture = new Texture$1(device, {
105885
106316
  width: size,
@@ -105958,7 +106389,7 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
105958
106389
  throw new Error('InfiniteGrid: QuadRender is not available in this PlayCanvas version.');
105959
106390
  }
105960
106391
  this.quadRender = new QuadRender$1(this.shader);
105961
- this.blueNoiseTexture = createBlueNoiseTexture(device);
106392
+ this.blueNoiseTexture = createBlueNoiseTexture$1(device);
105962
106393
  this._createBlendState();
105963
106394
  this._registerRenderHook();
105964
106395
  }
@@ -139095,6 +139526,7 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
139095
139526
  enableStats: false,
139096
139527
  autoFocus: true,
139097
139528
  maxSplats: 2000000,
139529
+ previewMode: false,
139098
139530
  camera: {
139099
139531
  position: { x: 0, y: 0, z: 10 },
139100
139532
  target: { x: 0, y: 0, z: 0 },
@@ -139184,6 +139616,10 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
139184
139616
  if (enableStats !== null) {
139185
139617
  config.enableStats = parseBoolean(enableStats);
139186
139618
  }
139619
+ const previewMode = element.getAttribute('preview-mode');
139620
+ if (previewMode !== null) {
139621
+ config.previewMode = parseBoolean(previewMode);
139622
+ }
139187
139623
  // Parse number attributes
139188
139624
  const maxSplats = element.getAttribute('max-splats');
139189
139625
  if (maxSplats !== null) {
@@ -140324,51 +140760,161 @@ fn fragmentMain(input: FragmentInput) -> FragmentOutput {
140324
140760
  return fallback.clone();
140325
140761
  }
140326
140762
 
140327
- const THICK_WIREFRAME_LINE_WIDTH_UV$1 = 0.06;
140328
- const THICK_WIREFRAME_OPACITY$1 = 1.0;
140329
- const THICK_WIREFRAME_BOX_VS = /* glsl */ `
140330
- attribute vec3 vertex_position;
140331
- attribute vec2 vertex_texCoord0;
140763
+ const cache = new WeakMap();
140764
+ function createBlueNoiseTexture(device) {
140765
+ const size = 32;
140766
+ const texture = new Texture$1(device, {
140767
+ width: size,
140768
+ height: size,
140769
+ format: PIXELFORMAT_R8_G8_B8_A8,
140770
+ mipmaps: false,
140771
+ });
140772
+ texture.addressU = ADDRESS_REPEAT;
140773
+ texture.addressV = ADDRESS_REPEAT;
140774
+ texture.minFilter = FILTER_NEAREST;
140775
+ texture.magFilter = FILTER_NEAREST;
140776
+ const pixels = texture.lock();
140777
+ const seed = 1337;
140778
+ let value = seed;
140779
+ const random = () => {
140780
+ value ^= value << 13;
140781
+ value ^= value >>> 17;
140782
+ value ^= value << 5;
140783
+ return ((value >>> 0) % 256) / 255;
140784
+ };
140785
+ for (let i = 0; i < size * size; i++) {
140786
+ const noise = Math.floor(random() * 255);
140787
+ const idx = i * 4;
140788
+ pixels[idx + 0] = noise;
140789
+ pixels[idx + 1] = noise;
140790
+ pixels[idx + 2] = noise;
140791
+ pixels[idx + 3] = 255;
140792
+ }
140793
+ texture.unlock();
140794
+ texture.name = 'supersplat-blue-noise';
140795
+ return texture;
140796
+ }
140797
+ function getBlueNoiseTex32(device) {
140798
+ const existing = cache.get(device);
140799
+ if (existing)
140800
+ return existing;
140801
+ const tex = createBlueNoiseTexture(device);
140802
+ cache.set(device, tex);
140803
+ return tex;
140804
+ }
140332
140805
 
140333
- uniform mat4 matrix_model;
140334
- uniform mat4 matrix_viewProjection;
140806
+ // SuperSplat-like selection box shader: screen-space ray/box intersection that draws a thick
140807
+ // white wire/grid pattern (not dependent on WebGL line width).
140808
+ const BOX_SELECT_VS = /* glsl */ `
140809
+ attribute vec3 vertex_position;
140335
140810
 
140336
- varying vec2 vUv;
140811
+ uniform mat4 matrix_model;
140812
+ uniform mat4 matrix_viewProjection;
140337
140813
 
140338
- void main(void) {
140339
- vUv = vertex_texCoord0;
140340
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140341
- }
140814
+ void main() {
140815
+ gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140816
+ }
140342
140817
  `;
140343
- const THICK_WIREFRAME_BOX_FS = /* glsl */ `
140344
- #ifdef GL_OES_standard_derivatives
140345
- #extension GL_OES_standard_derivatives : enable
140346
- #endif
140818
+ const BOX_SELECT_FS = /* glsl */ `
140819
+ // ray-box intersection in box space
140820
+ bool intersectBox(out float t0, out float t1, out int axis0, out int axis1, vec3 pos, vec3 dir, vec3 boxCen, vec3 boxLen)
140821
+ {
140822
+ bvec3 validDir = notEqual(dir, vec3(0.0));
140823
+ vec3 absDir = abs(dir);
140824
+ vec3 signDir = sign(dir);
140825
+ vec3 m = vec3(
140826
+ validDir.x ? 1.0 / absDir.x : 0.0,
140827
+ validDir.y ? 1.0 / absDir.y : 0.0,
140828
+ validDir.z ? 1.0 / absDir.z : 0.0
140829
+ ) * signDir;
140347
140830
 
140348
- precision mediump float;
140831
+ vec3 n = m * (pos - boxCen);
140832
+ vec3 k = abs(m) * boxLen;
140349
140833
 
140350
- uniform vec3 lineColor;
140351
- uniform float lineWidth;
140352
- uniform float opacity;
140834
+ vec3 v0 = -n - k;
140835
+ vec3 v1 = -n + k;
140353
140836
 
140354
- varying vec2 vUv;
140837
+ // replace invalid axes with -inf and +inf so the tests below ignore them
140838
+ v0 = mix(vec3(-1.0 / 0.0000001), v0, validDir);
140839
+ v1 = mix(vec3(1.0 / 0.0000001), v1, validDir);
140355
140840
 
140356
- void main(void) {
140357
- // Distance to nearest face border in UV space (0 at border, 0.5 at center).
140358
- float d = min(min(vUv.x, 1.0 - vUv.x), min(vUv.y, 1.0 - vUv.y));
140841
+ axis0 = (v0.x > v0.y) ? ((v0.x > v0.z) ? 0 : 2) : ((v0.y > v0.z) ? 1 : 2);
140842
+ axis1 = (v1.x < v1.y) ? ((v1.x < v1.z) ? 0 : 2) : ((v1.y < v1.z) ? 1 : 2);
140359
140843
 
140360
- // Anti-aliased edge mask (1 near border, 0 in face interior).
140361
- float aa = 0.002;
140362
- #ifdef GL_OES_standard_derivatives
140363
- aa = max(aa, fwidth(d) * 1.5);
140364
- #endif
140365
- float a = 1.0 - smoothstep(lineWidth, lineWidth + aa, d);
140366
- a *= opacity;
140844
+ t0 = v0[axis0];
140845
+ t1 = v1[axis1];
140367
140846
 
140368
- if (a <= 0.001) discard;
140369
- gl_FragColor = vec4(lineColor, a);
140370
- }
140847
+ if (t0 > t1 || t1 < 0.0) {
140848
+ return false;
140849
+ }
140850
+
140851
+ return true;
140852
+ }
140853
+
140854
+ float calcDepth(in vec3 pos, in mat4 viewProjection) {
140855
+ vec4 v = viewProjection * vec4(pos, 1.0);
140856
+ return (v.z / v.w) * 0.5 + 0.5;
140857
+ }
140858
+
140859
+ uniform sampler2D blueNoiseTex32;
140860
+ uniform mat4 matrix_viewProjection;
140861
+ uniform vec3 boxCen;
140862
+ uniform vec3 boxLen;
140863
+
140864
+ uniform vec3 near_origin;
140865
+ uniform vec3 near_x;
140866
+ uniform vec3 near_y;
140867
+
140868
+ uniform vec3 far_origin;
140869
+ uniform vec3 far_x;
140870
+ uniform vec3 far_y;
140871
+
140872
+ uniform vec2 targetSize;
140873
+ uniform vec3 lineColor;
140874
+
140875
+ bool writeDepth(float alpha) {
140876
+ ivec2 uv = ivec2(gl_FragCoord.xy);
140877
+ ivec2 size = textureSize(blueNoiseTex32, 0);
140878
+ return alpha > texelFetch(blueNoiseTex32, uv % size, 0).y;
140879
+ }
140880
+
140881
+ bool strips(vec3 pos, int axis) {
140882
+ // Thickness tuned to match SuperSplat viewer "thick wire"
140883
+ bvec3 b = lessThan(fract(pos * 2.0 + vec3(0.015)), vec3(0.06));
140884
+ b[axis] = false;
140885
+ return any(b);
140886
+ }
140887
+
140888
+ void main() {
140889
+ vec2 clip = gl_FragCoord.xy / targetSize;
140890
+ vec3 worldNear = near_origin + near_x * clip.x + near_y * clip.y;
140891
+ vec3 worldFar = far_origin + far_x * clip.x + far_y * clip.y;
140892
+ vec3 rayDir = normalize(worldFar - worldNear);
140893
+
140894
+ float t0, t1;
140895
+ int axis0, axis1;
140896
+ if (!intersectBox(t0, t1, axis0, axis1, worldNear, rayDir, boxCen, boxLen)) {
140897
+ discard;
140898
+ }
140899
+
140900
+ vec3 frontPos = worldNear + rayDir * t0;
140901
+ bool front = t0 > 0.0 && strips(frontPos - boxCen, axis0);
140902
+
140903
+ vec3 backPos = worldNear + rayDir * t1;
140904
+ bool back = strips(backPos - boxCen, axis1);
140905
+
140906
+ if (front) {
140907
+ gl_FragColor = vec4(lineColor, 0.6);
140908
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(frontPos, matrix_viewProjection) : 1.0;
140909
+ } else if (back) {
140910
+ gl_FragColor = vec4(lineColor, 0.6);
140911
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(backPos, matrix_viewProjection) : 1.0;
140912
+ } else {
140913
+ discard;
140914
+ }
140915
+ }
140371
140916
  `;
140917
+ const tmpPos$1 = new Vec3();
140372
140918
  class BoxSelectionAPI {
140373
140919
  constructor() {
140374
140920
  this.boxes = new Map();
@@ -140405,6 +140951,7 @@ void main(void) {
140405
140951
  this.ensureTranslateGizmo();
140406
140952
  this.updateGizmoSize();
140407
140953
  this.updateGizmoAttachment();
140954
+ this.updateBoxShaderUniforms();
140408
140955
  }
140409
140956
  createBox(options = {}) {
140410
140957
  if (!this.app || !this.parent) {
@@ -140427,7 +140974,7 @@ void main(void) {
140427
140974
  entity.render.castShadows = false;
140428
140975
  entity.render.receiveShadows = false;
140429
140976
  entity.render.enabled = visible;
140430
- // Render as a thick wireframe (shader-based) so line width is consistent across browsers.
140977
+ // Render as a thick wireframe (shader-based) to match SuperSplat viewer.
140431
140978
  this.parent.addChild(entity);
140432
140979
  const record = {
140433
140980
  id,
@@ -140514,8 +141061,8 @@ void main(void) {
140514
141061
  return false;
140515
141062
  }
140516
141063
  record.color.set(r, g, b);
140517
- const mat = this.buildWireframeMaterial(record.color);
140518
- record.entity.render.material = mat;
141064
+ const mat = record.entity.render?.material;
141065
+ mat?.setParameter?.('lineColor', [record.color.x, record.color.y, record.color.z]);
140519
141066
  this.requestRender();
140520
141067
  return true;
140521
141068
  }
@@ -140710,19 +141257,34 @@ void main(void) {
140710
141257
  }
140711
141258
  buildWireframeMaterial(color) {
140712
141259
  const material = new ShaderMaterial$1({
140713
- uniqueName: 'boxSelectionThickWireframe',
140714
- vertexGLSL: THICK_WIREFRAME_BOX_VS,
140715
- fragmentGLSL: THICK_WIREFRAME_BOX_FS,
141260
+ uniqueName: 'boxSelectionSupersplatWire',
141261
+ vertexGLSL: BOX_SELECT_VS,
141262
+ fragmentGLSL: BOX_SELECT_FS,
140716
141263
  });
140717
- material.cull = CULLFACE_NONE;
140718
- material.depthWrite = false;
141264
+ material.cull = CULLFACE_FRONT;
140719
141265
  material.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE_MINUS_SRC_ALPHA);
140720
- material.setParameter('lineColor', [color.x, color.y, color.z]);
140721
- material.setParameter('lineWidth', THICK_WIREFRAME_LINE_WIDTH_UV$1);
140722
- material.setParameter('opacity', THICK_WIREFRAME_OPACITY$1);
140723
141266
  material.update();
141267
+ material.setParameter('lineColor', [color.x, color.y, color.z]);
140724
141268
  return material;
140725
141269
  }
141270
+ updateBoxShaderUniforms() {
141271
+ if (!this.app)
141272
+ return;
141273
+ const device = this.app.graphicsDevice;
141274
+ // Ensure required global uniforms for selection shaders.
141275
+ device.scope.resolve('targetSize').setValue([device.width, device.height]);
141276
+ device.scope.resolve('blueNoiseTex32').setValue(getBlueNoiseTex32(device));
141277
+ for (const record of this.boxes.values()) {
141278
+ const mat = record.entity.render?.material;
141279
+ if (!mat?.setParameter)
141280
+ continue;
141281
+ // World-space center
141282
+ record.entity.getWorldTransform().getTranslation(tmpPos$1);
141283
+ mat.setParameter('boxCen', [tmpPos$1.x, tmpPos$1.y, tmpPos$1.z]);
141284
+ // Half extents
141285
+ mat.setParameter('boxLen', [record.lenX * 0.5, record.lenY * 0.5, record.lenZ * 0.5]);
141286
+ }
141287
+ }
140726
141288
  emitEvent(event, detail) {
140727
141289
  if (this.onEvent) {
140728
141290
  this.onEvent(event, detail);
@@ -140734,59 +141296,110 @@ void main(void) {
140734
141296
  }
140735
141297
  }
140736
141298
 
140737
- const THICK_WIREFRAME_LINE_WIDTH_UV = 0.035;
140738
- const THICK_WIREFRAME_OPACITY = 1.0;
140739
- const SPHERE_GRID_U = 14.0;
140740
- const SPHERE_GRID_V = 10.0;
140741
- const THICK_WIREFRAME_SPHERE_VS = /* glsl */ `
140742
- attribute vec3 vertex_position;
140743
- attribute vec2 vertex_texCoord0;
140744
-
140745
- uniform mat4 matrix_model;
140746
- uniform mat4 matrix_viewProjection;
141299
+ // SuperSplat-like selection sphere shader: screen-space ray/sphere intersection that draws a thick
141300
+ // white wire/grid pattern (not dependent on WebGL line width).
141301
+ const SPHERE_SELECT_VS = /* glsl */ `
141302
+ attribute vec3 vertex_position;
140747
141303
 
140748
- varying vec2 vUv;
141304
+ uniform mat4 matrix_model;
141305
+ uniform mat4 matrix_viewProjection;
140749
141306
 
140750
- void main(void) {
140751
- vUv = vertex_texCoord0;
140752
- gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
140753
- }
141307
+ void main() {
141308
+ gl_Position = matrix_viewProjection * matrix_model * vec4(vertex_position, 1.0);
141309
+ }
140754
141310
  `;
140755
- const THICK_WIREFRAME_SPHERE_FS = /* glsl */ `
140756
- #ifdef GL_OES_standard_derivatives
140757
- #extension GL_OES_standard_derivatives : enable
140758
- #endif
141311
+ const SPHERE_SELECT_FS = /* glsl */ `
141312
+ bool intersectSphere(out float t0, out float t1, vec3 pos, vec3 dir, vec4 sphere) {
141313
+ vec3 L = sphere.xyz - pos;
141314
+ float tca = dot(L, dir);
140759
141315
 
140760
- precision mediump float;
141316
+ float d2 = sphere.w * sphere.w - (dot(L, L) - tca * tca);
141317
+ if (d2 <= 0.0) {
141318
+ return false;
141319
+ }
140761
141320
 
140762
- uniform vec3 lineColor;
140763
- uniform float lineWidth;
140764
- uniform float opacity;
141321
+ float thc = sqrt(d2);
141322
+ t0 = tca - thc;
141323
+ t1 = tca + thc;
141324
+ if (t1 <= 0.0) {
141325
+ return false;
141326
+ }
140765
141327
 
140766
- varying vec2 vUv;
141328
+ return true;
141329
+ }
140767
141330
 
140768
- float gridLineDist(float v, float freq) {
140769
- float f = fract(v * freq);
140770
- return min(f, 1.0 - f);
140771
- }
141331
+ float calcDepth(in vec3 pos, in mat4 viewProjection) {
141332
+ vec4 v = viewProjection * vec4(pos, 1.0);
141333
+ return (v.z / v.w) * 0.5 + 0.5;
141334
+ }
140772
141335
 
140773
- void main(void) {
140774
- // "Wireframe-like" lat/long grid in UV space.
140775
- float du = gridLineDist(vUv.x, ${SPHERE_GRID_U});
140776
- float dv = gridLineDist(vUv.y, ${SPHERE_GRID_V});
140777
- float d = min(du, dv);
140778
-
140779
- float aa = 0.002;
140780
- #ifdef GL_OES_standard_derivatives
140781
- aa = max(aa, fwidth(d) * 1.5);
140782
- #endif
140783
- float a = 1.0 - smoothstep(lineWidth, lineWidth + aa, d);
140784
- a *= opacity;
141336
+ vec2 calcAzimuthElev(in vec3 dir) {
141337
+ float azimuth = atan(dir.z, dir.x);
141338
+ float elev = asin(dir.y);
141339
+ return vec2(azimuth, elev) * 180.0 / 3.14159;
141340
+ }
140785
141341
 
140786
- if (a <= 0.001) discard;
140787
- gl_FragColor = vec4(lineColor, a);
140788
- }
141342
+ uniform sampler2D blueNoiseTex32;
141343
+ uniform mat4 matrix_viewProjection;
141344
+ uniform vec4 sphere;
141345
+ uniform vec3 lineColor;
141346
+
141347
+ uniform vec3 near_origin;
141348
+ uniform vec3 near_x;
141349
+ uniform vec3 near_y;
141350
+
141351
+ uniform vec3 far_origin;
141352
+ uniform vec3 far_x;
141353
+ uniform vec3 far_y;
141354
+
141355
+ uniform vec2 targetSize;
141356
+
141357
+ bool writeDepth(float alpha) {
141358
+ vec2 uv = fract(gl_FragCoord.xy / 32.0);
141359
+ float noise = texture2DLod(blueNoiseTex32, uv, 0.0).y;
141360
+ return alpha > noise;
141361
+ }
141362
+
141363
+ bool strips(vec3 lp) {
141364
+ vec2 ae = calcAzimuthElev(normalize(lp));
141365
+
141366
+ float spacing = 180.0 / (2.0 * 3.14159 * sphere.w);
141367
+ // Thickness tuned to match SuperSplat viewer "thick wire"
141368
+ float size = 0.06;
141369
+ return fract(ae.x / spacing) < size ||
141370
+ fract(ae.y / spacing) < size;
141371
+ }
141372
+
141373
+ void main() {
141374
+ vec2 clip = gl_FragCoord.xy / targetSize;
141375
+ vec3 worldNear = near_origin + near_x * clip.x + near_y * clip.y;
141376
+ vec3 worldFar = far_origin + far_x * clip.x + far_y * clip.y;
141377
+
141378
+ vec3 rayDir = normalize(worldFar - worldNear);
141379
+
141380
+ float t0, t1;
141381
+ if (!intersectSphere(t0, t1, worldNear, rayDir, sphere)) {
141382
+ discard;
141383
+ }
141384
+
141385
+ vec3 frontPos = worldNear + rayDir * t0;
141386
+ bool front = t0 > 0.0 && strips(frontPos - sphere.xyz);
141387
+
141388
+ vec3 backPos = worldNear + rayDir * t1;
141389
+ bool back = strips(backPos - sphere.xyz);
141390
+
141391
+ if (front) {
141392
+ gl_FragColor = vec4(lineColor, 0.6);
141393
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(frontPos, matrix_viewProjection) : 1.0;
141394
+ } else if (back) {
141395
+ gl_FragColor = vec4(lineColor, 0.6);
141396
+ gl_FragDepth = writeDepth(0.6) ? calcDepth(backPos, matrix_viewProjection) : 1.0;
141397
+ } else {
141398
+ discard;
141399
+ }
141400
+ }
140789
141401
  `;
141402
+ const tmpPos = new Vec3();
140790
141403
  class SphereSelectionAPI {
140791
141404
  constructor() {
140792
141405
  this.spheres = new Map();
@@ -140822,6 +141435,7 @@ void main(void) {
140822
141435
  this.ensureTranslateGizmo();
140823
141436
  this.updateGizmoSize();
140824
141437
  this.updateGizmoAttachment();
141438
+ this.updateSphereShaderUniforms();
140825
141439
  }
140826
141440
  createSphere(options = {}) {
140827
141441
  if (!this.app || !this.parent) {
@@ -140834,7 +141448,8 @@ void main(void) {
140834
141448
  const visible = options.visible ?? true;
140835
141449
  const entity = new Entity(id);
140836
141450
  entity.addComponent('render', {
140837
- type: 'sphere',
141451
+ // Use a box proxy like SuperSplat; shader ray-marches a true sphere.
141452
+ type: 'box',
140838
141453
  material: this.buildWireframeMaterial(color),
140839
141454
  });
140840
141455
  entity.setLocalScale(radius * 2, radius * 2, radius * 2); // sphere diameter = radius * 2
@@ -140842,7 +141457,7 @@ void main(void) {
140842
141457
  entity.render.castShadows = false;
140843
141458
  entity.render.receiveShadows = false;
140844
141459
  entity.render.enabled = visible;
140845
- // Render as a thick wireframe (shader-based) so line width is consistent across browsers.
141460
+ // Render as a thick wireframe (shader-based) to match SuperSplat viewer.
140846
141461
  this.parent.addChild(entity);
140847
141462
  const record = {
140848
141463
  id,
@@ -140925,8 +141540,12 @@ void main(void) {
140925
141540
  return false;
140926
141541
  }
140927
141542
  record.color.set(r, g, b);
140928
- const mat = this.buildWireframeMaterial(record.color);
140929
- record.entity.render.material = mat;
141543
+ const mat = record.entity.render?.material;
141544
+ mat?.setParameter?.('lineColor', [
141545
+ record.color.x,
141546
+ record.color.y,
141547
+ record.color.z,
141548
+ ]);
140930
141549
  this.requestRender();
140931
141550
  return true;
140932
141551
  }
@@ -141124,19 +141743,30 @@ void main(void) {
141124
141743
  }
141125
141744
  buildWireframeMaterial(color) {
141126
141745
  const material = new ShaderMaterial$1({
141127
- uniqueName: 'sphereSelectionThickWireframe',
141128
- vertexGLSL: THICK_WIREFRAME_SPHERE_VS,
141129
- fragmentGLSL: THICK_WIREFRAME_SPHERE_FS,
141746
+ uniqueName: 'sphereSelectionSupersplatWire',
141747
+ vertexGLSL: SPHERE_SELECT_VS,
141748
+ fragmentGLSL: SPHERE_SELECT_FS,
141130
141749
  });
141131
- material.cull = CULLFACE_NONE;
141132
- material.depthWrite = false;
141750
+ material.cull = CULLFACE_FRONT;
141133
141751
  material.blendState = new BlendState(true, BLENDEQUATION_ADD, BLENDMODE_SRC_ALPHA, BLENDMODE_ONE_MINUS_SRC_ALPHA, BLENDEQUATION_ADD, BLENDMODE_ONE, BLENDMODE_ONE_MINUS_SRC_ALPHA);
141134
- material.setParameter('lineColor', [color.x, color.y, color.z]);
141135
- material.setParameter('lineWidth', THICK_WIREFRAME_LINE_WIDTH_UV);
141136
- material.setParameter('opacity', THICK_WIREFRAME_OPACITY);
141137
141752
  material.update();
141753
+ material.setParameter('lineColor', [color.x, color.y, color.z]);
141138
141754
  return material;
141139
141755
  }
141756
+ updateSphereShaderUniforms() {
141757
+ if (!this.app)
141758
+ return;
141759
+ const device = this.app.graphicsDevice;
141760
+ device.scope.resolve('targetSize').setValue([device.width, device.height]);
141761
+ device.scope.resolve('blueNoiseTex32').setValue(getBlueNoiseTex32(device));
141762
+ for (const record of this.spheres.values()) {
141763
+ const mat = record.entity.render?.material;
141764
+ if (!mat?.setParameter)
141765
+ continue;
141766
+ record.entity.getWorldTransform().getTranslation(tmpPos);
141767
+ mat.setParameter('sphere', [tmpPos.x, tmpPos.y, tmpPos.z, record.radius]);
141768
+ }
141769
+ }
141140
141770
  emitEvent(event, detail) {
141141
141771
  if (this.onEvent) {
141142
141772
  this.onEvent(event, detail);
@@ -142543,222 +143173,222 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
142543
143173
  }
142544
143174
  }
142545
143175
 
142546
- const vertexShader$6 = /* glsl */ `
142547
- attribute vec2 vertex_position;
142548
- void main(void) {
142549
- gl_Position = vec4(vertex_position, 0.0, 1.0);
142550
- }
142551
- `;
142552
- const fragmentShader$6 = /* glsl */ `
142553
- uniform highp usampler2D transformA; // splat center x, y, z
142554
- uniform highp usampler2D splatTransform; // transform palette index
142555
- uniform sampler2D transformPalette; // palette of transforms
142556
- uniform sampler2D splatState; // per-splat state
142557
- uniform highp ivec3 splat_params; // texture width, texture height, num splats
142558
- uniform highp uint mode; // 0: selected, 1: visible
142559
-
142560
- // calculate min and max for a single column of splats
142561
- void main(void) {
142562
-
142563
- vec3 boundMin = vec3(1e6);
142564
- vec3 boundMax = vec3(-1e6);
142565
-
142566
- for (int id = 0; id < splat_params.y; id++) {
142567
- // calculate splatUV
142568
- ivec2 splatUV = ivec2(gl_FragCoord.x, id);
142569
-
142570
- // skip out-of-range splats
142571
- if ((splatUV.x + splatUV.y * splat_params.x) >= splat_params.z) {
142572
- continue;
142573
- }
142574
-
142575
- // read splat state
142576
- uint state = uint(texelFetch(splatState, splatUV, 0).r * 255.0);
142577
-
142578
- // skip deleted or locked splats
142579
- if (((mode == 0u) && (state != 1u)) || ((mode == 1u) && ((state & 4u) != 0u))) {
142580
- continue;
142581
- }
142582
-
142583
- // read splat center
142584
- vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
142585
-
142586
- // apply optional per-splat transform
142587
- uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
142588
- if (transformIndex > 0u) {
142589
- // read transform matrix
142590
- int u = int(transformIndex % 512u) * 3;
142591
- int v = int(transformIndex / 512u);
142592
-
142593
- mat3x4 t;
142594
- t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
142595
- t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
142596
- t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
142597
-
142598
- center = vec4(center, 1.0) * t;
142599
- }
142600
-
142601
- boundMin = min(boundMin, mix(center, boundMin, isinf(center)));
142602
- boundMax = max(boundMax, mix(center, boundMax, isinf(center)));
142603
- }
142604
-
142605
- pcFragColor0 = vec4(boundMin, 0.0);
142606
- pcFragColor1 = vec4(boundMax, 0.0);
142607
- }
142608
- `;
142609
-
142610
- const vertexShader$5 = /* glsl */ `
142611
- attribute vec2 vertex_position;
142612
- void main(void) {
142613
- gl_Position = vec4(vertex_position, 0.0, 1.0);
142614
- }
142615
- `;
142616
- const fragmentShader$5 = /* glsl */ `
142617
- uniform highp usampler2D transformA; // splat center x, y, z
142618
- uniform highp usampler2D splatTransform; // transform palette index
142619
- uniform sampler2D transformPalette; // palette of transforms
142620
- uniform uvec2 splat_params; // splat texture width, num splats
142621
-
142622
- uniform mat4 matrix_model;
142623
- uniform mat4 matrix_viewProjection;
142624
-
142625
- uniform uvec2 output_params; // output width, height
142626
-
142627
- // 0: mask, 1: rect, 2: sphere
142628
- uniform int mode;
142629
-
142630
- // mask params
142631
- uniform sampler2D mask; // mask in alpha channel
142632
- uniform vec2 mask_params; // mask width, height
142633
-
142634
- // rect params
142635
- uniform vec4 rect_params; // rect x, y, width, height
142636
-
142637
- // sphere params
142638
- uniform vec4 sphere_params; // sphere x, y, z, radius
142639
-
142640
- // box params
142641
- uniform vec4 box_params; // box x, y, z
142642
- uniform vec4 aabb_params; // len x, y, z
142643
-
142644
- void main(void) {
142645
- // calculate output id
142646
- uvec2 outputUV = uvec2(gl_FragCoord);
142647
- uint outputId = (outputUV.x + outputUV.y * output_params.x) * 4u;
142648
-
142649
- vec4 clr = vec4(0.0);
142650
-
142651
- for (uint i = 0u; i < 4u; i++) {
142652
- uint id = outputId + i;
142653
-
142654
- if (id >= splat_params.y) {
142655
- continue;
142656
- }
142657
-
142658
- // calculate splatUV
142659
- ivec2 splatUV = ivec2(
142660
- int(id % splat_params.x),
142661
- int(id / splat_params.x)
142662
- );
142663
-
142664
- // read splat center
142665
- vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
142666
-
142667
- // apply optional per-splat transform
142668
- uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
142669
- if (transformIndex > 0u) {
142670
- // read transform matrix
142671
- int u = int(transformIndex % 512u) * 3;
142672
- int v = int(transformIndex / 512u);
142673
-
142674
- mat3x4 t;
142675
- t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
142676
- t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
142677
- t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
142678
-
142679
- center = vec4(center, 1.0) * t;
142680
- }
142681
-
142682
- // transform to clip space and discard if outside
142683
- vec3 world = (matrix_model * vec4(center, 1.0)).xyz;
142684
- vec4 clip = matrix_viewProjection * vec4(world, 1.0);
142685
- vec3 ndc = clip.xyz / clip.w;
142686
-
142687
- // skip offscreen fragments
142688
- if (!any(greaterThan(abs(ndc), vec3(1.0)))) {
142689
- if (mode == 0) {
142690
- // select by mask
142691
- ivec2 maskUV = ivec2((ndc.xy * vec2(0.5, -0.5) + 0.5) * mask_params);
142692
- clr[i] = texelFetch(mask, maskUV, 0).a < 1.0 ? 0.0 : 1.0;
142693
- } else if (mode == 1) {
142694
- // select by rect
142695
- 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;
142696
- } else if (mode == 2) {
142697
- // select by sphere
142698
- clr[i] = length(world - sphere_params.xyz) < sphere_params.w ? 1.0 : 0.0;
142699
- } else if (mode == 3) {
142700
- // select by box
142701
- vec3 relativePosition = world - box_params.xyz;
142702
- bool isInsideCube = true;
142703
- if (relativePosition.x < -aabb_params.x || relativePosition.x > aabb_params.x) {
142704
- isInsideCube = false;
142705
- }
142706
- if (relativePosition.y < -aabb_params.y || relativePosition.y > aabb_params.y) {
142707
- isInsideCube = false;
142708
- }
142709
- if (relativePosition.z < -aabb_params.z || relativePosition.z > aabb_params.z) {
142710
- isInsideCube = false;
142711
- }
142712
- clr[i] = isInsideCube ? 1.0 : 0.0;
142713
- }
142714
- }
142715
- }
142716
-
142717
- gl_FragColor = clr;
142718
- }
142719
- `;
142720
-
142721
- const vertexShader$4 = /* glsl */ `
142722
- attribute vec2 vertex_position;
142723
- void main(void) {
142724
- gl_Position = vec4(vertex_position, 0.0, 1.0);
142725
- }
142726
- `;
142727
- const fragmentShader$4 = /* glsl */ `
142728
- uniform highp usampler2D transformA; // splat center x, y, z
142729
- uniform highp usampler2D splatTransform; // transform palette index
142730
- uniform sampler2D transformPalette; // palette of transforms
142731
- uniform ivec2 splat_params; // splat texture width, num splats
142732
-
142733
- void main(void) {
142734
- // calculate output id
142735
- ivec2 splatUV = ivec2(gl_FragCoord);
142736
-
142737
- // skip if splat index is out of bounds
142738
- if (splatUV.x + splatUV.y * splat_params.x >= splat_params.y) {
142739
- discard;
142740
- }
142741
-
142742
- // read splat center
142743
- vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
142744
-
142745
- // apply optional per-splat transform
142746
- uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
142747
- if (transformIndex > 0u) {
142748
- // read transform matrix
142749
- int u = int(transformIndex % 512u) * 3;
142750
- int v = int(transformIndex / 512u);
142751
-
142752
- mat3x4 t;
142753
- t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
142754
- t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
142755
- t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
142756
-
142757
- center = vec4(center, 1.0) * t;
142758
- }
142759
-
142760
- gl_FragColor = vec4(center, 0.0);
142761
- }
143176
+ const vertexShader$6 = /* glsl */ `
143177
+ attribute vec2 vertex_position;
143178
+ void main(void) {
143179
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
143180
+ }
143181
+ `;
143182
+ const fragmentShader$6 = /* glsl */ `
143183
+ uniform highp usampler2D transformA; // splat center x, y, z
143184
+ uniform highp usampler2D splatTransform; // transform palette index
143185
+ uniform sampler2D transformPalette; // palette of transforms
143186
+ uniform sampler2D splatState; // per-splat state
143187
+ uniform highp ivec3 splat_params; // texture width, texture height, num splats
143188
+ uniform highp uint mode; // 0: selected, 1: visible
143189
+
143190
+ // calculate min and max for a single column of splats
143191
+ void main(void) {
143192
+
143193
+ vec3 boundMin = vec3(1e6);
143194
+ vec3 boundMax = vec3(-1e6);
143195
+
143196
+ for (int id = 0; id < splat_params.y; id++) {
143197
+ // calculate splatUV
143198
+ ivec2 splatUV = ivec2(gl_FragCoord.x, id);
143199
+
143200
+ // skip out-of-range splats
143201
+ if ((splatUV.x + splatUV.y * splat_params.x) >= splat_params.z) {
143202
+ continue;
143203
+ }
143204
+
143205
+ // read splat state
143206
+ uint state = uint(texelFetch(splatState, splatUV, 0).r * 255.0);
143207
+
143208
+ // skip deleted or locked splats
143209
+ if (((mode == 0u) && (state != 1u)) || ((mode == 1u) && ((state & 4u) != 0u))) {
143210
+ continue;
143211
+ }
143212
+
143213
+ // read splat center
143214
+ vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
143215
+
143216
+ // apply optional per-splat transform
143217
+ uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
143218
+ if (transformIndex > 0u) {
143219
+ // read transform matrix
143220
+ int u = int(transformIndex % 512u) * 3;
143221
+ int v = int(transformIndex / 512u);
143222
+
143223
+ mat3x4 t;
143224
+ t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
143225
+ t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
143226
+ t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
143227
+
143228
+ center = vec4(center, 1.0) * t;
143229
+ }
143230
+
143231
+ boundMin = min(boundMin, mix(center, boundMin, isinf(center)));
143232
+ boundMax = max(boundMax, mix(center, boundMax, isinf(center)));
143233
+ }
143234
+
143235
+ pcFragColor0 = vec4(boundMin, 0.0);
143236
+ pcFragColor1 = vec4(boundMax, 0.0);
143237
+ }
143238
+ `;
143239
+
143240
+ const vertexShader$5 = /* glsl */ `
143241
+ attribute vec2 vertex_position;
143242
+ void main(void) {
143243
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
143244
+ }
143245
+ `;
143246
+ const fragmentShader$5 = /* glsl */ `
143247
+ uniform highp usampler2D transformA; // splat center x, y, z
143248
+ uniform highp usampler2D splatTransform; // transform palette index
143249
+ uniform sampler2D transformPalette; // palette of transforms
143250
+ uniform uvec2 splat_params; // splat texture width, num splats
143251
+
143252
+ uniform mat4 matrix_model;
143253
+ uniform mat4 matrix_viewProjection;
143254
+
143255
+ uniform uvec2 output_params; // output width, height
143256
+
143257
+ // 0: mask, 1: rect, 2: sphere
143258
+ uniform int mode;
143259
+
143260
+ // mask params
143261
+ uniform sampler2D mask; // mask in alpha channel
143262
+ uniform vec2 mask_params; // mask width, height
143263
+
143264
+ // rect params
143265
+ uniform vec4 rect_params; // rect x, y, width, height
143266
+
143267
+ // sphere params
143268
+ uniform vec4 sphere_params; // sphere x, y, z, radius
143269
+
143270
+ // box params
143271
+ uniform vec4 box_params; // box x, y, z
143272
+ uniform vec4 aabb_params; // len x, y, z
143273
+
143274
+ void main(void) {
143275
+ // calculate output id
143276
+ uvec2 outputUV = uvec2(gl_FragCoord);
143277
+ uint outputId = (outputUV.x + outputUV.y * output_params.x) * 4u;
143278
+
143279
+ vec4 clr = vec4(0.0);
143280
+
143281
+ for (uint i = 0u; i < 4u; i++) {
143282
+ uint id = outputId + i;
143283
+
143284
+ if (id >= splat_params.y) {
143285
+ continue;
143286
+ }
143287
+
143288
+ // calculate splatUV
143289
+ ivec2 splatUV = ivec2(
143290
+ int(id % splat_params.x),
143291
+ int(id / splat_params.x)
143292
+ );
143293
+
143294
+ // read splat center
143295
+ vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
143296
+
143297
+ // apply optional per-splat transform
143298
+ uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
143299
+ if (transformIndex > 0u) {
143300
+ // read transform matrix
143301
+ int u = int(transformIndex % 512u) * 3;
143302
+ int v = int(transformIndex / 512u);
143303
+
143304
+ mat3x4 t;
143305
+ t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
143306
+ t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
143307
+ t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
143308
+
143309
+ center = vec4(center, 1.0) * t;
143310
+ }
143311
+
143312
+ // transform to clip space and discard if outside
143313
+ vec3 world = (matrix_model * vec4(center, 1.0)).xyz;
143314
+ vec4 clip = matrix_viewProjection * vec4(world, 1.0);
143315
+ vec3 ndc = clip.xyz / clip.w;
143316
+
143317
+ // skip offscreen fragments
143318
+ if (!any(greaterThan(abs(ndc), vec3(1.0)))) {
143319
+ if (mode == 0) {
143320
+ // select by mask
143321
+ ivec2 maskUV = ivec2((ndc.xy * vec2(0.5, -0.5) + 0.5) * mask_params);
143322
+ clr[i] = texelFetch(mask, maskUV, 0).a < 1.0 ? 0.0 : 1.0;
143323
+ } else if (mode == 1) {
143324
+ // select by rect
143325
+ 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;
143326
+ } else if (mode == 2) {
143327
+ // select by sphere
143328
+ clr[i] = length(world - sphere_params.xyz) < sphere_params.w ? 1.0 : 0.0;
143329
+ } else if (mode == 3) {
143330
+ // select by box
143331
+ vec3 relativePosition = world - box_params.xyz;
143332
+ bool isInsideCube = true;
143333
+ if (relativePosition.x < -aabb_params.x || relativePosition.x > aabb_params.x) {
143334
+ isInsideCube = false;
143335
+ }
143336
+ if (relativePosition.y < -aabb_params.y || relativePosition.y > aabb_params.y) {
143337
+ isInsideCube = false;
143338
+ }
143339
+ if (relativePosition.z < -aabb_params.z || relativePosition.z > aabb_params.z) {
143340
+ isInsideCube = false;
143341
+ }
143342
+ clr[i] = isInsideCube ? 1.0 : 0.0;
143343
+ }
143344
+ }
143345
+ }
143346
+
143347
+ gl_FragColor = clr;
143348
+ }
143349
+ `;
143350
+
143351
+ const vertexShader$4 = /* glsl */ `
143352
+ attribute vec2 vertex_position;
143353
+ void main(void) {
143354
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
143355
+ }
143356
+ `;
143357
+ const fragmentShader$4 = /* glsl */ `
143358
+ uniform highp usampler2D transformA; // splat center x, y, z
143359
+ uniform highp usampler2D splatTransform; // transform palette index
143360
+ uniform sampler2D transformPalette; // palette of transforms
143361
+ uniform ivec2 splat_params; // splat texture width, num splats
143362
+
143363
+ void main(void) {
143364
+ // calculate output id
143365
+ ivec2 splatUV = ivec2(gl_FragCoord);
143366
+
143367
+ // skip if splat index is out of bounds
143368
+ if (splatUV.x + splatUV.y * splat_params.x >= splat_params.y) {
143369
+ discard;
143370
+ }
143371
+
143372
+ // read splat center
143373
+ vec3 center = uintBitsToFloat(texelFetch(transformA, splatUV, 0).xyz);
143374
+
143375
+ // apply optional per-splat transform
143376
+ uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
143377
+ if (transformIndex > 0u) {
143378
+ // read transform matrix
143379
+ int u = int(transformIndex % 512u) * 3;
143380
+ int v = int(transformIndex / 512u);
143381
+
143382
+ mat3x4 t;
143383
+ t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
143384
+ t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
143385
+ t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
143386
+
143387
+ center = vec4(center, 1.0) * t;
143388
+ }
143389
+
143390
+ gl_FragColor = vec4(center, 0.0);
143391
+ }
142762
143392
  `;
142763
143393
 
142764
143394
  const v1 = new Vec3();
@@ -142795,18 +143425,18 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
142795
143425
  attributes: {
142796
143426
  vertex_position: SEMANTIC_POSITION
142797
143427
  },
142798
- vertexGLSL: `
142799
- attribute vec2 vertex_position;
142800
- void main(void) {
142801
- gl_Position = vec4(vertex_position, 0.0, 1.0);
142802
- }
143428
+ vertexGLSL: `
143429
+ attribute vec2 vertex_position;
143430
+ void main(void) {
143431
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
143432
+ }
142803
143433
  `,
142804
- fragmentGLSL: `
142805
- uniform sampler2D colorTex;
142806
- void main(void) {
142807
- ivec2 texel = ivec2(gl_FragCoord.xy);
142808
- gl_FragColor = texelFetch(colorTex, texel, 0);
142809
- }
143434
+ fragmentGLSL: `
143435
+ uniform sampler2D colorTex;
143436
+ void main(void) {
143437
+ ivec2 texel = ivec2(gl_FragCoord.xy);
143438
+ gl_FragColor = texelFetch(colorTex, texel, 0);
143439
+ }
142810
143440
  `
142811
143441
  });
142812
143442
  // intersection test
@@ -144045,176 +144675,176 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
144045
144675
  }
144046
144676
  }
144047
144677
 
144048
- const vertexShader$3 = /* glsl*/ `
144049
- uniform vec3 near_origin;
144050
- uniform vec3 near_x;
144051
- uniform vec3 near_y;
144052
-
144053
- uniform vec3 far_origin;
144054
- uniform vec3 far_x;
144055
- uniform vec3 far_y;
144056
-
144057
- attribute vec2 vertex_position;
144058
-
144059
- varying vec3 worldFar;
144060
- varying vec3 worldNear;
144061
-
144062
- void main(void) {
144063
- gl_Position = vec4(vertex_position, 0.0, 1.0);
144064
-
144065
- vec2 p = vertex_position * 0.5 + 0.5;
144066
- worldNear = near_origin + near_x * p.x + near_y * p.y;
144067
- worldFar = far_origin + far_x * p.x + far_y * p.y;
144068
- }
144069
- `;
144070
- const fragmentShader$3 = /* glsl*/ `
144071
- uniform vec3 view_position;
144072
- uniform mat4 matrix_viewProjection;
144073
- uniform sampler2D blueNoiseTex32;
144074
-
144075
- uniform int plane; // 0: x (yz), 1: y (xz), 2: z (xy)
144076
-
144077
- vec4 planes[3] = vec4[3](
144078
- vec4(1.0, 0.0, 0.0, 0.0),
144079
- vec4(0.0, 1.0, 0.0, 0.0),
144080
- vec4(0.0, 0.0, 1.0, 0.0)
144081
- );
144082
-
144083
- vec3 colors[3] = vec3[3](
144084
- vec3(1.0, 0.2, 0.2),
144085
- vec3(0.2, 1.0, 0.2),
144086
- vec3(0.2, 0.2, 1.0)
144087
- );
144088
-
144089
- int axis0[3] = int[3](1, 0, 0);
144090
- int axis1[3] = int[3](2, 2, 1);
144091
-
144092
- varying vec3 worldNear;
144093
- varying vec3 worldFar;
144094
-
144095
- bool intersectPlane(inout float t, vec3 pos, vec3 dir, vec4 plane) {
144096
- float d = dot(dir, plane.xyz);
144097
- if (abs(d) < 1e-06) {
144098
- return false;
144099
- }
144100
-
144101
- float n = -(dot(pos, plane.xyz) + plane.w) / d;
144102
- if (n < 0.0) {
144103
- return false;
144104
- }
144105
-
144106
- t = n;
144107
-
144108
- return true;
144109
- }
144110
-
144111
- // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c
144112
- float pristineGrid(in vec2 uv, in vec2 ddx, in vec2 ddy, vec2 lineWidth) {
144113
- vec2 uvDeriv = vec2(length(vec2(ddx.x, ddy.x)), length(vec2(ddx.y, ddy.y)));
144114
- bvec2 invertLine = bvec2(lineWidth.x > 0.5, lineWidth.y > 0.5);
144115
- vec2 targetWidth = vec2(
144116
- invertLine.x ? 1.0 - lineWidth.x : lineWidth.x,
144117
- invertLine.y ? 1.0 - lineWidth.y : lineWidth.y
144118
- );
144119
- vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5));
144120
- vec2 lineAA = uvDeriv * 1.5;
144121
- vec2 gridUV = abs(fract(uv) * 2.0 - 1.0);
144122
- gridUV.x = invertLine.x ? gridUV.x : 1.0 - gridUV.x;
144123
- gridUV.y = invertLine.y ? gridUV.y : 1.0 - gridUV.y;
144124
- vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
144125
-
144126
- grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0);
144127
- grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0));
144128
- grid2.x = invertLine.x ? 1.0 - grid2.x : grid2.x;
144129
- grid2.y = invertLine.y ? 1.0 - grid2.y : grid2.y;
144130
-
144131
- return mix(grid2.x, 1.0, grid2.y);
144132
- }
144133
-
144134
- float calcDepth(vec3 p) {
144135
- vec4 v = matrix_viewProjection * vec4(p, 1.0);
144136
- return (v.z / v.w) * 0.5 + 0.5;
144137
- }
144138
-
144139
- bool writeDepth(float alpha) {
144140
- vec2 uv = fract(gl_FragCoord.xy / 32.0);
144141
- float noise = texture2DLod(blueNoiseTex32, uv, 0.0).y;
144142
- return alpha > noise;
144143
- }
144144
-
144145
- void main(void) {
144146
- vec3 p = worldNear;
144147
- vec3 v = normalize(worldFar - worldNear);
144148
-
144149
- // intersect ray with the world xz plane
144150
- float t;
144151
- if (!intersectPlane(t, p, v, planes[plane])) {
144152
- discard;
144153
- }
144154
-
144155
- // calculate grid intersection
144156
- vec3 worldPos = p + v * t;
144157
- vec2 pos = plane == 0 ? worldPos.yz : (plane == 1 ? worldPos.xz : worldPos.xy);
144158
- vec2 ddx = dFdx(pos);
144159
- vec2 ddy = dFdy(pos);
144160
-
144161
- float epsilon = 1.0 / 255.0;
144162
-
144163
- // calculate fade
144164
- float fade = 1.0 - smoothstep(400.0, 1000.0, length(worldPos - view_position));
144165
- if (fade < epsilon) {
144166
- discard;
144167
- }
144168
-
144169
- vec2 levelPos;
144170
- float levelSize;
144171
- float levelAlpha;
144172
-
144173
- // 10m grid with colored main axes
144174
- levelPos = pos * 0.1;
144175
- levelSize = 2.0 / 1000.0;
144176
- levelAlpha = pristineGrid(levelPos, ddx * 0.1, ddy * 0.1, vec2(levelSize)) * fade;
144177
- if (levelAlpha > epsilon) {
144178
- vec3 color;
144179
- vec2 loc = abs(levelPos);
144180
- if (loc.x < levelSize) {
144181
- if (loc.y < levelSize) {
144182
- color = vec3(1.0);
144183
- } else {
144184
- color = colors[axis1[plane]];
144185
- }
144186
- } else if (loc.y < levelSize) {
144187
- color = colors[axis0[plane]];
144188
- } else {
144189
- color = vec3(0.9);
144190
- }
144191
- gl_FragColor = vec4(color, levelAlpha);
144192
- gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144193
- return;
144194
- }
144195
-
144196
- // 1m grid
144197
- levelPos = pos;
144198
- levelSize = 1.0 / 100.0;
144199
- levelAlpha = pristineGrid(levelPos, ddx, ddy, vec2(levelSize)) * fade;
144200
- if (levelAlpha > epsilon) {
144201
- gl_FragColor = vec4(vec3(0.7), levelAlpha);
144202
- gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144203
- return;
144204
- }
144205
-
144206
- // 0.1m grid
144207
- levelPos = pos * 10.0;
144208
- levelSize = 1.0 / 100.0;
144209
- levelAlpha = pristineGrid(levelPos, ddx * 10.0, ddy * 10.0, vec2(levelSize)) * fade;
144210
- if (levelAlpha > epsilon) {
144211
- gl_FragColor = vec4(vec3(0.7), levelAlpha);
144212
- gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144213
- return;
144214
- }
144215
-
144216
- discard;
144217
- }
144678
+ const vertexShader$3 = /* glsl*/ `
144679
+ uniform vec3 near_origin;
144680
+ uniform vec3 near_x;
144681
+ uniform vec3 near_y;
144682
+
144683
+ uniform vec3 far_origin;
144684
+ uniform vec3 far_x;
144685
+ uniform vec3 far_y;
144686
+
144687
+ attribute vec2 vertex_position;
144688
+
144689
+ varying vec3 worldFar;
144690
+ varying vec3 worldNear;
144691
+
144692
+ void main(void) {
144693
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
144694
+
144695
+ vec2 p = vertex_position * 0.5 + 0.5;
144696
+ worldNear = near_origin + near_x * p.x + near_y * p.y;
144697
+ worldFar = far_origin + far_x * p.x + far_y * p.y;
144698
+ }
144699
+ `;
144700
+ const fragmentShader$3 = /* glsl*/ `
144701
+ uniform vec3 view_position;
144702
+ uniform mat4 matrix_viewProjection;
144703
+ uniform sampler2D blueNoiseTex32;
144704
+
144705
+ uniform int plane; // 0: x (yz), 1: y (xz), 2: z (xy)
144706
+
144707
+ vec4 planes[3] = vec4[3](
144708
+ vec4(1.0, 0.0, 0.0, 0.0),
144709
+ vec4(0.0, 1.0, 0.0, 0.0),
144710
+ vec4(0.0, 0.0, 1.0, 0.0)
144711
+ );
144712
+
144713
+ vec3 colors[3] = vec3[3](
144714
+ vec3(1.0, 0.2, 0.2),
144715
+ vec3(0.2, 1.0, 0.2),
144716
+ vec3(0.2, 0.2, 1.0)
144717
+ );
144718
+
144719
+ int axis0[3] = int[3](1, 0, 0);
144720
+ int axis1[3] = int[3](2, 2, 1);
144721
+
144722
+ varying vec3 worldNear;
144723
+ varying vec3 worldFar;
144724
+
144725
+ bool intersectPlane(inout float t, vec3 pos, vec3 dir, vec4 plane) {
144726
+ float d = dot(dir, plane.xyz);
144727
+ if (abs(d) < 1e-06) {
144728
+ return false;
144729
+ }
144730
+
144731
+ float n = -(dot(pos, plane.xyz) + plane.w) / d;
144732
+ if (n < 0.0) {
144733
+ return false;
144734
+ }
144735
+
144736
+ t = n;
144737
+
144738
+ return true;
144739
+ }
144740
+
144741
+ // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c
144742
+ float pristineGrid(in vec2 uv, in vec2 ddx, in vec2 ddy, vec2 lineWidth) {
144743
+ vec2 uvDeriv = vec2(length(vec2(ddx.x, ddy.x)), length(vec2(ddx.y, ddy.y)));
144744
+ bvec2 invertLine = bvec2(lineWidth.x > 0.5, lineWidth.y > 0.5);
144745
+ vec2 targetWidth = vec2(
144746
+ invertLine.x ? 1.0 - lineWidth.x : lineWidth.x,
144747
+ invertLine.y ? 1.0 - lineWidth.y : lineWidth.y
144748
+ );
144749
+ vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5));
144750
+ vec2 lineAA = uvDeriv * 1.5;
144751
+ vec2 gridUV = abs(fract(uv) * 2.0 - 1.0);
144752
+ gridUV.x = invertLine.x ? gridUV.x : 1.0 - gridUV.x;
144753
+ gridUV.y = invertLine.y ? gridUV.y : 1.0 - gridUV.y;
144754
+ vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV);
144755
+
144756
+ grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0);
144757
+ grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0));
144758
+ grid2.x = invertLine.x ? 1.0 - grid2.x : grid2.x;
144759
+ grid2.y = invertLine.y ? 1.0 - grid2.y : grid2.y;
144760
+
144761
+ return mix(grid2.x, 1.0, grid2.y);
144762
+ }
144763
+
144764
+ float calcDepth(vec3 p) {
144765
+ vec4 v = matrix_viewProjection * vec4(p, 1.0);
144766
+ return (v.z / v.w) * 0.5 + 0.5;
144767
+ }
144768
+
144769
+ bool writeDepth(float alpha) {
144770
+ vec2 uv = fract(gl_FragCoord.xy / 32.0);
144771
+ float noise = texture2DLod(blueNoiseTex32, uv, 0.0).y;
144772
+ return alpha > noise;
144773
+ }
144774
+
144775
+ void main(void) {
144776
+ vec3 p = worldNear;
144777
+ vec3 v = normalize(worldFar - worldNear);
144778
+
144779
+ // intersect ray with the world xz plane
144780
+ float t;
144781
+ if (!intersectPlane(t, p, v, planes[plane])) {
144782
+ discard;
144783
+ }
144784
+
144785
+ // calculate grid intersection
144786
+ vec3 worldPos = p + v * t;
144787
+ vec2 pos = plane == 0 ? worldPos.yz : (plane == 1 ? worldPos.xz : worldPos.xy);
144788
+ vec2 ddx = dFdx(pos);
144789
+ vec2 ddy = dFdy(pos);
144790
+
144791
+ float epsilon = 1.0 / 255.0;
144792
+
144793
+ // calculate fade
144794
+ float fade = 1.0 - smoothstep(400.0, 1000.0, length(worldPos - view_position));
144795
+ if (fade < epsilon) {
144796
+ discard;
144797
+ }
144798
+
144799
+ vec2 levelPos;
144800
+ float levelSize;
144801
+ float levelAlpha;
144802
+
144803
+ // 10m grid with colored main axes
144804
+ levelPos = pos * 0.1;
144805
+ levelSize = 2.0 / 1000.0;
144806
+ levelAlpha = pristineGrid(levelPos, ddx * 0.1, ddy * 0.1, vec2(levelSize)) * fade;
144807
+ if (levelAlpha > epsilon) {
144808
+ vec3 color;
144809
+ vec2 loc = abs(levelPos);
144810
+ if (loc.x < levelSize) {
144811
+ if (loc.y < levelSize) {
144812
+ color = vec3(1.0);
144813
+ } else {
144814
+ color = colors[axis1[plane]];
144815
+ }
144816
+ } else if (loc.y < levelSize) {
144817
+ color = colors[axis0[plane]];
144818
+ } else {
144819
+ color = vec3(0.9);
144820
+ }
144821
+ gl_FragColor = vec4(color, levelAlpha);
144822
+ gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144823
+ return;
144824
+ }
144825
+
144826
+ // 1m grid
144827
+ levelPos = pos;
144828
+ levelSize = 1.0 / 100.0;
144829
+ levelAlpha = pristineGrid(levelPos, ddx, ddy, vec2(levelSize)) * fade;
144830
+ if (levelAlpha > epsilon) {
144831
+ gl_FragColor = vec4(vec3(0.7), levelAlpha);
144832
+ gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144833
+ return;
144834
+ }
144835
+
144836
+ // 0.1m grid
144837
+ levelPos = pos * 10.0;
144838
+ levelSize = 1.0 / 100.0;
144839
+ levelAlpha = pristineGrid(levelPos, ddx * 10.0, ddy * 10.0, vec2(levelSize)) * fade;
144840
+ if (levelAlpha > epsilon) {
144841
+ gl_FragColor = vec4(vec3(0.7), levelAlpha);
144842
+ gl_FragDepth = writeDepth(levelAlpha) ? calcDepth(worldPos) : 1.0;
144843
+ return;
144844
+ }
144845
+
144846
+ discard;
144847
+ }
144218
144848
  `;
144219
144849
 
144220
144850
  const resolve = (scope, values) => {
@@ -144285,36 +144915,36 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
144285
144915
  }
144286
144916
  }
144287
144917
 
144288
- const vertexShader$2 = /* glsl*/ `
144289
- attribute vec2 vertex_position;
144290
- void main(void) {
144291
- gl_Position = vec4(vertex_position, 0.0, 1.0);
144292
- }
144293
- `;
144294
- const fragmentShader$2 = /* glsl*/ `
144295
- uniform sampler2D outlineTexture;
144296
- uniform float alphaCutoff;
144297
- uniform vec4 clr;
144298
-
144299
- void main(void) {
144300
- ivec2 texel = ivec2(gl_FragCoord.xy);
144301
-
144302
- // skip solid pixels
144303
- if (texelFetch(outlineTexture, texel, 0).a > alphaCutoff) {
144304
- discard;
144305
- }
144306
-
144307
- for (int x = -2; x <= 2; x++) {
144308
- for (int y = -2; y <= 2; y++) {
144309
- if ((x != 0) && (y != 0) && (texelFetch(outlineTexture, texel + ivec2(x, y), 0).a > alphaCutoff)) {
144310
- gl_FragColor = clr;
144311
- return;
144312
- }
144313
- }
144314
- }
144315
-
144316
- discard;
144317
- }
144918
+ const vertexShader$2 = /* glsl*/ `
144919
+ attribute vec2 vertex_position;
144920
+ void main(void) {
144921
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
144922
+ }
144923
+ `;
144924
+ const fragmentShader$2 = /* glsl*/ `
144925
+ uniform sampler2D outlineTexture;
144926
+ uniform float alphaCutoff;
144927
+ uniform vec4 clr;
144928
+
144929
+ void main(void) {
144930
+ ivec2 texel = ivec2(gl_FragCoord.xy);
144931
+
144932
+ // skip solid pixels
144933
+ if (texelFetch(outlineTexture, texel, 0).a > alphaCutoff) {
144934
+ discard;
144935
+ }
144936
+
144937
+ for (int x = -2; x <= 2; x++) {
144938
+ for (int y = -2; y <= 2; y++) {
144939
+ if ((x != 0) && (y != 0) && (texelFetch(outlineTexture, texel + ivec2(x, y), 0).a > alphaCutoff)) {
144940
+ gl_FragColor = clr;
144941
+ return;
144942
+ }
144943
+ }
144944
+ }
144945
+
144946
+ discard;
144947
+ }
144318
144948
  `;
144319
144949
 
144320
144950
  class Outline extends Element$1 {
@@ -144632,72 +145262,72 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
144632
145262
  }
144633
145263
  }
144634
145264
 
144635
- const vertexShader$1 = /* glsl */ `
144636
- attribute uint vertex_id;
144637
-
144638
- uniform mat4 matrix_model;
144639
- uniform mat4 matrix_viewProjection;
144640
-
144641
- uniform sampler2D splatState;
144642
- uniform highp usampler2D splatPosition;
144643
- uniform highp usampler2D splatTransform; // per-splat index into transform palette
144644
- uniform sampler2D transformPalette; // palette of transform matrices
144645
-
144646
- uniform uvec2 texParams;
144647
-
144648
- uniform float splatSize;
144649
- uniform vec4 selectedClr;
144650
- uniform vec4 unselectedClr;
144651
-
144652
- varying vec4 varying_color;
144653
-
144654
- // calculate the current splat index and uv
144655
- ivec2 calcSplatUV(uint index, uint width) {
144656
- return ivec2(int(index % width), int(index / width));
144657
- }
144658
-
144659
- void main(void) {
144660
- ivec2 splatUV = calcSplatUV(vertex_id, texParams.x);
144661
- uint splatState = uint(texelFetch(splatState, splatUV, 0).r * 255.0);
144662
-
144663
- if ((splatState & 6u) != 0u) {
144664
- // deleted or locked (4 or 2)
144665
- gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
144666
- gl_PointSize = 0.0;
144667
- } else {
144668
- mat4 model = matrix_model;
144669
-
144670
- // handle per-splat transform
144671
- uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
144672
- if (transformIndex > 0u) {
144673
- // read transform matrix
144674
- int u = int(transformIndex % 512u) * 3;
144675
- int v = int(transformIndex / 512u);
144676
-
144677
- mat4 t;
144678
- t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
144679
- t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
144680
- t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
144681
- t[3] = vec4(0.0, 0.0, 0.0, 1.0);
144682
-
144683
- model = matrix_model * transpose(t);
144684
- }
144685
-
144686
- varying_color = (splatState == 1u) ? selectedClr : unselectedClr;
144687
-
144688
- vec3 center = uintBitsToFloat(texelFetch(splatPosition, splatUV, 0).xyz);
144689
-
144690
- gl_Position = matrix_viewProjection * model * vec4(center, 1.0);
144691
- gl_PointSize = splatSize;
144692
- }
144693
- }
144694
- `;
144695
- const fragmentShader$1 = /* glsl */ `
144696
- varying vec4 varying_color;
144697
-
144698
- void main(void) {
144699
- gl_FragColor = varying_color;
144700
- }
145265
+ const vertexShader$1 = /* glsl */ `
145266
+ attribute uint vertex_id;
145267
+
145268
+ uniform mat4 matrix_model;
145269
+ uniform mat4 matrix_viewProjection;
145270
+
145271
+ uniform sampler2D splatState;
145272
+ uniform highp usampler2D splatPosition;
145273
+ uniform highp usampler2D splatTransform; // per-splat index into transform palette
145274
+ uniform sampler2D transformPalette; // palette of transform matrices
145275
+
145276
+ uniform uvec2 texParams;
145277
+
145278
+ uniform float splatSize;
145279
+ uniform vec4 selectedClr;
145280
+ uniform vec4 unselectedClr;
145281
+
145282
+ varying vec4 varying_color;
145283
+
145284
+ // calculate the current splat index and uv
145285
+ ivec2 calcSplatUV(uint index, uint width) {
145286
+ return ivec2(int(index % width), int(index / width));
145287
+ }
145288
+
145289
+ void main(void) {
145290
+ ivec2 splatUV = calcSplatUV(vertex_id, texParams.x);
145291
+ uint splatState = uint(texelFetch(splatState, splatUV, 0).r * 255.0);
145292
+
145293
+ if ((splatState & 6u) != 0u) {
145294
+ // deleted or locked (4 or 2)
145295
+ gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
145296
+ gl_PointSize = 0.0;
145297
+ } else {
145298
+ mat4 model = matrix_model;
145299
+
145300
+ // handle per-splat transform
145301
+ uint transformIndex = texelFetch(splatTransform, splatUV, 0).r;
145302
+ if (transformIndex > 0u) {
145303
+ // read transform matrix
145304
+ int u = int(transformIndex % 512u) * 3;
145305
+ int v = int(transformIndex / 512u);
145306
+
145307
+ mat4 t;
145308
+ t[0] = texelFetch(transformPalette, ivec2(u, v), 0);
145309
+ t[1] = texelFetch(transformPalette, ivec2(u + 1, v), 0);
145310
+ t[2] = texelFetch(transformPalette, ivec2(u + 2, v), 0);
145311
+ t[3] = vec4(0.0, 0.0, 0.0, 1.0);
145312
+
145313
+ model = matrix_model * transpose(t);
145314
+ }
145315
+
145316
+ varying_color = (splatState == 1u) ? selectedClr : unselectedClr;
145317
+
145318
+ vec3 center = uintBitsToFloat(texelFetch(splatPosition, splatUV, 0).xyz);
145319
+
145320
+ gl_Position = matrix_viewProjection * model * vec4(center, 1.0);
145321
+ gl_PointSize = splatSize;
145322
+ }
145323
+ }
145324
+ `;
145325
+ const fragmentShader$1 = /* glsl */ `
145326
+ varying vec4 varying_color;
145327
+
145328
+ void main(void) {
145329
+ gl_FragColor = varying_color;
145330
+ }
144701
145331
  `;
144702
145332
 
144703
145333
  class SplatOverlay extends Element$1 {
@@ -144786,19 +145416,19 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
144786
145416
  }
144787
145417
  }
144788
145418
 
144789
- const vertexShader = /* glsl*/ `
144790
- attribute vec2 vertex_position;
144791
- void main(void) {
144792
- gl_Position = vec4(vertex_position, 0.0, 1.0);
144793
- }
145419
+ const vertexShader = /* glsl*/ `
145420
+ attribute vec2 vertex_position;
145421
+ void main(void) {
145422
+ gl_Position = vec4(vertex_position, 0.0, 1.0);
145423
+ }
144794
145424
  `;
144795
- const fragmentShader = /* glsl*/ `
144796
- uniform sampler2D blitTexture;
144797
- void main(void) {
144798
- ivec2 texel = ivec2(gl_FragCoord.xy);
144799
-
144800
- gl_FragColor = texelFetch(blitTexture, texel, 0);
144801
- }
145425
+ const fragmentShader = /* glsl*/ `
145426
+ uniform sampler2D blitTexture;
145427
+ void main(void) {
145428
+ ivec2 texel = ivec2(gl_FragCoord.xy);
145429
+
145430
+ gl_FragColor = texelFetch(blitTexture, texel, 0);
145431
+ }
144802
145432
  `;
144803
145433
 
144804
145434
  class Underlay extends Element$1 {
@@ -146763,7 +147393,7 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
146763
147393
  const { config } = this.scene;
146764
147394
  const state = this.viewerEventState;
146765
147395
  // Colors and view settings from scene config
146766
- const selectedClr = config.selectedClr ?? { r: 1, g: 1, b: 0, a: 1 };
147396
+ const selectedClr = config.selectedClr ?? { r: 1, g: 0.5, b: 0, a: 1 };
146767
147397
  const unselectedClr = config.unselectedClr ?? { r: 0, g: 0, b: 1, a: 0.5 };
146768
147398
  const lockedClr = config.lockedClr ?? { r: 0, g: 0, b: 0, a: 0.05 };
146769
147399
  const bgClr = config.bgClr ?? { r: 0, g: 0, b: 0, a: 1 };
@@ -147075,6 +147705,7 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
147075
147705
  };
147076
147706
  this.enableStats = false;
147077
147707
  this.autoFocus = true;
147708
+ this.previewMode = false;
147078
147709
  this.isLoading = false;
147079
147710
  this.hasModel = false;
147080
147711
  this.error = null;
@@ -147127,6 +147758,11 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
147127
147758
  this.canvas = options.canvas || null;
147128
147759
  this.enableStats = options.enableStats || false;
147129
147760
  this.autoFocus = options.autoFocus !== false;
147761
+ this.previewMode = options.previewMode || false;
147762
+ // In preview mode, always enable auto-focus
147763
+ if (this.previewMode) {
147764
+ this.autoFocus = true;
147765
+ }
147130
147766
  this._navigationCubeConfig = options.navigationCube || null;
147131
147767
  if (options.onStatsUpdate !== undefined) {
147132
147768
  this._onStatsUpdate = options.onStatsUpdate;
@@ -147157,11 +147793,20 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
147157
147793
  const emitEvent = (type, detail) => {
147158
147794
  this.emit({ type, detail });
147159
147795
  };
147160
- this._supersplat = new SupersplatAdapter(this.canvas, this._navigationCubeConfig, emitEvent);
147796
+ this._supersplat = new SupersplatAdapter(this.canvas,
147797
+ // Disable navigation cube in preview mode
147798
+ this.previewMode ? null : this._navigationCubeConfig, emitEvent);
147161
147799
  this._supersplatReady = this._supersplat.init();
147162
147800
  this._supersplatReady
147163
147801
  ?.then(() => {
147164
- this._setupFlyCameraForSupersplat();
147802
+ // Disable camera controls in preview mode
147803
+ if (this.previewMode && this._supersplat) {
147804
+ this._supersplat.setCameraControlsEnabled?.(false);
147805
+ }
147806
+ // Only set up fly camera if not in preview mode
147807
+ if (!this.previewMode) {
147808
+ this._setupFlyCameraForSupersplat();
147809
+ }
147165
147810
  })
147166
147811
  .catch(error => {
147167
147812
  console.error('SplatViewerCore.init: Failed to set up fly camera for supersplat path', error);
@@ -147197,6 +147842,9 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
147197
147842
  this.app = ctx.app;
147198
147843
  this.entities.camera = ctx.camera;
147199
147844
  const cameraAny = this.entities.camera;
147845
+ // SuperSplat's PCApp omits ScriptComponentSystem, so `addComponent('script')`
147846
+ // will not produce `camera.script.create()`. We keep the attempt (in case
147847
+ // the underlying app changes), but also support a controller-based fallback.
147200
147848
  if (cameraAny &&
147201
147849
  !cameraAny.script &&
147202
147850
  typeof cameraAny.addComponent === 'function') {
@@ -147222,16 +147870,25 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
147222
147870
  minHeight: DEFAULT_FLY_CAMERA_CONFIG.minHeight,
147223
147871
  maxHeight: DEFAULT_FLY_CAMERA_CONFIG.maxHeight,
147224
147872
  };
147225
- this._fly = cameraAny?.script?.create?.('flyCamera', {
147226
- attributes: flyAttributes,
147227
- });
147228
- if (this._fly) {
147229
- ;
147230
- this._fly.emitFlyEvent = (type, detail) => {
147231
- this.emit({ type: type, detail });
147232
- };
147233
- // Deactivate by default; orbit is the initial mode
147234
- this._fly.deactivate?.();
147873
+ // Prefer script-based fly when available; fallback to controller otherwise.
147874
+ const canCreateScript = typeof cameraAny?.script?.create === 'function';
147875
+ if (canCreateScript) {
147876
+ const created = cameraAny.script.create('flyCamera', {
147877
+ attributes: flyAttributes,
147878
+ });
147879
+ this._fly = created;
147880
+ if (this._fly) {
147881
+ ;
147882
+ this._fly.emitFlyEvent = (type, detail) => {
147883
+ this.emit({ type: type, detail });
147884
+ };
147885
+ this._fly.deactivate?.();
147886
+ }
147887
+ }
147888
+ else {
147889
+ const controller = new FlyCameraController(ctx.app, cameraAny, (type, detail) => this.emit({ type: type, detail }), flyAttributes);
147890
+ controller.deactivate();
147891
+ this._fly = controller;
147235
147892
  }
147236
147893
  this._cameraMode = 'orbit';
147237
147894
  }
@@ -149082,6 +149739,11 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149082
149739
  // Camera Mode / Fly Camera API
149083
149740
  // ==========================================
149084
149741
  setCameraMode(mode) {
149742
+ // Prevent camera mode changes in preview mode
149743
+ if (this.previewMode) {
149744
+ console.warn('SplatViewerCore.setCameraMode: Camera controls are disabled in preview mode');
149745
+ return;
149746
+ }
149085
149747
  // supersplat-core path: manage mode switching explicitly (camera entity is updated by supersplat-core each frame)
149086
149748
  if (this._supersplat) {
149087
149749
  const prev = this._cameraMode;
@@ -149100,17 +149762,33 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149100
149762
  // Stop supersplat orbit updates + input
149101
149763
  this._supersplat.setCameraControlsEnabled(false);
149102
149764
  this._supersplat.setCameraManualControl(true);
149103
- // Align fly yaw/pitch with current camera rotation to avoid snapping
149104
- try {
149105
- const euler = this.entities.camera?.getEulerAngles?.();
149106
- if (euler && this._fly) {
149107
- ;
149108
- this._fly._pitch = euler.x || 0;
149109
- this._fly._yaw = euler.y || 0;
149765
+ // Preserve camera position and rotation when switching to fly mode
149766
+ if (this._fly) {
149767
+ // For FlyCameraController (fallback path)
149768
+ if (typeof this._fly.syncFromEntity === 'function') {
149769
+ this._fly.syncFromEntity();
149770
+ }
149771
+ else {
149772
+ // For FlyCameraScript (legacy path)
149773
+ try {
149774
+ const pos = this.entities.camera?.getPosition?.();
149775
+ if (pos) {
149776
+ const posVec = pos.clone
149777
+ ? pos.clone()
149778
+ : new Vec3(pos.x || 0, pos.y || 0, pos.z || 0);
149779
+ this.entities.camera?.setPosition?.(posVec);
149780
+ }
149781
+ const euler = this.entities.camera?.getEulerAngles?.();
149782
+ if (euler) {
149783
+ ;
149784
+ this._fly._pitch = euler.x || 0;
149785
+ this._fly._yaw = euler.y || 0;
149786
+ }
149787
+ }
149788
+ catch {
149789
+ // ignore
149790
+ }
149110
149791
  }
149111
- }
149112
- catch {
149113
- // ignore
149114
149792
  }
149115
149793
  this._fly?.activate?.();
149116
149794
  this._cameraMode = 'fly';
@@ -149227,9 +149905,48 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149227
149905
  this._cameraModeManager?.setFlyConfig(config);
149228
149906
  }
149229
149907
  getFlyCameraConfig() {
149908
+ // SuperSplat path: fly is either a script instance or controller fallback
149909
+ if (this._supersplat && this._fly) {
149910
+ try {
149911
+ const flyAny = this._fly;
149912
+ const cfg = {
149913
+ moveSpeed: flyAny.moveSpeed,
149914
+ fastSpeedMultiplier: flyAny.fastSpeedMultiplier,
149915
+ slowSpeedMultiplier: flyAny.slowSpeedMultiplier,
149916
+ lookSensitivity: flyAny.lookSensitivity,
149917
+ invertY: !!flyAny.invertY,
149918
+ keyBindings: { ...(flyAny.keyBindings || {}) },
149919
+ smoothing: flyAny.smoothing,
149920
+ friction: flyAny.friction,
149921
+ enableCollision: !!flyAny.enableCollision,
149922
+ minHeight: flyAny.minHeight ?? null,
149923
+ maxHeight: flyAny.maxHeight ?? null,
149924
+ };
149925
+ return cfg;
149926
+ }
149927
+ catch {
149928
+ return null;
149929
+ }
149930
+ }
149230
149931
  return this._cameraModeManager?.getFlyConfig() || null;
149231
149932
  }
149232
149933
  getFlyCameraState() {
149934
+ // SuperSplat path: fly is either a script instance or controller fallback
149935
+ if (this._supersplat && this._fly?.getState) {
149936
+ try {
149937
+ const state = this._fly.getState();
149938
+ return {
149939
+ mode: this._cameraMode,
149940
+ position: state.position,
149941
+ rotation: state.rotation,
149942
+ velocity: state.velocity,
149943
+ isMoving: state.isMoving,
149944
+ };
149945
+ }
149946
+ catch {
149947
+ return null;
149948
+ }
149949
+ }
149233
149950
  return this._cameraModeManager?.getFlyState() || null;
149234
149951
  }
149235
149952
  _setupScene() {
@@ -149281,8 +149998,8 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149281
149998
  panSensitivity: 1.0,
149282
149999
  zoomSensitivity: 0.1,
149283
150000
  };
149284
- // Add navigation cube configuration if available
149285
- if (this._navigationCubeConfig) {
150001
+ // Add navigation cube configuration if available (but not in preview mode)
150002
+ if (this._navigationCubeConfig && !this.previewMode) {
149286
150003
  orbitAttributes.enableNavigationCube =
149287
150004
  this._navigationCubeConfig.enabled || false;
149288
150005
  this.entities.camera._navigationCubeConfig =
@@ -149297,48 +150014,57 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149297
150014
  detail: { type: interactionType },
149298
150015
  });
149299
150016
  };
150017
+ // Disable camera controls in preview mode
150018
+ if (this.previewMode && this._orbit) {
150019
+ const orbitAny = this._orbit;
150020
+ if (typeof orbitAny.setEnabled === 'function') {
150021
+ orbitAny.setEnabled(false);
150022
+ }
150023
+ }
149300
150024
  this.entities.camera.setPosition(0, 0, 10);
149301
150025
  this.entities.camera.lookAt(Vec3.ZERO);
149302
150026
  // ==============================
149303
- // Setup fly camera (disabled by default)
150027
+ // Setup fly camera (disabled by default, skipped in preview mode)
149304
150028
  // ==============================
149305
- try {
149306
- registerFlyCameraScript();
149307
- // Ensure script component exists (created above)
149308
- const flyAttributes = {
149309
- moveSpeed: DEFAULT_FLY_CAMERA_CONFIG.moveSpeed,
149310
- fastSpeedMultiplier: DEFAULT_FLY_CAMERA_CONFIG.fastSpeedMultiplier,
149311
- slowSpeedMultiplier: DEFAULT_FLY_CAMERA_CONFIG.slowSpeedMultiplier,
149312
- lookSensitivity: DEFAULT_FLY_CAMERA_CONFIG.lookSensitivity,
149313
- invertY: DEFAULT_FLY_CAMERA_CONFIG.invertY,
149314
- keyBindings: DEFAULT_FLY_CAMERA_CONFIG.keyBindings,
149315
- smoothing: DEFAULT_FLY_CAMERA_CONFIG.smoothing,
149316
- friction: DEFAULT_FLY_CAMERA_CONFIG.friction,
149317
- enableCollision: DEFAULT_FLY_CAMERA_CONFIG.enableCollision,
149318
- minHeight: DEFAULT_FLY_CAMERA_CONFIG.minHeight,
149319
- maxHeight: DEFAULT_FLY_CAMERA_CONFIG.maxHeight,
149320
- };
149321
- this._fly = this.entities.camera.script.create('flyCamera', {
149322
- attributes: flyAttributes,
149323
- });
149324
- // Wire event emission to core
149325
- if (this._fly) {
149326
- ;
149327
- this._fly.emitFlyEvent = (type, detail) => {
149328
- this.emit({ type: type, detail });
150029
+ if (!this.previewMode) {
150030
+ try {
150031
+ registerFlyCameraScript();
150032
+ // Ensure script component exists (created above)
150033
+ const flyAttributes = {
150034
+ moveSpeed: DEFAULT_FLY_CAMERA_CONFIG.moveSpeed,
150035
+ fastSpeedMultiplier: DEFAULT_FLY_CAMERA_CONFIG.fastSpeedMultiplier,
150036
+ slowSpeedMultiplier: DEFAULT_FLY_CAMERA_CONFIG.slowSpeedMultiplier,
150037
+ lookSensitivity: DEFAULT_FLY_CAMERA_CONFIG.lookSensitivity,
150038
+ invertY: DEFAULT_FLY_CAMERA_CONFIG.invertY,
150039
+ keyBindings: DEFAULT_FLY_CAMERA_CONFIG.keyBindings,
150040
+ smoothing: DEFAULT_FLY_CAMERA_CONFIG.smoothing,
150041
+ friction: DEFAULT_FLY_CAMERA_CONFIG.friction,
150042
+ enableCollision: DEFAULT_FLY_CAMERA_CONFIG.enableCollision,
150043
+ minHeight: DEFAULT_FLY_CAMERA_CONFIG.minHeight,
150044
+ maxHeight: DEFAULT_FLY_CAMERA_CONFIG.maxHeight,
149329
150045
  };
150046
+ this._fly = this.entities.camera.script.create('flyCamera', {
150047
+ attributes: flyAttributes,
150048
+ });
150049
+ // Wire event emission to core
150050
+ if (this._fly) {
150051
+ ;
150052
+ this._fly.emitFlyEvent = (type, detail) => {
150053
+ this.emit({ type: type, detail });
150054
+ };
150055
+ }
150056
+ // Deactivate fly by default; orbit is the initial mode
150057
+ if (this._fly?.deactivate) {
150058
+ this._fly.deactivate();
150059
+ }
150060
+ // Initialize camera mode manager
150061
+ this._cameraModeManager = new CameraModeManager(this.app, this.entities.camera, this._orbit, this._fly, (eventType, detail) => {
150062
+ this.emit({ type: eventType, detail });
150063
+ }, 'orbit');
149330
150064
  }
149331
- // Deactivate fly by default; orbit is the initial mode
149332
- if (this._fly?.deactivate) {
149333
- this._fly.deactivate();
150065
+ catch (e) {
150066
+ console.warn('Failed to set up fly camera', e);
149334
150067
  }
149335
- // Initialize camera mode manager
149336
- this._cameraModeManager = new CameraModeManager(this.app, this.entities.camera, this._orbit, this._fly, (eventType, detail) => {
149337
- this.emit({ type: eventType, detail });
149338
- }, 'orbit');
149339
- }
149340
- catch (e) {
149341
- console.warn('Failed to set up fly camera', e);
149342
150068
  }
149343
150069
  }
149344
150070
  _setupStats() {
@@ -149583,6 +150309,7 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149583
150309
  'enable-stats',
149584
150310
  'auto-focus',
149585
150311
  'max-splats',
150312
+ 'preview-mode',
149586
150313
  'camera-position',
149587
150314
  'camera-target',
149588
150315
  'orbit-sensitivity',
@@ -149613,6 +150340,9 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149613
150340
  get autoFocus() {
149614
150341
  return this.hasAttribute('auto-focus');
149615
150342
  }
150343
+ get previewMode() {
150344
+ return this.hasAttribute('preview-mode');
150345
+ }
149616
150346
  get maxSplats() {
149617
150347
  return this.getAttribute('max-splats');
149618
150348
  }
@@ -149690,6 +150420,14 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149690
150420
  this.removeAttribute('auto-focus');
149691
150421
  }
149692
150422
  }
150423
+ set previewMode(value) {
150424
+ if (value) {
150425
+ this.setAttribute('preview-mode', '');
150426
+ }
150427
+ else {
150428
+ this.removeAttribute('preview-mode');
150429
+ }
150430
+ }
149693
150431
  set maxSplats(value) {
149694
150432
  if (value === null) {
149695
150433
  this.removeAttribute('max-splats');
@@ -149871,6 +150609,7 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
149871
150609
  return value.length > 0;
149872
150610
  case 'enable-stats':
149873
150611
  case 'auto-focus':
150612
+ case 'preview-mode':
149874
150613
  case 'enable-navigation-cube':
149875
150614
  // Boolean attributes - any value is valid
149876
150615
  return true;
@@ -150817,7 +151556,7 @@ bool initCenter(SplatSource source, vec3 modelCenter, out SplatCenter center) {
150817
151556
  if (!this._core) {
150818
151557
  throw new Error('SplatViewerElement: Core not initialized. Call connectedCallback first.');
150819
151558
  }
150820
- return this._core.selectSplatsInSphere(center, radius, modelId, addToSelection);
151559
+ return this._core.selectSplatsInSphere(new Vec3(center.x, center.y, center.z), radius, modelId, addToSelection);
150821
151560
  }
150822
151561
  clearSplatSelection() {
150823
151562
  if (!this._core) {