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

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 (25) hide show
  1. package/README.md +52 -336
  2. package/dist/web-component/splat-viewer.esm.js +88 -19
  3. package/dist/web-component/splat-viewer.esm.min.js +1 -1
  4. package/dist/web-component/splat-viewer.js +88 -19
  5. package/dist/web-component/splat-viewer.min.js +1 -1
  6. package/dist/web-component/supersplat-core/controllers.d.ts +5 -0
  7. package/dist/web-component/supersplat-core/controllers.d.ts.map +1 -1
  8. package/dist/web-component/supersplat-core/splat.d.ts.map +1 -1
  9. package/dist/web-component/types/supersplat-core/controllers.d.ts +5 -0
  10. package/dist/web-component/types/supersplat-core/controllers.d.ts.map +1 -1
  11. package/dist/web-component/types/supersplat-core/splat.d.ts.map +1 -1
  12. package/dist/web-component/types/web-component/OrbitCameraScript.d.ts.map +1 -1
  13. package/dist/web-component/types/web-component/SplatViewerCore.d.ts.map +1 -1
  14. package/dist/web-component/types/web-component/SupersplatAdapter.d.ts +9 -0
  15. package/dist/web-component/types/web-component/SupersplatAdapter.d.ts.map +1 -1
  16. package/dist/web-component/types/web-component/utils/config.d.ts +1 -0
  17. package/dist/web-component/types/web-component/utils/config.d.ts.map +1 -1
  18. package/dist/web-component/web-component/OrbitCameraScript.d.ts.map +1 -1
  19. package/dist/web-component/web-component/SplatViewerCore.d.ts.map +1 -1
  20. package/dist/web-component/web-component/SupersplatAdapter.d.ts +9 -0
  21. package/dist/web-component/web-component/SupersplatAdapter.d.ts.map +1 -1
  22. package/dist/web-component/web-component/utils/config.d.ts +1 -0
  23. package/dist/web-component/web-component/utils/config.d.ts.map +1 -1
  24. package/package.json +5 -12
  25. package/CHANGELOG.md +0 -78
package/README.md CHANGED
@@ -1,382 +1,98 @@
1
- # Heliguy Web Viewer
1
+ # @heliguy-xyz/splat-viewer
2
2
 
3
- A self-contained 3D Gaussian Splat viewer web component built with PlayCanvas for high-performance
4
- rendering of point cloud data. Framework-agnostic and embeddable in any web application with zero
5
- external dependencies.
3
+ A standalone 3D Gaussian Splat viewer web component with multi-model support, transform system, fly camera, and scene configuration. Framework-agnostic and works with vanilla JavaScript, React, Vue, Angular, and any other framework.
6
4
 
7
- ## Features
8
-
9
- - 🎯 **Gaussian Splat Rendering**: Native support for `.splat` files using PlayCanvas
10
- - 📊 **PLY Support**: Traditional point cloud rendering for `.ply` and `.ply.compressed` formats
11
- - 📦 **SOG Support**: Native support for `.sog` (Spatially Ordered Gaussians) files
12
- - 🎮 **Interactive Controls**: Smooth camera navigation and manipulation
13
- - 🧭 **Navigation Cube**: Interactive 3D cube for quick camera orientation control
14
- - 📈 **Performance Monitoring**: Real-time rendering statistics
15
- - ⚡ **High Performance**: WebGL2-powered rendering with PlayCanvas engine
16
- - 📱 **Responsive Design**: Full-screen immersive viewing experience
17
- - 🔧 **Self-Contained**: Single file with bundled PlayCanvas engine (2MB)
18
- - 🎨 **Built-in Loading**: Professional loading spinner with orange/black theme
19
- - 🚀 **Zero Dependencies**: No external scripts required
20
-
21
- ### Enhanced Features (v1.0.0)
22
-
23
- - 🎭 **Multi-Model Support**: Load and manage multiple models in the same scene
24
- - 🔄 **Transform System**: Programmatically position, rotate, and scale models
25
- - 🎥 **Fly Camera Mode**: First-person WASD + mouse-look navigation with configurable controls
26
- - 🎨 **Scene Configuration**: Adjust FOV and background color with smooth animations
27
- - 👁️ **Preview Mode**: Disable camera controls for product previews with programmatic rotation
28
- - 📡 **Comprehensive Events**: React to model lifecycle, transforms, camera changes, and more
29
- - 📘 **TypeScript Support**: Full type definitions for all APIs
30
-
31
- ## Quick Start
32
-
33
- ### Installation
34
-
35
- #### Via npm (Recommended)
5
+ ## Installation
36
6
 
37
7
  ```bash
38
- npm install @heliguy/web-viewer
8
+ npm install @heliguy-xyz/splat-viewer
39
9
  ```
40
10
 
41
- #### Via CDN
42
-
43
- ```html
44
- <!-- UMD Bundle -->
45
- <script src="https://unpkg.com/@heliguy/web-viewer@1.0.0/dist/web-component/splat-viewer.min.js"></script>
46
-
47
- <!-- ESM Bundle -->
48
- <script type="module">
49
- import '@heliguy/web-viewer/esm'
50
- </script>
51
- ```
52
-
53
- ### Prerequisites
54
-
55
- - Modern browser with WebGL2 support
56
- - Node.js ≥18.0.0 (for development only)
57
-
58
- ### Usage
11
+ ## Quick Start
59
12
 
60
- #### Vanilla HTML
13
+ ### Vanilla JavaScript / HTML
61
14
 
62
15
  ```html
63
16
  <!DOCTYPE html>
64
17
  <html>
65
- <head>
66
- <title>3D Splat Viewer</title>
67
- </head>
68
- <body>
69
- <splat-viewer
70
- id="viewer"
71
- width="100%"
72
- height="600px"
73
- auto-focus
74
- enable-navigation-cube
75
- ></splat-viewer>
76
-
77
- <!-- Import from npm package -->
78
- <script src="node_modules/@heliguy/web-viewer/dist/web-component/splat-viewer.min.js"></script>
79
-
80
- <script>
81
- const viewer = document.getElementById('viewer')
82
- viewer.addEventListener('ready', () => {
83
- viewer.load('path/to/model.splat')
84
- })
85
- </script>
86
- </body>
18
+ <head>
19
+ <script type="module" src="node_modules/@heliguy-xyz/splat-viewer/dist/splat-viewer.esm.js"></script>
20
+ </head>
21
+ <body>
22
+ <splat-viewer
23
+ src="path/to/model.splat"
24
+ width="800"
25
+ height="600"
26
+ auto-focus
27
+ ></splat-viewer>
28
+ </body>
87
29
  </html>
88
30
  ```
89
31
 
90
- #### Embed via iframe
91
-
92
- Use the provided `viewer.html` page that reads query parameters and configures the web component.
93
- Host `viewer.html` alongside `dist/web-component/splat-viewer.min.js`.
94
-
95
- Supported query params:
96
-
97
- - `src` (required): URL to your model (encode it!)
98
- - `width` (optional): CSS width (e.g. `100%`, `800px`)
99
- - `height` (optional): CSS height (e.g. `100vh`, `600px`)
100
- - `autoFocus` (optional): `true` | `false` (default: `true`)
101
- - `stats` (optional): `true` | `false`
102
-
103
- Example:
104
-
105
- ```html
106
- <iframe
107
- src="https://your-domain.com/viewer.html?src=https%3A%2F%2Fcdn.yourdomain.com%2Fmodels%2FRyhope.ply&width=100%25&height=100vh&autoFocus=true"
108
- width="100%"
109
- height="600"
110
- style="border:0; background:#000"
111
- allowfullscreen
112
- ></iframe>
113
- ```
114
-
115
- Notes:
116
-
117
- - Ensure CORS allows the viewer origin to fetch the model (`Access-Control-Allow-Origin`).
118
- - Prefer hosting `viewer.html`, the bundle, and models on the same domain to avoid CORS.
119
- - URL-encode the `src` value.
120
-
121
- #### React / Next.js
32
+ ### React
122
33
 
123
34
  ```tsx
124
- import { useEffect, useRef } from 'react'
125
- import '@heliguy/web-viewer'
35
+ import '@heliguy-xyz/splat-viewer';
36
+ import { useRef, useEffect } from 'react';
126
37
 
127
38
  function App() {
128
- const viewerRef = useRef<any>(null)
39
+ const viewerRef = useRef(null);
129
40
 
130
41
  useEffect(() => {
131
- const viewer = viewerRef.current
132
- if (!viewer) return
42
+ const viewer = viewerRef.current;
43
+ if (!viewer) return;
133
44
 
134
- const handleReady = () => {
135
- viewer.load('/models/scene.splat')
136
- }
137
-
138
- viewer.addEventListener('ready', handleReady)
139
- return () => viewer.removeEventListener('ready', handleReady)
140
- }, [])
45
+ viewer.addEventListener('loaded', (e) => {
46
+ console.log('Model loaded:', e.detail);
47
+ });
48
+ }, []);
141
49
 
142
50
  return (
143
51
  <splat-viewer
144
52
  ref={viewerRef}
53
+ src="/model.splat"
145
54
  width="100%"
146
55
  height="600px"
147
56
  auto-focus
148
57
  />
149
- )
150
- }
151
- ```
152
-
153
- **TypeScript:** Add to your `global.d.ts`:
154
- ```typescript
155
- declare namespace JSX {
156
- interface IntrinsicElements {
157
- 'splat-viewer': React.DetailedHTMLProps<
158
- React.HTMLAttributes<HTMLElement> & {
159
- ref?: React.Ref<any>
160
- src?: string
161
- width?: string
162
- height?: string
163
- 'auto-focus'?: boolean
164
- 'enable-stats'?: boolean
165
- 'preview-mode'?: boolean
166
- },
167
- HTMLElement
168
- >
169
- }
58
+ );
170
59
  }
171
60
  ```
172
61
 
173
- #### Vue 3
174
-
175
- ```vue
176
- <template>
177
- <splat-viewer
178
- ref="viewerRef"
179
- width="100%"
180
- height="600px"
181
- auto-focus
182
- />
183
- </template>
184
-
185
- <script setup>
186
- import { ref, onMounted } from 'vue'
187
- import '@heliguy/web-viewer'
188
-
189
- const viewerRef = ref(null)
190
-
191
- onMounted(() => {
192
- const viewer = viewerRef.value
193
- const handleReady = () => {
194
- viewer.load('/models/scene.splat')
195
- }
196
- viewer.addEventListener('ready', handleReady)
197
- })
198
- </script>
199
- ```
200
-
201
- ## Commands
202
-
203
- | Command | Description |
204
- | ----------------------------- | ---------------------------------- |
205
- | `npm run web-component:serve` | Start development server |
206
- | `npm run web-component:build` | Build self-contained web component |
207
- | `npm run web-component:clean` | Clean build artifacts |
208
- | `npm run lint` | Check code quality with ESLint |
209
- | `npm run lint:fix` | Fix linting issues automatically |
210
- | `npm run format` | Format code with Prettier |
211
- | `npm run format:check` | Check code formatting |
212
-
213
- ## Web Component API
214
-
215
- ### Attributes
216
-
217
- #### Basic Configuration
218
-
219
- - `src` - Model file path or URL
220
- - `width` - Canvas width (default: "100%")
221
- - `height` - Canvas height (default: "400px")
222
- - `auto-focus` - Auto-focus on load (default: true)
223
- - `enable-stats` - Show performance stats (default: false)
224
- - `max-splats` - Maximum splat count (default: 2000000)
225
- - `preview-mode` - Disable camera controls for preview use (default: false)
226
-
227
- #### Camera Controls
228
-
229
- - `orbit-sensitivity` - Mouse orbit sensitivity (default: 0.3)
230
- - `pan-sensitivity` - Mouse pan sensitivity (default: 0.5)
231
- - `zoom-sensitivity` - Mouse zoom sensitivity (default: 0.1)
232
- - `min-distance` - Minimum camera distance (default: 1)
233
- - `max-distance` - Maximum camera distance (default: 100)
234
-
235
- #### Navigation Cube
236
-
237
- - `enable-navigation-cube` - Show interactive navigation cube (default: false)
238
- - `navigation-cube-size` - Cube size in pixels (default: 120)
239
- - `navigation-cube-transition` - Camera transition duration in ms (default: 800)
240
-
241
- ### Preview Mode
242
-
243
- Preview mode disables all camera controls (mouse/touch interactions) while maintaining programmatic rotation capabilities. This is ideal for:
244
-
245
- - Product configurators with UI-controlled rotation
246
- - Model previews with custom control interfaces
247
- - Embedded viewers where you want to control all interactions
248
-
249
- **Usage:**
250
-
251
- ```html
252
- <splat-viewer src="model.splat" preview-mode></splat-viewer>
253
- ```
254
-
255
- **Features in Preview Mode:**
256
- - ✅ Camera controls are completely disabled
257
- - ✅ Model is automatically centered and framed on load
258
- - ✅ Programmatic rotation via `setModelRotation()` API still works
259
- - ✅ All other APIs remain functional (visibility, transforms, etc.)
260
-
261
- **Example with Rotation Controls:**
262
-
263
- ```javascript
264
- const viewer = document.getElementById('viewer');
265
-
266
- viewer.addEventListener('loaded', () => {
267
- const models = viewer.getModels();
268
- const modelId = models[0].id;
269
-
270
- // Programmatically set rotation
271
- viewer.setModelRotation(modelId, { x: 0, y: 45, z: 0 });
272
- });
273
- ```
274
-
275
- See `examples/preview-mode.html` for a complete working example.
276
-
277
- ### Events
278
-
279
- #### Core Events
280
- - `ready` - Component initialized
281
- - `loading-start` - Model loading started
282
- - `loading-progress` - Model loading progress update
283
- - `loaded` - Model loaded successfully
284
- - `error` - Loading error occurred
285
- - `stats-update` - Performance statistics update
286
- - `camera-change` - Camera position/target changed
287
- - `interaction-start` / `interaction-end` - User interaction with orbit controls
288
-
289
- #### Multi-Model Events (v1.0.0)
290
- - `model-added` - Model added to scene
291
- - `model-removed` - Model removed from scene
292
- - `scene-cleared` - All models cleared
293
-
294
- #### Transform Events (v1.0.0)
295
- - `model-transform-changed` - Model transform updated
296
-
297
- #### Camera Events (v1.0.0)
298
- - `camera-mode-changed` - Camera mode switched (orbit/fly)
299
- - `fly-camera-move` - Fly camera position changed
300
- - `fly-camera-look` - Fly camera rotation changed
301
-
302
- #### Scene Config Events (v1.0.0)
303
- - `camera-fov-changed` - Camera FOV changed
304
- - `background-color-changed` - Background color changed
305
-
306
- ## Project Structure
307
-
308
- ```
309
- src/
310
- ├── web-component/ # Main web component implementation
311
- │ ├── SplatViewerElement.ts # Web component class
312
- │ ├── SplatViewerCore.ts # Core rendering engine
313
- │ ├── NavigationCubeController.ts # Navigation cube controller
314
- │ ├── OrbitCameraScript.ts # Camera controls
315
- │ ├── navigation-cube/ # Navigation cube modules
316
- │ │ ├── CubeRenderer.ts # 3D cube visual rendering
317
- │ │ ├── ControlPointManager.ts # Control point UI elements
318
- │ │ └── CameraTransitionController.ts # Camera animations
319
- │ ├── types/ # TypeScript definitions
320
- │ ├── loader/ # File loading system (.ply, .sog, .splat)
321
- │ └── utils/ # Utility functions
322
- ├── build/ # Build configuration
323
- ├── dist/web-component/ # Built self-contained web component
324
- │ └── splat-viewer.min.js # Single 2MB file with PlayCanvas
325
- ├── test-web-component.html # Basic test page
326
- └── test-navigation-cube.html # Navigation cube test page
327
-
328
- assets/ # 3D model assets for testing
329
- docs/ # Documentation
330
- ```
331
-
332
- ## Supported File Formats
333
-
334
- - **.sog** - Spatially Ordered Gaussians (PlayCanvas native support)
335
- - **.splat** - Gaussian Splat files (PlayCanvas native support)
336
- - **.ply** - Point cloud files (standard format)
337
- - **.ply.compressed** - Compressed PLY files (gzip/deflate)
338
-
339
- ## Self-Contained Bundle
340
-
341
- The web component is distributed as a single 2MB file (`splat-viewer.min.js`) that includes:
62
+ ## Features
342
63
 
343
- - Complete PlayCanvas engine (v2.11.8)
344
- - Web component implementation
345
- - File format loaders
346
- - Performance monitoring
347
- - Interactive controls
64
+ - 🎨 **Multi-Model Support** - Load and manage multiple models in one scene
65
+ - 🎥 **Dual Camera Modes** - Orbit and fly camera controls
66
+ - 🔧 **Transform System** - Position, rotate, and scale models programmatically
67
+ - 📦 **Multiple Formats** - Support for PLY, SPLAT, and SOG files
68
+ - **High Performance** - Optimized rendering with PlayCanvas
69
+ - 🎯 **Selection Tools** - Box and sphere selection for splats
70
+ - 🎨 **Scene Configuration** - Background, grid, FOV, and more
71
+ - 📱 **Responsive** - Works on desktop, tablet, and mobile
348
72
 
349
- **Benefits:**
73
+ ## API
350
74
 
351
- - Zero external dependencies
352
- - Version consistency guaranteed
353
- - Easy embedding in any web application
354
- - Offline functionality
75
+ See the full [API documentation](https://github.com/Heliguy-com/3d-web-viewer/tree/main/packages/core/docs) for detailed information.
355
76
 
356
77
  ## Development
357
78
 
79
+ This package is part of the `@heliguy-xyz/splat-viewer` monorepo.
80
+
358
81
  ```bash
359
- # Start development server
360
- npm run web-component:serve
82
+ # Build
83
+ npm run build
361
84
 
362
- # Build for production
363
- npm run web-component:build
85
+ # Development server
86
+ npm run dev
364
87
 
365
- # Clean build artifacts
366
- npm run web-component:clean
88
+ # Type check
89
+ npm run type-check
367
90
  ```
368
91
 
369
- ## Deployment
370
-
371
- ### Netlify
372
-
373
- - Publish directory: `dist`
374
- - Build command: `npm run build`
375
- - Node version: 18 (configured in `netlify.toml`)
92
+ ## License
376
93
 
377
- The build emits `dist/index.html` from `viewer.html` using a small zero-dependency Node script
378
- (`build/copy-viewer-to-index.mjs`). Rollup uses an ESM config at `build/rollup.config.mjs`.
94
+ MIT © Heliguy
379
95
 
380
- ## License
96
+ ## Related Packages
381
97
 
382
- MIT License - see LICENSE file for details.
98
+ - [@heliguy-xyz/splat-viewer-react-ui](https://www.npmjs.com/package/@heliguy-xyz/splat-viewer-react-ui) - Full-featured React UI component
@@ -139557,11 +139557,15 @@ function parseJsonSafely(jsonString) {
139557
139557
  }
139558
139558
  /**
139559
139559
  * Convert a string to a boolean value
139560
+ * For HTML boolean attributes, an empty string means the attribute is present (true)
139560
139561
  */
139561
139562
  function parseBoolean(value) {
139562
139563
  if (typeof value === 'boolean')
139563
139564
  return value;
139564
139565
  if (typeof value === 'string') {
139566
+ // Empty string means attribute is present without value (should be true for boolean attributes)
139567
+ if (value === '')
139568
+ return true;
139565
139569
  return value.toLowerCase() === 'true' || value === '1';
139566
139570
  }
139567
139571
  return false;
@@ -140452,6 +140456,10 @@ function registerOrbitCameraScript() {
140452
140456
  this.isPanning = false;
140453
140457
  // Allow host to temporarily disable camera input (e.g., while dragging gizmos)
140454
140458
  this._inputEnabled = true;
140459
+ // Granular control over specific input types
140460
+ this._orbitEnabled = true;
140461
+ this._panEnabled = true;
140462
+ this._zoomEnabled = true;
140455
140463
  // Setup context menu prevention
140456
140464
  this.setupContextMenuPrevention();
140457
140465
  // Use PlayCanvas mouse events for reliable input handling
@@ -140489,6 +140497,19 @@ function registerOrbitCameraScript() {
140489
140497
  this.isPanning = false;
140490
140498
  }
140491
140499
  };
140500
+ OrbitCamera.prototype.setInputControls = function (options) {
140501
+ if (options.orbit !== undefined)
140502
+ this._orbitEnabled = !!options.orbit;
140503
+ if (options.pan !== undefined)
140504
+ this._panEnabled = !!options.pan;
140505
+ if (options.zoom !== undefined)
140506
+ this._zoomEnabled = !!options.zoom;
140507
+ // Clear any in-progress interactions if being disabled
140508
+ if (this._orbitEnabled === false)
140509
+ this.isOrbiting = false;
140510
+ if (this._panEnabled === false)
140511
+ this.isPanning = false;
140512
+ };
140492
140513
  OrbitCamera.prototype.update = function (dt) {
140493
140514
  // Update navigation cube if enabled
140494
140515
  if (this.navigationCube &&
@@ -140505,12 +140526,13 @@ function registerOrbitCameraScript() {
140505
140526
  typeof this.navigationCube.cancelTransition === 'function') {
140506
140527
  this.navigationCube.cancelTransition('manual-interaction');
140507
140528
  }
140508
- if (event.button === MOUSEBUTTON_LEFT) {
140529
+ if (event.button === MOUSEBUTTON_LEFT && this._orbitEnabled !== false) {
140509
140530
  this.isOrbiting = true;
140510
140531
  this.emitInteractionEvent?.('interaction-start', 'rotate');
140511
140532
  }
140512
- else if (event.button === MOUSEBUTTON_RIGHT ||
140513
- event.button === MOUSEBUTTON_MIDDLE) {
140533
+ else if ((event.button === MOUSEBUTTON_RIGHT ||
140534
+ event.button === MOUSEBUTTON_MIDDLE) &&
140535
+ this._panEnabled !== false) {
140514
140536
  this.isPanning = true;
140515
140537
  this.emitInteractionEvent?.('interaction-start', 'pan');
140516
140538
  }
@@ -140558,7 +140580,7 @@ function registerOrbitCameraScript() {
140558
140580
  }
140559
140581
  };
140560
140582
  OrbitCamera.prototype.onMouseWheel = function (event) {
140561
- if (this._inputEnabled === false)
140583
+ if (this._inputEnabled === false || this._zoomEnabled === false)
140562
140584
  return;
140563
140585
  // Cancel any ongoing navigation cube transition when manual zoom interaction starts
140564
140586
  if (this.navigationCube &&
@@ -142694,6 +142716,10 @@ class Splat extends Element$1 {
142694
142716
  this.entity.setEulerAngles(orientation);
142695
142717
  this.entity.addComponent('gsplat', { asset });
142696
142718
  const instance = this.entity.gsplat.instance;
142719
+ // Check if the gsplat component initialized properly
142720
+ if (!instance || !instance.meshInstance) {
142721
+ throw new Error('Failed to initialize gsplat component. The file may not contain valid Gaussian Splatting data.');
142722
+ }
142697
142723
  // use custom render order distance calculation for splats
142698
142724
  instance.meshInstance.calculateSortDistance = (meshInstance, pos, dir) => {
142699
142725
  const bound = this.localBound;
@@ -143802,6 +143828,10 @@ class PointerController {
143802
143828
  };
143803
143829
  // Allow temporarily disabling camera controls (e.g. while dragging gizmos).
143804
143830
  let enabled = true;
143831
+ // Granular control over specific input types
143832
+ let orbitEnabled = true;
143833
+ let panEnabled = true;
143834
+ let zoomEnabled = true;
143805
143835
  const resetState = () => {
143806
143836
  pressedButton = -1;
143807
143837
  touches = [];
@@ -143908,13 +143938,13 @@ class PointerController {
143908
143938
  (event.shiftKey || event.ctrlKey ? 'orbit' :
143909
143939
  (event.altKey || event.metaKey ? 'zoom' : null)) :
143910
143940
  null;
143911
- if (mod === 'orbit' || (mod === null && pressedButton === 0)) {
143941
+ if ((mod === 'orbit' || (mod === null && pressedButton === 0)) && orbitEnabled) {
143912
143942
  orbit(dx, dy);
143913
143943
  }
143914
- else if (mod === 'zoom' || (mod === null && pressedButton === 1)) {
143944
+ else if ((mod === 'zoom' || (mod === null && pressedButton === 1)) && zoomEnabled) {
143915
143945
  zoom(dy * -0.02);
143916
143946
  }
143917
- else if (mod === 'pan' || (mod === null && pressedButton === 2)) {
143947
+ else if ((mod === 'pan' || (mod === null && pressedButton === 2)) && panEnabled) {
143918
143948
  pan(x, y, dx, dy);
143919
143949
  }
143920
143950
  }
@@ -143925,7 +143955,9 @@ class PointerController {
143925
143955
  const dy = event.offsetY - touch.y;
143926
143956
  touch.x = event.offsetX;
143927
143957
  touch.y = event.offsetY;
143928
- orbit(dx, dy);
143958
+ if (orbitEnabled) {
143959
+ orbit(dx, dy);
143960
+ }
143929
143961
  }
143930
143962
  else if (touches.length === 2) {
143931
143963
  const touch = touches[touches.map(t => t.id).indexOf(event.pointerId)];
@@ -143934,8 +143966,12 @@ class PointerController {
143934
143966
  const mx = (touches[0].x + touches[1].x) * 0.5;
143935
143967
  const my = (touches[0].y + touches[1].y) * 0.5;
143936
143968
  const ml = dist(touches[0].x, touches[0].y, touches[1].x, touches[1].y);
143937
- pan(mx, my, (mx - midx), (my - midy));
143938
- zoom((ml - midlen) * 0.01);
143969
+ if (panEnabled) {
143970
+ pan(mx, my, (mx - midx), (my - midy));
143971
+ }
143972
+ if (zoomEnabled) {
143973
+ zoom((ml - midlen) * 0.01);
143974
+ }
143939
143975
  midx = mx;
143940
143976
  midy = my;
143941
143977
  midlen = ml;
@@ -143953,16 +143989,20 @@ class PointerController {
143953
143989
  return;
143954
143990
  const { deltaX, deltaY } = event;
143955
143991
  if (isMouseEvent(deltaX, deltaY)) {
143956
- zoom(deltaY * -2e-3);
143992
+ if (zoomEnabled)
143993
+ zoom(deltaY * -2e-3);
143957
143994
  }
143958
143995
  else if (event.ctrlKey || event.metaKey) {
143959
- zoom(deltaY * -0.02);
143996
+ if (zoomEnabled)
143997
+ zoom(deltaY * -0.02);
143960
143998
  }
143961
143999
  else if (event.shiftKey) {
143962
- pan(event.offsetX, event.offsetY, deltaX, deltaY);
144000
+ if (panEnabled)
144001
+ pan(event.offsetX, event.offsetY, deltaX, deltaY);
143963
144002
  }
143964
144003
  else {
143965
- orbit(deltaX, deltaY);
144004
+ if (orbitEnabled)
144005
+ orbit(deltaX, deltaY);
143966
144006
  }
143967
144007
  event.preventDefault();
143968
144008
  };
@@ -144030,6 +144070,14 @@ class PointerController {
144030
144070
  resetState();
144031
144071
  }
144032
144072
  };
144073
+ this.setInputControls = (options) => {
144074
+ if (options.orbit !== undefined)
144075
+ orbitEnabled = !!options.orbit;
144076
+ if (options.pan !== undefined)
144077
+ panEnabled = !!options.pan;
144078
+ if (options.zoom !== undefined)
144079
+ zoomEnabled = !!options.zoom;
144080
+ };
144033
144081
  }
144034
144082
  }
144035
144083
 
@@ -147334,6 +147382,26 @@ class SupersplatAdapter {
147334
147382
  console.warn('SupersplatAdapter: Failed to toggle camera controls', e);
147335
147383
  }
147336
147384
  }
147385
+ /**
147386
+ * Configure granular control over specific camera input types.
147387
+ * Allows enabling/disabling orbit, pan, and zoom independently.
147388
+ */
147389
+ setCameraInputControls(options) {
147390
+ const controller = this.scene?.camera?.controller;
147391
+ if (!controller)
147392
+ return;
147393
+ try {
147394
+ if (typeof controller.setInputControls === 'function') {
147395
+ controller.setInputControls(options);
147396
+ }
147397
+ else {
147398
+ console.warn('SupersplatAdapter: Camera controller does not support setInputControls');
147399
+ }
147400
+ }
147401
+ catch (e) {
147402
+ console.warn('SupersplatAdapter: Failed to set camera input controls', e);
147403
+ }
147404
+ }
147337
147405
  /**
147338
147406
  * Enable/disable supersplat-core camera auto-update (orbit tweens).
147339
147407
  * When enabled, the camera entity transform may be driven by an external controller (fly mode).
@@ -147792,9 +147860,9 @@ class SplatViewerCore {
147792
147860
  this._supersplatReady = this._supersplat.init();
147793
147861
  this._supersplatReady
147794
147862
  ?.then(() => {
147795
- // Disable camera controls in preview mode
147863
+ // In preview mode, enable zoom only (disable orbit and pan)
147796
147864
  if (this.previewMode && this._supersplat) {
147797
- this._supersplat.setCameraControlsEnabled?.(false);
147865
+ this._supersplat.setCameraInputControls?.({ orbit: false, pan: false, zoom: true });
147798
147866
  }
147799
147867
  // Only set up fly camera if not in preview mode
147800
147868
  if (!this.previewMode) {
@@ -150007,11 +150075,12 @@ class SplatViewerCore {
150007
150075
  detail: { type: interactionType },
150008
150076
  });
150009
150077
  };
150010
- // Disable camera controls in preview mode
150078
+ // In preview mode, enable zoom only (disable orbit and pan)
150011
150079
  if (this.previewMode && this._orbit) {
150012
150080
  const orbitAny = this._orbit;
150013
- if (typeof orbitAny.setEnabled === 'function') {
150014
- orbitAny.setEnabled(false);
150081
+ if (typeof orbitAny.setInputControls === 'function') {
150082
+ // Enable zoom, disable orbit and pan
150083
+ orbitAny.setInputControls({ orbit: false, pan: false, zoom: true });
150015
150084
  }
150016
150085
  }
150017
150086
  this.entities.camera.setPosition(0, 0, 10);