Skip to content

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

jsonSave.autoConvertToSog === true

Trigger 2: PlayCanvas viewer selected (regardless of toggle)

jsonSave.preferredViewer === 'playcanvas'

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 loadedModelUrl gracefully
  • 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:

🎮 ✅ Using optimized .sog format from sogModelUrl: https://.../file.sog

OR

🎮 ℹ️ Using original format from loadedModelUrl: https://.../file.ply

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 loadedModelUrl and sogModelUrl
  • [ ] 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, no sogModelUrl
  • [ ] 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

  1. src/types/SceneTypes.ts - Added sogModelUrl field
  2. src/utils/StorageManager.ts - Dual upload logic in uploadSplat() and updateSplat()
  3. discover/components/SplatViewer.tsx - Read preferredViewer from SaveFile
  4. discover/components/StorySplatPlayCanvasViewer.tsx - Prefer sogModelUrl over loadedModelUrl

API Endpoints Used

Conversion API: https://webar-colocation-sonny.duckdns.org/api/splat

  • POST /upload - Upload file for conversion
  • GET /status/:jobId - Check conversion status
  • GET /download/:jobId - Download converted file

Firebase Storage: users/{userId}/splats/

  • Stores both original and converted files
  • Separate URLs for each format

Future Enhancements

  1. Automatic cleanup: Delete old .sog files when scenes are re-uploaded
  2. Storage optimization: Optionally store only .sog for PlayCanvas-only scenes
  3. Format detection: Auto-select viewer based on available file formats
  4. Batch conversion: Convert all old scenes in bulk via admin panel