storysplat-viewer 2.7.1 → 2.7.2

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 (2) hide show
  1. package/README.md +198 -0
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -668,6 +668,204 @@ import fs from 'fs';
668
668
  fs.writeFileSync('scene.html', html);
669
669
  ```
670
670
 
671
+ ## Portals (Scene-to-Scene Navigation)
672
+
673
+ Portals allow users to navigate between multiple 3D scenes by clicking or walking near portal markers.
674
+
675
+ ### How Portals Work
676
+
677
+ Portals are configured in the scene JSON with a `targetSceneId`:
678
+
679
+ ```json
680
+ {
681
+ "portals": [
682
+ {
683
+ "id": "portal-1",
684
+ "targetSceneId": "abc123xyz",
685
+ "targetSceneName": "Campervan Interior",
686
+ "position": { "x": 2, "y": 0, "z": -3 },
687
+ "type": "sphere",
688
+ "activationMode": "click"
689
+ }
690
+ ]
691
+ }
692
+ ```
693
+
694
+ ### Portal Paths for Self-Hosted Scenes
695
+
696
+ **Important:** By default, portals use StorySplat scene IDs and fetch from `discover.storysplat.com`. For fully self-hosted portals, you need to intercept the `portalActivated` event and implement custom navigation.
697
+
698
+ #### Self-Hosted Portal Example
699
+
700
+ ```javascript
701
+ import { createViewer } from 'storysplat-viewer';
702
+
703
+ // Your self-hosted scene folder structure:
704
+ // /scenes/
705
+ // ├── scene_campervan/
706
+ // │ └── scene.json
707
+ // ├── scene_cabin/
708
+ // │ └── scene.json
709
+ // └── scene_garden/
710
+ // └── scene.json
711
+
712
+ // Load initial scene
713
+ const initialScene = await fetch('/scenes/scene_campervan/scene.json').then(r => r.json());
714
+ let viewer = await createViewer(container, initialScene);
715
+
716
+ // Handle portal navigation with custom paths
717
+ viewer.on('portalActivated', async (data) => {
718
+ console.log('Portal clicked:', data.targetSceneId);
719
+
720
+ // Map scene IDs to your folder paths
721
+ const scenePaths = {
722
+ 'cabin-scene-id': '/scenes/scene_cabin/scene.json',
723
+ 'garden-scene-id': '/scenes/scene_garden/scene.json',
724
+ // Or use the scene ID directly as folder name:
725
+ // [data.targetSceneId]: `/scenes/${data.targetSceneId}/scene.json`
726
+ };
727
+
728
+ const scenePath = scenePaths[data.targetSceneId];
729
+ if (!scenePath) {
730
+ console.error('Unknown scene:', data.targetSceneId);
731
+ return;
732
+ }
733
+
734
+ // Fetch from YOUR server
735
+ const newSceneData = await fetch(scenePath).then(r => r.json());
736
+
737
+ // Dispose old viewer and create new one
738
+ viewer.destroy();
739
+ viewer = await createViewer(container, newSceneData);
740
+
741
+ // Re-attach portal handler for the new scene
742
+ viewer.on('portalActivated', arguments.callee);
743
+ });
744
+ ```
745
+
746
+ #### Using Relative Paths
747
+
748
+ If you want to use the `targetSceneId` directly as a folder name:
749
+
750
+ ```javascript
751
+ viewer.on('portalActivated', async (data) => {
752
+ // targetSceneId becomes the folder name
753
+ const scenePath = `/scenes/${data.targetSceneId}/scene.json`;
754
+
755
+ const newSceneData = await fetch(scenePath).then(r => r.json());
756
+ viewer.destroy();
757
+ viewer = await createViewer(container, newSceneData);
758
+ });
759
+ ```
760
+
761
+ Then structure your folders to match:
762
+ ```
763
+ /scenes/
764
+ ├── campervan/ ← targetSceneId: "campervan"
765
+ │ └── scene.json
766
+ ├── cabin/ ← targetSceneId: "cabin"
767
+ │ └── scene.json
768
+ ```
769
+
770
+ #### Fully Self-Hosted (No StorySplat at All)
771
+
772
+ If both scenes are created locally and **never uploaded to StorySplat**, you have full control over the `targetSceneId` values. They don't need to be real StorySplat IDs—use any string identifier you want:
773
+
774
+ **Step 1: Edit your scene.json files to add portals with custom IDs**
775
+
776
+ ```json
777
+ // scenes/campervan/scene.json
778
+ {
779
+ "name": "Campervan Tour",
780
+ "portals": [
781
+ {
782
+ "id": "portal-1",
783
+ "targetSceneId": "cabin",
784
+ "targetSceneName": "Mountain Cabin",
785
+ "position": { "x": 2, "y": 0, "z": -3 },
786
+ "type": "sphere",
787
+ "activationMode": "click"
788
+ }
789
+ ]
790
+ }
791
+ ```
792
+
793
+ ```json
794
+ // scenes/cabin/scene.json
795
+ {
796
+ "name": "Mountain Cabin",
797
+ "portals": [
798
+ {
799
+ "id": "portal-back",
800
+ "targetSceneId": "campervan",
801
+ "targetSceneName": "Back to Campervan",
802
+ "position": { "x": -1, "y": 0, "z": 2 },
803
+ "type": "sphere",
804
+ "activationMode": "click"
805
+ }
806
+ ]
807
+ }
808
+ ```
809
+
810
+ **Step 2: Complete self-hosted viewer code**
811
+
812
+ ```javascript
813
+ import { createViewer } from 'storysplat-viewer';
814
+
815
+ const container = document.getElementById('viewer');
816
+
817
+ // Map your scene identifiers to file paths
818
+ const SCENES = {
819
+ 'campervan': '/scenes/campervan/scene.json',
820
+ 'cabin': '/scenes/cabin/scene.json',
821
+ 'garden': '/scenes/garden/scene.json'
822
+ };
823
+
824
+ let currentViewer = null;
825
+
826
+ async function loadScene(sceneId) {
827
+ // Cleanup previous viewer
828
+ if (currentViewer) {
829
+ currentViewer.destroy();
830
+ }
831
+
832
+ // Fetch scene from YOUR server
833
+ const sceneData = await fetch(SCENES[sceneId]).then(r => r.json());
834
+
835
+ // Create viewer
836
+ currentViewer = await createViewer(container, sceneData);
837
+
838
+ // Handle portal clicks - loads target scene from your server
839
+ currentViewer.on('portalActivated', (data) => {
840
+ loadScene(data.targetSceneId);
841
+ });
842
+ }
843
+
844
+ // Start with initial scene
845
+ loadScene('campervan');
846
+ ```
847
+
848
+ **Key point:** The `targetSceneId` is just a string you define. It doesn't need to exist on StorySplat—you map it to your own file paths.
849
+
850
+ #### Portal Events
851
+
852
+ | Event | Description | Data |
853
+ |-------|-------------|------|
854
+ | `portalActivated` | Portal clicked or proximity triggered | `{ portalId, targetSceneId, targetSceneName }` |
855
+ | `portalNavigationStart` | Navigation beginning | `{ targetSceneId }` |
856
+ | `portalNavigationComplete` | New scene loaded | `{ sceneId }` |
857
+ | `portalNavigationError` | Navigation failed | `{ error, targetSceneId }` |
858
+
859
+ ### Q: Where does the viewer look for scene files?
860
+
861
+ **A: It depends on how you load scenes:**
862
+
863
+ - **`createViewerFromSceneId()`** - Fetches from `discover.storysplat.com/api/scene/{id}`
864
+ - **`createViewer(sceneData)`** - Uses the data object you pass in directly (no fetch)
865
+ - **Splat file URLs in scene JSON** - Can be absolute URLs or relative to your page
866
+
867
+ The viewer itself has no "base path" concept - splat file paths in your scene JSON should be absolute URLs or relative to your HTML page, not to the viewer JS file.
868
+
671
869
  ## Support
672
870
 
673
871
  - GitHub Issues: [github.com/SonnyC56/storysplat-viewer/issues](https://github.com/SonnyC56/storysplat-viewer/issues)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storysplat-viewer",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "description": "PlayCanvas-based 3D viewer for StorySplat scenes - HTML export & dynamic embedding",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",