feat: switch to cloud-only .titanscene saving and loading

This commit is contained in:
AI Bot
2026-08-07 15:21:50 +05:30
parent 2d31320add
commit bee5ae12bc
+201 -18
View File
@@ -189,7 +189,8 @@ interface SceneObject {
const SceneSetup = () => { const SceneSetup = () => {
const { gl } = useThree(); const { gl } = useThree();
useEffect(() => { useEffect(() => {
gl.shadowMap.type = THREE.PCFSoftShadowMap; gl.shadowMap.enabled = true;
gl.shadowMap.type = THREE.PCFShadowMap;
gl.localClippingEnabled = true; gl.localClippingEnabled = true;
}, [gl]); }, [gl]);
return null; return null;
@@ -314,7 +315,29 @@ export default function Editor() {
try { try {
const data = await api.getProject(projectId); const data = await api.getProject(projectId);
if (data.project && data.project.scene_data) { if (data.project && data.project.scene_data) {
const sd = data.project.scene_data; let sd = data.project.scene_data;
if (sd.titansceneUrl) {
try {
const res = await fetch(sd.titansceneUrl);
const buffer = await res.arrayBuffer();
const unzipped = fflate.unzipSync(new Uint8Array(buffer));
if (unzipped['scene.json']) {
const textDecoder = new TextDecoder();
const jsonString = textDecoder.decode(unzipped['scene.json']);
const parsedSd = JSON.parse(jsonString);
if (parsedSd.modelData && parsedSd.modelData.data === 'model.glb' && unzipped['model.glb']) {
const modelBlob = new Blob([unzipped['model.glb']]);
parsedSd.modelData.data = URL.createObjectURL(modelBlob);
}
sd = parsedSd;
}
} catch (err) {
console.error("Failed to load .titanscene from cloud", err);
}
}
if (sd.sceneObjects) setSceneObjects(sd.sceneObjects); if (sd.sceneObjects) setSceneObjects(sd.sceneObjects);
if (sd.lightProps) setLightProps(sd.lightProps); if (sd.lightProps) setLightProps(sd.lightProps);
if (sd.groundProps) setGroundProps(sd.groundProps); if (sd.groundProps) setGroundProps(sd.groundProps);
@@ -348,8 +371,9 @@ export default function Editor() {
reImportModel(sd.modelData); reImportModel(sd.modelData);
} }
} }
} catch (err) { } catch (error) {
console.error("Failed to fetch project", err); console.error("Failed to fetch project", error);
navigate('/dashboard', { replace: true });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -481,10 +505,10 @@ export default function Editor() {
const [vizAiBaseImage, setVizAiBaseImage] = useState<string | null>(null); const [vizAiBaseImage, setVizAiBaseImage] = useState<string | null>(null);
const [renderRequest, setRenderRequest] = useState<RenderSettings | null>(null); const [renderRequest, setRenderRequest] = useState<RenderSettings | null>(null);
const [exportRequest, setExportRequest] = useState(false); const [exportRequest, setExportRequest] = useState(false);
const [exportFormat, setExportFormat] = useState<'glb' | 'usdz'>('glb'); const [exportFormat, setExportFormat] = useState<'glb' | 'usdz' | 'titanscene'>('glb');
// Export / Save dialog // Export / Save dialog
const [exportDialog, setExportDialog] = useState<{ const [exportDialog, setExportDialog] = useState<{
mode: 'scene' | 'glb' | 'usdz'; mode: 'scene' | 'glb' | 'usdz' | 'titanscene';
defaultName: string; defaultName: string;
} | null>(null); } | null>(null);
const [exportDialogName, setExportDialogName] = useState(''); const [exportDialogName, setExportDialogName] = useState('');
@@ -657,33 +681,123 @@ export default function Editor() {
}; };
const saveScene = async (name?: string) => { const saveScene = async (name?: string) => {
if (!user) return;
const sceneData = { const sceneData = {
sceneObjects, lightProps, groundProps, cameraProps, sceneObjects, lightProps, groundProps, cameraProps,
envPreset, envRotation, backgroundColor, backgroundType, envPreset, envRotation, backgroundColor, backgroundType,
customHdri, backplateImage, additionalLights, modelBounds, modelData, cameraPresets customHdri, backplateImage, additionalLights, modelBounds, cameraPresets,
modelData: modelData ? { ...modelData } : null
}; };
const projectName = name || exportDialogName || 'Untitled Scene'; const projectName = name || exportDialogName || 'Untitled Scene';
try { try {
setIsRendering(true); // Re-use spinner state for saving setIsRendering(true);
const zipObj: Record<string, Uint8Array> = {};
if (sceneData.modelData && sceneData.modelData.data && typeof sceneData.modelData.data === 'string') {
if (sceneData.modelData.data.startsWith('data:')) {
const dataUrl = sceneData.modelData.data;
const base64 = dataUrl.split(',')[1];
const binaryStr = atob(base64);
const len = binaryStr.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
zipObj['model.glb'] = bytes;
sceneData.modelData.data = 'model.glb';
} else if (sceneData.modelData.data.startsWith('blob:') || sceneData.modelData.data.startsWith('http')) {
const res = await fetch(sceneData.modelData.data);
const buf = await res.arrayBuffer();
zipObj['model.glb'] = new Uint8Array(buf);
sceneData.modelData.data = 'model.glb';
}
}
const jsonString = JSON.stringify(sceneData);
const textEncoder = new TextEncoder();
zipObj['scene.json'] = textEncoder.encode(jsonString);
const zipped = fflate.zipSync(zipObj);
const blob = new Blob([zipped], { type: 'application/octet-stream' });
if (!user) {
setIsRendering(false);
setExportDialog(null);
return;
}
// Upload to Cloud
const file = new File([blob], `${projectName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.titanscene`, { type: 'application/octet-stream' });
const fileUrl = await api.uploadAsset(file);
const result = await api.saveProject({ const result = await api.saveProject({
id: projectId && projectId !== 'new' ? projectId : undefined, id: projectId && projectId !== 'new' ? projectId : undefined,
name: projectName, name: projectName,
scene_data: sceneData scene_data: { titansceneUrl: fileUrl }
}); });
if (projectId === 'new' && result.id) { if (projectId === 'new' && result.id) {
navigate(`/editor/${result.id}`, { replace: true }); navigate(`/editor/${result.id}`, { replace: true });
} }
} catch (err: any) { } catch (error) {
console.error("Failed to save project to cloud", err); console.error("Failed to save project to cloud", error);
alert("Failed to save project: " + (err.message || JSON.stringify(err))); } finally {
setIsRendering(false);
}
setExportDialog(null);
};
const exportTitanScene = async (name: string) => {
const sceneData = {
sceneObjects, lightProps, groundProps, cameraProps,
envPreset, envRotation, backgroundColor, backgroundType,
customHdri, backplateImage, additionalLights, modelBounds, cameraPresets,
modelData: modelData ? { ...modelData } : null
};
const projectName = name || exportDialogName || 'Untitled Scene';
try {
setIsRendering(true);
const zipObj: Record<string, Uint8Array> = {};
if (sceneData.modelData && sceneData.modelData.data && typeof sceneData.modelData.data === 'string') {
if (sceneData.modelData.data.startsWith('data:')) {
const dataUrl = sceneData.modelData.data;
const base64 = dataUrl.split(',')[1];
const binaryStr = atob(base64);
const len = binaryStr.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
zipObj['model.glb'] = bytes;
sceneData.modelData.data = 'model.glb';
} else if (sceneData.modelData.data.startsWith('blob:') || sceneData.modelData.data.startsWith('http')) {
const res = await fetch(sceneData.modelData.data);
const buf = await res.arrayBuffer();
zipObj['model.glb'] = new Uint8Array(buf);
sceneData.modelData.data = 'model.glb';
}
}
const jsonString = JSON.stringify(sceneData);
const textEncoder = new TextEncoder();
zipObj['scene.json'] = textEncoder.encode(jsonString);
const zipped = fflate.zipSync(zipObj);
const blob = new Blob([zipped], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${projectName.replace(/[^a-z0-9]/gi, '_').toLowerCase()}.titanscene`;
a.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error("Failed to export .titanscene", error);
} finally { } finally {
setIsRendering(false); setIsRendering(false);
setOpenMenu(null);
setExportDialog(null);
} }
}; };
@@ -706,6 +820,8 @@ export default function Editor() {
const name = exportDialogName.trim() || exportDialog.defaultName; const name = exportDialogName.trim() || exportDialog.defaultName;
if (exportDialog.mode === 'scene') { if (exportDialog.mode === 'scene') {
saveScene(name); saveScene(name);
} else if (exportDialog.mode === 'titanscene' as any) {
exportTitanScene(name);
} else { } else {
setExportFormat(exportDialog.mode); setExportFormat(exportDialog.mode);
// Store name so ExportManager can use it // Store name so ExportManager can use it
@@ -717,9 +833,66 @@ export default function Editor() {
const [exportFileName, setExportFileName] = useState<string>('model'); const [exportFileName, setExportFileName] = useState<string>('model');
const loadScene = (event: React.ChangeEvent<HTMLInputElement>) => { const loadScene = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (!file) return; if (!file) return;
if (file.name.endsWith('.titanscene')) {
try {
const buffer = await file.arrayBuffer();
const unzipped = fflate.unzipSync(new Uint8Array(buffer));
if (unzipped['scene.json']) {
const textDecoder = new TextDecoder();
const jsonString = textDecoder.decode(unzipped['scene.json']);
const data = JSON.parse(jsonString);
if (data.modelData && data.modelData.data === 'model.glb' && unzipped['model.glb']) {
const modelBlob = new Blob([unzipped['model.glb']]);
data.modelData.data = URL.createObjectURL(modelBlob);
}
if (data.sceneObjects) setSceneObjects(data.sceneObjects);
if (data.lightProps) setLightProps(data.lightProps);
if (data.groundProps) setGroundProps(data.groundProps);
if (data.cameraProps) {
const cProps = data.cameraProps;
setCameraProps(cProps);
const updateControls = (attempts = 0) => {
if (orbitControlsRef.current) {
const controls = orbitControlsRef.current;
if (cProps.target) controls.target.set(cProps.target[0], cProps.target[1], cProps.target[2]);
if (cProps.position) {
controls.object.position.set(cProps.position[0], cProps.position[1], cProps.position[2]);
}
controls.update();
} else if (attempts < 10) {
setTimeout(() => updateControls(attempts + 1), 100);
}
};
updateControls();
}
if (data.envPreset !== undefined) setEnvPreset(data.envPreset);
if (data.envRotation !== undefined) setEnvRotation(data.envRotation);
if (data.backgroundColor) setBackgroundColor(data.backgroundColor);
if (data.backgroundType) setBackgroundType(data.backgroundType);
if (data.customHdri) setCustomHdri(data.customHdri);
if (data.backplateImage) setBackplateImage(data.backplateImage);
if (data.additionalLights) setAdditionalLights(data.additionalLights);
if (data.modelBounds) setModelBounds(data.modelBounds);
if (data.cameraPresets) setCameraPresets(data.cameraPresets);
if (data.modelData && data.modelData.data) {
reImportModel(data.modelData);
}
}
} catch (err) {
console.error("Failed to parse .titanscene", err);
}
setOpenMenu(null);
event.target.value = '';
return;
}
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
try { try {
@@ -1706,7 +1879,7 @@ export default function Editor() {
<input <input
type="file" type="file"
ref={sceneInputRef} ref={sceneInputRef}
accept=".json" accept=".json,.titanscene"
onChange={loadScene} onChange={loadScene}
className="hidden" className="hidden"
/> />
@@ -1793,6 +1966,16 @@ export default function Editor() {
<span>Export Scene (USDZ)...</span> <span>Export Scene (USDZ)...</span>
</div> </div>
</button> </button>
<div className="h-px bg-white/10 my-1" />
<button
onClick={() => openExportDialog('titanscene' as any)}
className="w-full text-left px-4 py-2 hover:bg-primary hover:text-white flex items-center justify-between transition-colors"
>
<div className="flex items-center gap-2">
<Save size={14} />
<span>Export Local (.titanscene)</span>
</div>
</button>
</div> </div>
)} )}
</div> </div>