feat: switch to cloud-only .titanscene saving and loading
This commit is contained in:
+200
-17
@@ -189,7 +189,8 @@ interface SceneObject {
|
||||
const SceneSetup = () => {
|
||||
const { gl } = useThree();
|
||||
useEffect(() => {
|
||||
gl.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
gl.shadowMap.enabled = true;
|
||||
gl.shadowMap.type = THREE.PCFShadowMap;
|
||||
gl.localClippingEnabled = true;
|
||||
}, [gl]);
|
||||
return null;
|
||||
@@ -314,7 +315,29 @@ export default function Editor() {
|
||||
try {
|
||||
const data = await api.getProject(projectId);
|
||||
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.lightProps) setLightProps(sd.lightProps);
|
||||
if (sd.groundProps) setGroundProps(sd.groundProps);
|
||||
@@ -348,8 +371,9 @@ export default function Editor() {
|
||||
reImportModel(sd.modelData);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch project", err);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch project", error);
|
||||
navigate('/dashboard', { replace: true });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -481,10 +505,10 @@ export default function Editor() {
|
||||
const [vizAiBaseImage, setVizAiBaseImage] = useState<string | null>(null);
|
||||
const [renderRequest, setRenderRequest] = useState<RenderSettings | null>(null);
|
||||
const [exportRequest, setExportRequest] = useState(false);
|
||||
const [exportFormat, setExportFormat] = useState<'glb' | 'usdz'>('glb');
|
||||
const [exportFormat, setExportFormat] = useState<'glb' | 'usdz' | 'titanscene'>('glb');
|
||||
// Export / Save dialog
|
||||
const [exportDialog, setExportDialog] = useState<{
|
||||
mode: 'scene' | 'glb' | 'usdz';
|
||||
mode: 'scene' | 'glb' | 'usdz' | 'titanscene';
|
||||
defaultName: string;
|
||||
} | null>(null);
|
||||
const [exportDialogName, setExportDialogName] = useState('');
|
||||
@@ -657,33 +681,123 @@ export default function Editor() {
|
||||
};
|
||||
|
||||
const saveScene = async (name?: string) => {
|
||||
if (!user) return;
|
||||
|
||||
const sceneData = {
|
||||
sceneObjects, lightProps, groundProps, cameraProps,
|
||||
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';
|
||||
|
||||
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({
|
||||
id: projectId && projectId !== 'new' ? projectId : undefined,
|
||||
name: projectName,
|
||||
scene_data: sceneData
|
||||
scene_data: { titansceneUrl: fileUrl }
|
||||
});
|
||||
if (projectId === 'new' && result.id) {
|
||||
navigate(`/editor/${result.id}`, { replace: true });
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Failed to save project to cloud", err);
|
||||
alert("Failed to save project: " + (err.message || JSON.stringify(err)));
|
||||
} catch (error) {
|
||||
console.error("Failed to save project to cloud", error);
|
||||
} finally {
|
||||
setIsRendering(false);
|
||||
setOpenMenu(null);
|
||||
}
|
||||
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 {
|
||||
setIsRendering(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -706,6 +820,8 @@ export default function Editor() {
|
||||
const name = exportDialogName.trim() || exportDialog.defaultName;
|
||||
if (exportDialog.mode === 'scene') {
|
||||
saveScene(name);
|
||||
} else if (exportDialog.mode === 'titanscene' as any) {
|
||||
exportTitanScene(name);
|
||||
} else {
|
||||
setExportFormat(exportDialog.mode);
|
||||
// Store name so ExportManager can use it
|
||||
@@ -717,9 +833,66 @@ export default function Editor() {
|
||||
|
||||
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];
|
||||
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();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
@@ -1706,7 +1879,7 @@ export default function Editor() {
|
||||
<input
|
||||
type="file"
|
||||
ref={sceneInputRef}
|
||||
accept=".json"
|
||||
accept=".json,.titanscene"
|
||||
onChange={loadScene}
|
||||
className="hidden"
|
||||
/>
|
||||
@@ -1793,6 +1966,16 @@ export default function Editor() {
|
||||
<span>Export Scene (USDZ)...</span>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user