From 498da300db977f8be14a0d8cfb433fcdf9b1dea5 Mon Sep 17 00:00:00 2001 From: wass08 Date: Fri, 30 Jan 2026 07:48:52 +0900 Subject: [PATCH] export / load json --- .../components/editor/export-manager.tsx | 54 ++++++++++ apps/editor/components/editor/index.tsx | 2 + .../sidebar/panels/settings-panel/index.tsx | 98 ++++++++++++++++++- packages/core/src/store/use-scene.ts | 13 +++ .../components/renderers/scene-renderer.tsx | 10 +- packages/viewer/src/store/use-viewer.ts | 7 ++ 6 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 apps/editor/components/editor/export-manager.tsx diff --git a/apps/editor/components/editor/export-manager.tsx b/apps/editor/components/editor/export-manager.tsx new file mode 100644 index 00000000..5eeaccea --- /dev/null +++ b/apps/editor/components/editor/export-manager.tsx @@ -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((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 +} diff --git a/apps/editor/components/editor/index.tsx b/apps/editor/components/editor/index.tsx index f28a3a29..633b2b70 100644 --- a/apps/editor/components/editor/index.tsx +++ b/apps/editor/components/editor/index.tsx @@ -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() { + {/* Editor only system to toggle zone visibility */} {/* */} diff --git a/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx index 98a2d052..17e50228 100644 --- a/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx +++ b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx @@ -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(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) => { + 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 (
+ {/* Export Section */} +
+ + +
+ + {/* Save/Load Section */} +
+ + + + + + + +
+ {/* Danger Zone */}