From cc9d9916b795ba708eea1706c97a34bd4b81f28c Mon Sep 17 00:00:00 2001 From: wass08 Date: Fri, 23 Jan 2026 09:56:06 +0900 Subject: [PATCH] data persist --- .../components/ui/sidebar/app-sidebar.tsx | 10 +- .../sidebar/panels/settings-panel/index.tsx | 34 ++ packages/core/src/store/use-scene.ts | 302 ++++++++---------- .../renderers/item/item-renderer.tsx | 1 + 4 files changed, 176 insertions(+), 171 deletions(-) create mode 100644 apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx diff --git a/apps/editor/components/ui/sidebar/app-sidebar.tsx b/apps/editor/components/ui/sidebar/app-sidebar.tsx index 1159f6bb..40b93b6e 100644 --- a/apps/editor/components/ui/sidebar/app-sidebar.tsx +++ b/apps/editor/components/ui/sidebar/app-sidebar.tsx @@ -27,6 +27,7 @@ import { import { cn } from "@/lib/utils"; import useEditor from "@/store/use-editor"; import { useViewer } from "@pascal-app/viewer"; +import { SettingsPanel } from "./panels/settings-panel"; import { SitePanel } from "./panels/site-panel"; import { ZonePanel } from "./panels/zone-panel"; @@ -83,13 +84,8 @@ export function AppSidebar() { return ; // case "collections": // return ; - // case "settings": - // return ( - // setIsHelpOpen(true)} - // onOpenJsonInspector={() => setIsJsonInspectorOpen(true)} - // /> - // ); + case "settings": + return ; default: return null; } diff --git a/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx new file mode 100644 index 00000000..6eb9a064 --- /dev/null +++ b/apps/editor/components/ui/sidebar/panels/settings-panel/index.tsx @@ -0,0 +1,34 @@ +import { useScene } from "@pascal-app/core"; +import { useViewer } from "@pascal-app/viewer"; +import { Trash2 } from "lucide-react"; +import { Button } from "@/components/ui/primitives/button"; + +export function SettingsPanel() { + const clearScene = useScene((state) => state.clearScene); + const resetSelection = useViewer((state) => state.resetSelection); + + const handleResetToDefault = () => { + clearScene(); + resetSelection(); + }; + + return ( +
+ {/* Danger Zone */} +
+ + + +
+
+ ); +} diff --git a/packages/core/src/store/use-scene.ts b/packages/core/src/store/use-scene.ts index 8b762806..53afbd88 100644 --- a/packages/core/src/store/use-scene.ts +++ b/packages/core/src/store/use-scene.ts @@ -1,213 +1,187 @@ -"use client"; +'use client' -import { temporal } from "zundo"; -import { create } from "zustand"; -import { BuildingNode, ItemNode, type Zone } from "../schema"; -import { LevelNode } from "../schema/nodes/level"; -import { WallNode } from "../schema/nodes/wall"; -import type { AnyNode, AnyNodeId } from "../schema/types"; -import * as nodeActions from "./actions/node-actions"; -import * as zoneActions from "./actions/zone-actions"; +import { temporal } from 'zundo' +import { create } from 'zustand' +import { persist } from 'zustand/middleware' +import { BuildingNode, type Zone } from '../schema' +import { LevelNode } from '../schema/nodes/level' +import type { AnyNode, AnyNodeId } from '../schema/types' +import * as nodeActions from './actions/node-actions' +import * as zoneActions from './actions/zone-actions' export type SceneState = { // 1. The Data: A flat dictionary of all nodes - nodes: Record; - zones: Record; + nodes: Record + zones: Record // 2. The Root: Which nodes are at the top level? - rootNodeIds: AnyNodeId[]; - zoneIds: Zone["id"][]; + rootNodeIds: AnyNodeId[] + zoneIds: Zone['id'][] // 3. The "Dirty" Set: For the Wall/Physics systems - dirtyNodes: Set; + dirtyNodes: Set // Actions - loadScene: () => void; - markDirty: (id: AnyNodeId) => void; - clearDirty: (id: AnyNodeId) => void; + loadScene: () => void + clearScene: () => void - createNode: (node: AnyNode, parentId?: AnyNodeId) => void; - createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void; + markDirty: (id: AnyNodeId) => void + clearDirty: (id: AnyNodeId) => void - updateNode: (id: AnyNodeId, data: Partial) => void; - updateNodes: (updates: { id: AnyNodeId; data: Partial }[]) => void; + createNode: (node: AnyNode, parentId?: AnyNodeId) => void + createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void - deleteNode: (id: AnyNodeId) => void; - deleteNodes: (ids: AnyNodeId[]) => void; + updateNode: (id: AnyNodeId, data: Partial) => void + updateNodes: (updates: { id: AnyNodeId; data: Partial }[]) => void + + deleteNode: (id: AnyNodeId) => void + deleteNodes: (ids: AnyNodeId[]) => void // Zone actions - createZone: (zone: Zone) => void; - createZones: (zones: Zone[]) => void; - updateZone: (id: Zone["id"], data: Partial) => void; - updateZones: (updates: { id: Zone["id"]; data: Partial }[]) => void; - deleteZone: (id: Zone["id"]) => void; - deleteZones: (ids: Zone["id"][]) => void; -}; + createZone: (zone: Zone) => void + createZones: (zones: Zone[]) => void + updateZone: (id: Zone['id'], data: Partial) => void + updateZones: (updates: { id: Zone['id']; data: Partial }[]) => void + deleteZone: (id: Zone['id']) => void + deleteZones: (ids: Zone['id'][]) => void +} // type PartializedStoreState = Pick; const useScene = create()( - temporal( - (set, get) => ({ - // 1. Flat dictionary of all nodes - nodes: {}, - zones: {}, + persist( + temporal( + (set, get) => ({ + // 1. Flat dictionary of all nodes + nodes: {}, + zones: {}, - // 2. Root node IDs - rootNodeIds: [], - zoneIds: [], + // 2. Root node IDs + rootNodeIds: [], + zoneIds: [], - // 3. Dirty set - dirtyNodes: new Set(), + // 3. Dirty set + dirtyNodes: new Set(), - loadScene: () => { - const building = BuildingNode.parse({ - children: [], - }); + clearScene: () => { + set({ + nodes: {}, + rootNodeIds: [], + zones: {}, + zoneIds: [], + dirtyNodes: new Set(), + }) + get().loadScene() // Default scene + }, - const level0 = LevelNode.parse({ - level: 0, - children: [], - }); - const level1 = LevelNode.parse({ - level: 1, - children: [], - }); - const level2 = LevelNode.parse({ - level: 2, - children: [], - }); + loadScene: () => { + if (get().rootNodeIds.length > 0) { + // Assign all nodes as dirty to force re-validation + Object.values(get().nodes).forEach((node) => { + get().markDirty(node.id) + }) + return // Scene already loaded + } - const wall0 = WallNode.parse({ - start: [0, 0], - end: [5, 0], - children: [], - parentId: level0.id, - }); + const building = BuildingNode.parse({ + children: [], + }) - const wall1 = WallNode.parse({ - start: [0, 0], - end: [0, 5], - children: [], - parentId: level0.id, - }); + const level0 = LevelNode.parse({ + level: 0, + children: [], + }) - const wall2 = WallNode.parse({ - start: [5, 5], - end: [0, 5], - children: [], - parentId: level0.id, - }); + building.children.push(level0.id) - const wall3 = WallNode.parse({ - start: [5, 5], - end: [5, 0], - children: [], - parentId: level1.id, - }); + // Define all nodes flat + const nodes: Record = { + [building.id]: building, + [level0.id]: level0, + } - const window1 = ItemNode.parse({ - type: "item", - name: "Window", - position: [2.5, 0.5, 0], - parentId: wall3.id, - asset: { - id: "window-round", - name: "Round Window", - thumbnail: "/items/window-small/thumbnail.png", - category: "windows", - attachTo: "wall", - src: "/items/window-small/model.glb", - }, - }); + // Root nodes are the levels + const rootNodeIds = [building.id] - wall3.children.push(window1.id); + set({ nodes, rootNodeIds }) + }, - level0.children.push(wall0.id, wall1.id, wall2.id); - level1.children.push(wall3.id); + markDirty: (id) => { + get().dirtyNodes.add(id) + }, - building.children.push(level0.id, level1.id, level2.id); + clearDirty: (id) => { + get().dirtyNodes.delete(id) + }, - // Define all nodes flat - const nodes: Record = { - [building.id]: building, - [level0.id]: level0, - [level1.id]: level1, - [level2.id]: level2, - [wall0.id]: wall0, - [wall1.id]: wall1, - [wall2.id]: wall2, - [wall3.id]: wall3, - [window1.id]: window1, - }; + createNodes: (ops) => nodeActions.createNodesAction(set, get, ops), + createNode: (node, parentId) => + nodeActions.createNodesAction(set, get, [{ node, parentId }]), - // Root nodes are the levels - const rootNodeIds = [building.id]; + updateNodes: (updates) => nodeActions.updateNodesAction(set, get, updates), + updateNode: (id, data) => nodeActions.updateNodesAction(set, get, [{ id, data }]), - get().dirtyNodes.add(wall0.id); - get().dirtyNodes.add(wall1.id); - get().dirtyNodes.add(wall2.id); - get().dirtyNodes.add(wall3.id); - set({ nodes, rootNodeIds }); + // --- DELETE --- + + deleteNodes: (ids) => nodeActions.deleteNodesAction(set, get, ids), + + deleteNode: (id) => nodeActions.deleteNodesAction(set, get, [id]), + + // --- ZONES --- + + createZones: (zones) => zoneActions.createZonesAction(set, get, zones), + createZone: (zone) => zoneActions.createZonesAction(set, get, [zone]), + + updateZones: (updates) => zoneActions.updateZonesAction(set, get, updates), + updateZone: (id, data) => zoneActions.updateZonesAction(set, get, [{ id, data }]), + + deleteZones: (ids) => zoneActions.deleteZonesAction(set, get, ids), + deleteZone: (id) => zoneActions.deleteZonesAction(set, get, [id]), + }), + { + partialize: (state) => { + const { nodes, rootNodeIds } = state // Only track nodes and rootNodeIds in history + return { nodes, rootNodeIds } + }, + limit: 50, // Limit to last 50 actions }, - - markDirty: (id) => { - get().dirtyNodes.add(id); - }, - - clearDirty: (id) => { - get().dirtyNodes.delete(id); - }, - - createNodes: (ops) => nodeActions.createNodesAction(set, get, ops), - createNode: (node, parentId) => - nodeActions.createNodesAction(set, get, [{ node, parentId }]), - - updateNodes: (updates) => - nodeActions.updateNodesAction(set, get, updates), - updateNode: (id, data) => - nodeActions.updateNodesAction(set, get, [{ id, data }]), - - // --- DELETE --- - - deleteNodes: (ids) => nodeActions.deleteNodesAction(set, get, ids), - - deleteNode: (id) => nodeActions.deleteNodesAction(set, get, [id]), - - // --- ZONES --- - - createZones: (zones) => zoneActions.createZonesAction(set, get, zones), - createZone: (zone) => zoneActions.createZonesAction(set, get, [zone]), - - updateZones: (updates) => zoneActions.updateZonesAction(set, get, updates), - updateZone: (id, data) => - zoneActions.updateZonesAction(set, get, [{ id, data }]), - - deleteZones: (ids) => zoneActions.deleteZonesAction(set, get, ids), - deleteZone: (id) => zoneActions.deleteZonesAction(set, get, [id]), - }), + ), { - partialize: (state) => { - const { nodes, rootNodeIds } = state; // Only track nodes and rootNodeIds in history - return { nodes, rootNodeIds }; + name: 'editor-storage', + partialize: (state) => ({ + nodes: state.nodes, + rootNodeIds: state.rootNodeIds, + zones: state.zones, + zoneIds: state.zoneIds, + }), + onRehydrateStorage: (state) => { + console.log('hydration starts') + + // optional + return (state, error) => { + if (error) { + console.log('an error happened during hydration', error) + } else { + console.log('hydration finished') + } + } }, - limit: 50, // Limit to last 50 actions }, ), -); +) -export default useScene; +export default useScene // Subscribe to the temporal store (Undo/Redo events) useScene.temporal.subscribe((state, prevState) => { // Check if we just jumped in time (Undo/Redo) // If the 'nodes' object changed but it wasn't a normal 'set' - const currentNodes = useScene.getState().nodes; + const currentNodes = useScene.getState().nodes // Trigger a full scene re-validation Object.values(currentNodes).forEach((node) => { - if (node.type === "wall") { - useScene.getState().markDirty(node.id); + if (node.type === 'wall') { + useScene.getState().markDirty(node.id) } - }); -}); + }) +}) diff --git a/packages/viewer/src/components/renderers/item/item-renderer.tsx b/packages/viewer/src/components/renderers/item/item-renderer.tsx index 16b68075..adb7a0fe 100644 --- a/packages/viewer/src/components/renderers/item/item-renderer.tsx +++ b/packages/viewer/src/components/renderers/item/item-renderer.tsx @@ -39,6 +39,7 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => { if ((child as Mesh).isMesh) { child.castShadow = true child.receiveShadow = true + // TODO: do it with a shared material child.material = new MeshStandardNodeMaterial({ color: 0xffffff, roughness: 0.8,