Dual URL Architecture - Implementation Summary
Overview
The application now supports storing both original .ply/.splat URLs (for BabylonJS) AND optimized .sog URLs (for PlayCanvas) in the SaveFile JSON. This ensures backward compatibility while enabling format-specific optimizations for different viewers.
Architecture Changes
1. SaveFile Type Definition
File: src/types/SceneTypes.ts (lines 310-311)
loadedModelUrl: string | null; // Primary model URL (used by BabylonJS, original .ply/.splat format)
sogModelUrl?: string | null; // Optimized .sog format URL (used by PlayCanvas viewer)
Key Points:
- loadedModelUrl remains required and always contains original format
- sogModelUrl is optional and only populated when conversion succeeds
- Old scenes without sogModelUrl continue working perfectly
Storage Behavior
2. Upload Flow (New Scenes)
File: src/utils/StorageManager.ts - uploadSplat() method
When conversion is enabled:
1. 0-15%: Convert original file to .sog format
2. 15-60%: Upload original .ply/.splat file to Firebase → loadedModelUrl
3. 60-100%: Upload converted .sog file to Firebase → sogModelUrl
Result: Two files stored in Firebase Storage:
- users/{userId}/splats/original_file.ply
- users/{userId}/splats/original_file.sog
When conversion is disabled:
- Only uploads original file → loadedModelUrl
- sogModelUrl remains undefined
3. Update Flow (Existing Scenes)
File: src/utils/StorageManager.ts - updateSplat() method
Same dual-upload logic as new scenes:
- Replaces BOTH files if conversion is enabled
- Updates both loadedModelUrl and sogModelUrl in SaveFile
- Falls back to single file upload if conversion fails
Viewer Selection Logic
4. Next.js Viewer - Preference Reading
File: discover/components/SplatViewer.tsx (lines 547-572)
New feature: Reads preferredViewer from SaveFile JSON
// Fetch scene JSON to read preferredViewer setting
useEffect(() => {
const loadScenePreferences = async () => {
const response = await fetch(currentSplat.jsonUrl)
const sceneJson = await response.json()
if (sceneJson.preferredViewer) {
const shouldUsePlayCanvas = sceneJson.preferredViewer === 'playcanvas'
setUsePlayCanvas(shouldUsePlayCanvas)
}
}
loadScenePreferences()
}, [currentSplat?.jsonUrl])
Behavior:
- If preferredViewer === 'playcanvas' → Uses PlayCanvas viewer
- If preferredViewer === 'babylonjs' → Uses BabylonJS viewer
- If not set → Defaults to PlayCanvas (current behavior)
5. PlayCanvas Viewer - URL Selection
File: discover/components/StorySplatPlayCanvasViewer.tsx (lines 54-81)
Smart URL selection:
// Prefer sogModelUrl for PlayCanvas, fallback to loadedModelUrl
const modelUrl = sceneJson.sogModelUrl || sceneJson.loadedModelUrl
const usingOptimizedFormat = !!sceneJson.sogModelUrl
if (usingOptimizedFormat) {
console.log('✅ Using optimized .sog format from sogModelUrl')
} else {
console.log('ℹ️ Using original format from loadedModelUrl')
}
// Update sceneJson.loadedModelUrl to the selected URL
sceneJson.loadedModelUrl = modelUrl
Behavior:
- First choice: Use sogModelUrl if available (optimized .sog)
- Fallback: Use loadedModelUrl (original format)
- Logs: Clearly indicates which format is being used
6. BabylonJS Viewer - Original Format Only
File: discover/components/StorySplatNpmViewer.tsx
No changes needed - Always uses loadedModelUrl
Conversion Triggers
The system converts to .sog format in two scenarios:
Trigger 1: Auto-convert setting enabled
Trigger 2: PlayCanvas viewer selected (regardless of toggle)
Logic:
const shouldConvert = (jsonSave.autoConvertToSog && needsSogConversion(file)) ||
(jsonSave.preferredViewer === 'playcanvas' && needsSogConversion(file));
Backward Compatibility
Old Scenes (No sogModelUrl)
- ✅ BabylonJS: Works perfectly using
loadedModelUrl - ✅ PlayCanvas: Falls back to
loadedModelUrlgracefully - ✅ No breaking changes
New Scenes (With sogModelUrl)
- ✅ BabylonJS: Uses
loadedModelUrl(original format) - ✅ PlayCanvas: Uses
sogModelUrl(optimized format) - ✅ Best performance for both viewers
File Structure Example
SaveFile JSON with dual URLs:
{
"sceneTitle": "My Scene",
"loadedModelUrl": "https://firebase.../original_file.ply",
"sogModelUrl": "https://firebase.../original_file.sog",
"preferredViewer": "playcanvas",
"autoConvertToSog": true,
"waypoints": [...],
"hotspots": [...]
}
SaveFile JSON without sogModelUrl (old scenes):
{
"sceneTitle": "My Old Scene",
"loadedModelUrl": "https://firebase.../old_file.ply",
"waypoints": [...],
"hotspots": [...]
}
Progress Bar Breakdown
With Conversion (Dual Upload):
- 0-15%: Converting to .sog format
- 15-60%: Uploading original file
- 60-100%: Uploading .sog file
Without Conversion (Single Upload):
- 0-100%: Uploading original file
Console Logs to Watch
Storage Manager Logs:
[StorageManager] Auto-converting file to .sog format before upload...
[StorageManager] SOG conversion successful. New file: yourfile.sog
[StorageManager] Will upload BOTH original (yourfile.ply) and converted (yourfile.sog) files
[StorageManager] Uploading original file: yourfile.ply
[StorageManager] Original file uploaded successfully: https://...
[StorageManager] Uploading .sog file: yourfile.sog
[StorageManager] .sog file uploaded successfully: https://...
Next.js Viewer Logs:
[SplatViewer] Fetching scene JSON to read viewer preference
[SplatViewer] Scene has preferred viewer: playcanvas
[SplatViewer] Setting usePlayCanvas to: true
🎮🎮🎮 USING PLAYCANVAS VIEWER 🎮🎮🎮
PlayCanvas Viewer Logs:
OR
Testing Checklist
Test 1: New scene with conversion enabled
- [ ] Upload .ply file with auto-convert ON
- [ ] Verify TWO files uploaded to Firebase Storage (.ply + .sog)
- [ ] Check SaveFile JSON has both
loadedModelUrlandsogModelUrl - [ ] PlayCanvas viewer uses .sog file
- [ ] Progress bar shows: 0-15% convert, 15-60% upload .ply, 60-100% upload .sog
Test 2: New scene with conversion disabled
- [ ] Upload .ply file with auto-convert OFF and viewer = BabylonJS
- [ ] Verify ONE file uploaded to Firebase Storage (.ply only)
- [ ] Check SaveFile JSON has only
loadedModelUrl, nosogModelUrl - [ ] BabylonJS viewer uses .ply file
Test 3: PlayCanvas forces conversion
- [ ] Set auto-convert toggle OFF
- [ ] Set preferred viewer to PlayCanvas
- [ ] Upload .ply file
- [ ] Verify conversion STILL happens (PlayCanvas requirement)
- [ ] Both files uploaded
Test 4: Old scene backward compatibility
- [ ] Load old scene without
sogModelUrl - [ ] Verify BabylonJS viewer works (uses
loadedModelUrl) - [ ] Verify PlayCanvas viewer works (falls back to
loadedModelUrl)
Test 5: Viewer preference respected
- [ ] Create scene with
preferredViewer: 'playcanvas' - [ ] Open in Next.js viewer
- [ ] Verify PlayCanvas viewer loads automatically
- [ ] Create scene with
preferredViewer: 'babylonjs' - [ ] Verify BabylonJS viewer loads automatically
Benefits
For BabylonJS:
- ✅ Always gets original .ply/.splat format (required for compatibility)
- ✅ No unnecessary conversions
- ✅ Existing workflows unchanged
For PlayCanvas:
- ✅ Gets optimized .sog format when available
- ✅ Faster loading and rendering
- ✅ Graceful fallback to original format
- ✅ Best performance for .sog-compatible scenes
For Users:
- ✅ No breaking changes
- ✅ Automatic optimization when beneficial
- ✅ Clear visibility via logs
- ✅ Per-scene viewer preferences respected
Files Modified
- ✅
src/types/SceneTypes.ts- AddedsogModelUrlfield - ✅
src/utils/StorageManager.ts- Dual upload logic inuploadSplat()andupdateSplat() - ✅
discover/components/SplatViewer.tsx- ReadpreferredViewerfrom SaveFile - ✅
discover/components/StorySplatPlayCanvasViewer.tsx- PrefersogModelUrloverloadedModelUrl
API Endpoints Used
Conversion API: https://webar-colocation-sonny.duckdns.org/api/splat
POST /upload- Upload file for conversionGET /status/:jobId- Check conversion statusGET /download/:jobId- Download converted file
Firebase Storage: users/{userId}/splats/
- Stores both original and converted files
- Separate URLs for each format
Future Enhancements
- Automatic cleanup: Delete old .sog files when scenes are re-uploaded
- Storage optimization: Optionally store only .sog for PlayCanvas-only scenes
- Format detection: Auto-select viewer based on available file formats
- Batch conversion: Convert all old scenes in bulk via admin panel