export / load json
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useThree } from '@react-three/fiber'
|
||||
import { useEffect } from 'react'
|
||||
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
|
||||
|
||||
export function ExportManager() {
|
||||
const scene = useThree((state) => state.scene)
|
||||
const setExportScene = useViewer((state) => state.setExportScene)
|
||||
|
||||
useEffect(() => {
|
||||
const exportFn = async () => {
|
||||
// Find the scene renderer group by name
|
||||
const sceneGroup = scene.getObjectByName('scene-renderer')
|
||||
if (!sceneGroup) {
|
||||
console.error('scene-renderer group not found')
|
||||
return
|
||||
}
|
||||
|
||||
const exporter = new GLTFExporter()
|
||||
const date = new Date().toISOString().split('T')[0]
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
exporter.parse(
|
||||
sceneGroup,
|
||||
(gltf) => {
|
||||
const blob = new Blob([gltf as ArrayBuffer], { type: 'model/gltf-binary' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `model_${date}.glb`
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
resolve()
|
||||
},
|
||||
(error) => {
|
||||
console.error('Export error:', error)
|
||||
reject(error)
|
||||
},
|
||||
{ binary: true }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
setExportScene(exportFn)
|
||||
|
||||
return () => {
|
||||
setExportScene(null)
|
||||
}
|
||||
}, [scene, setExportScene])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { RoofPanel } from '../ui/panels/roof-panel'
|
||||
import { SidebarProvider } from '../ui/primitives/sidebar'
|
||||
import { AppSidebar } from '../ui/sidebar/app-sidebar'
|
||||
import { CustomCameraControls } from './custom-camera-controls'
|
||||
import { ExportManager } from './export-manager'
|
||||
import { SelectionManager } from './selection-manager'
|
||||
|
||||
useScene.getState().loadScene()
|
||||
@@ -38,6 +39,7 @@ export default function Editor() {
|
||||
</SidebarProvider>
|
||||
<Viewer>
|
||||
<SelectionManager />
|
||||
<ExportManager />
|
||||
{/* Editor only system to toggle zone visibility */}
|
||||
<ZoneSystem />
|
||||
{/* <Stats /> */}
|
||||
|
||||
@@ -1,14 +1,62 @@
|
||||
import { useScene } from "@pascal-app/core";
|
||||
import { useViewer } from "@pascal-app/viewer";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { Download, Save, Trash2, Upload } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
import { Button } from "@/components/ui/primitives/button";
|
||||
import useEditor from "@/store/use-editor";
|
||||
|
||||
export function SettingsPanel() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const nodes = useScene((state) => state.nodes);
|
||||
const rootNodeIds = useScene((state) => state.rootNodeIds);
|
||||
const setScene = useScene((state) => state.setScene);
|
||||
const clearScene = useScene((state) => state.clearScene);
|
||||
const resetSelection = useViewer((state) => state.resetSelection);
|
||||
const exportScene = useViewer((state) => state.exportScene);
|
||||
const setPhase = useEditor((state) => state.setPhase);
|
||||
|
||||
const handleExport = async () => {
|
||||
if (exportScene) {
|
||||
await exportScene();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveBuild = () => {
|
||||
const sceneData = { nodes, rootNodeIds };
|
||||
const json = JSON.stringify(sceneData, null, 2);
|
||||
const blob = new Blob([json], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
const date = new Date().toISOString().split("T")[0];
|
||||
link.download = `layout_${date}.json`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleFileLoad = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.target?.result as string);
|
||||
if (data.nodes && data.rootNodeIds) {
|
||||
setScene(data.nodes, data.rootNodeIds);
|
||||
resetSelection();
|
||||
setPhase("site");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load build:", err);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
|
||||
// Reset input so the same file can be loaded again
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
clearScene();
|
||||
resetSelection();
|
||||
@@ -17,6 +65,54 @@ export function SettingsPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-3">
|
||||
{/* Export Section */}
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-muted-foreground text-xs uppercase">
|
||||
Export
|
||||
</label>
|
||||
<Button
|
||||
className="w-full justify-start gap-2"
|
||||
onClick={handleExport}
|
||||
variant="outline"
|
||||
>
|
||||
<Download className="size-4" />
|
||||
Export 3D Model
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Save/Load Section */}
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-muted-foreground text-xs uppercase">
|
||||
Save & Load
|
||||
</label>
|
||||
|
||||
<Button
|
||||
className="w-full justify-start gap-2"
|
||||
onClick={handleSaveBuild}
|
||||
variant="outline"
|
||||
>
|
||||
<Save className="size-4" />
|
||||
Save Build
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full justify-start gap-2"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
variant="outline"
|
||||
>
|
||||
<Upload className="size-4" />
|
||||
Load Build
|
||||
</Button>
|
||||
|
||||
<input
|
||||
accept="application/json"
|
||||
className="hidden"
|
||||
onChange={handleFileLoad}
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Danger Zone */}
|
||||
<div className="space-y-2">
|
||||
<label className="font-medium text-destructive text-xs uppercase">
|
||||
|
||||
@@ -22,6 +22,7 @@ export type SceneState = {
|
||||
// Actions
|
||||
loadScene: () => void
|
||||
clearScene: () => void
|
||||
setScene: (nodes: Record<AnyNodeId, AnyNode>, rootNodeIds: AnyNodeId[]) => void
|
||||
|
||||
markDirty: (id: AnyNodeId) => void
|
||||
clearDirty: (id: AnyNodeId) => void
|
||||
@@ -60,6 +61,18 @@ const useScene = create<SceneState>()(
|
||||
get().loadScene() // Default scene
|
||||
},
|
||||
|
||||
setScene: (nodes, rootNodeIds) => {
|
||||
set({
|
||||
nodes,
|
||||
rootNodeIds,
|
||||
dirtyNodes: new Set<AnyNodeId>(),
|
||||
})
|
||||
// Mark all nodes as dirty to trigger re-validation
|
||||
Object.values(nodes).forEach((node) => {
|
||||
get().markDirty(node.id)
|
||||
})
|
||||
},
|
||||
|
||||
loadScene: () => {
|
||||
if (get().rootNodeIds.length > 0) {
|
||||
// Assign all nodes as dirty to force re-validation
|
||||
|
||||
@@ -6,7 +6,11 @@ import { NodeRenderer } from "./node-renderer";
|
||||
export const SceneRenderer = () => {
|
||||
const rootNodes = useScene((state) => state.rootNodeIds);
|
||||
|
||||
return rootNodes.map((nodeId) => (
|
||||
return (
|
||||
<group name="scene-renderer">
|
||||
{rootNodes.map((nodeId) => (
|
||||
<NodeRenderer key={nodeId} nodeId={nodeId} />
|
||||
));
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,6 +45,10 @@ type ViewerState = {
|
||||
resetSelection: () => void;
|
||||
|
||||
outliner: Outliner; // No setter as we will manipulate directly the arrays
|
||||
|
||||
// Export functionality
|
||||
exportScene: (() => Promise<void>) | null;
|
||||
setExportScene: (fn: (() => Promise<void>) | null) => void;
|
||||
};
|
||||
|
||||
const useViewer = create<ViewerState>()((set, get) => ({
|
||||
@@ -94,6 +98,9 @@ const useViewer = create<ViewerState>()((set, get) => ({
|
||||
}),
|
||||
|
||||
outliner: { selectedObjects: [], hoveredObjects: [] },
|
||||
|
||||
exportScene: null,
|
||||
setExportScene: (fn) => set({ exportScene: fn }),
|
||||
}));
|
||||
|
||||
export default useViewer;
|
||||
|
||||
Reference in New Issue
Block a user