data persist
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import useEditor from "@/store/use-editor";
|
import useEditor from "@/store/use-editor";
|
||||||
import { useViewer } from "@pascal-app/viewer";
|
import { useViewer } from "@pascal-app/viewer";
|
||||||
|
import { SettingsPanel } from "./panels/settings-panel";
|
||||||
import { SitePanel } from "./panels/site-panel";
|
import { SitePanel } from "./panels/site-panel";
|
||||||
import { ZonePanel } from "./panels/zone-panel";
|
import { ZonePanel } from "./panels/zone-panel";
|
||||||
|
|
||||||
@@ -83,13 +84,8 @@ export function AppSidebar() {
|
|||||||
return <ZonePanel />;
|
return <ZonePanel />;
|
||||||
// case "collections":
|
// case "collections":
|
||||||
// return <CollectionsPanel />;
|
// return <CollectionsPanel />;
|
||||||
// case "settings":
|
case "settings":
|
||||||
// return (
|
return <SettingsPanel />;
|
||||||
// <SettingsPanel
|
|
||||||
// onOpenHelp={() => setIsHelpOpen(true)}
|
|
||||||
// onOpenJsonInspector={() => setIsJsonInspectorOpen(true)}
|
|
||||||
// />
|
|
||||||
// );
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="flex flex-col gap-6 p-3">
|
||||||
|
{/* Danger Zone */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="font-medium text-destructive text-xs uppercase">
|
||||||
|
Danger Zone
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="w-full justify-start gap-2"
|
||||||
|
onClick={handleResetToDefault}
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
Clear & Start New
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,213 +1,187 @@
|
|||||||
"use client";
|
'use client'
|
||||||
|
|
||||||
import { temporal } from "zundo";
|
import { temporal } from 'zundo'
|
||||||
import { create } from "zustand";
|
import { create } from 'zustand'
|
||||||
import { BuildingNode, ItemNode, type Zone } from "../schema";
|
import { persist } from 'zustand/middleware'
|
||||||
import { LevelNode } from "../schema/nodes/level";
|
import { BuildingNode, type Zone } from '../schema'
|
||||||
import { WallNode } from "../schema/nodes/wall";
|
import { LevelNode } from '../schema/nodes/level'
|
||||||
import type { AnyNode, AnyNodeId } from "../schema/types";
|
import type { AnyNode, AnyNodeId } from '../schema/types'
|
||||||
import * as nodeActions from "./actions/node-actions";
|
import * as nodeActions from './actions/node-actions'
|
||||||
import * as zoneActions from "./actions/zone-actions";
|
import * as zoneActions from './actions/zone-actions'
|
||||||
|
|
||||||
export type SceneState = {
|
export type SceneState = {
|
||||||
// 1. The Data: A flat dictionary of all nodes
|
// 1. The Data: A flat dictionary of all nodes
|
||||||
nodes: Record<AnyNodeId, AnyNode>;
|
nodes: Record<AnyNodeId, AnyNode>
|
||||||
zones: Record<Zone["id"], Zone>;
|
zones: Record<Zone['id'], Zone>
|
||||||
|
|
||||||
// 2. The Root: Which nodes are at the top level?
|
// 2. The Root: Which nodes are at the top level?
|
||||||
rootNodeIds: AnyNodeId[];
|
rootNodeIds: AnyNodeId[]
|
||||||
zoneIds: Zone["id"][];
|
zoneIds: Zone['id'][]
|
||||||
|
|
||||||
// 3. The "Dirty" Set: For the Wall/Physics systems
|
// 3. The "Dirty" Set: For the Wall/Physics systems
|
||||||
dirtyNodes: Set<AnyNodeId>;
|
dirtyNodes: Set<AnyNodeId>
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
loadScene: () => void;
|
loadScene: () => void
|
||||||
markDirty: (id: AnyNodeId) => void;
|
clearScene: () => void
|
||||||
clearDirty: (id: AnyNodeId) => void;
|
|
||||||
|
|
||||||
createNode: (node: AnyNode, parentId?: AnyNodeId) => void;
|
markDirty: (id: AnyNodeId) => void
|
||||||
createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void;
|
clearDirty: (id: AnyNodeId) => void
|
||||||
|
|
||||||
updateNode: (id: AnyNodeId, data: Partial<AnyNode>) => void;
|
createNode: (node: AnyNode, parentId?: AnyNodeId) => void
|
||||||
updateNodes: (updates: { id: AnyNodeId; data: Partial<AnyNode> }[]) => void;
|
createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void
|
||||||
|
|
||||||
deleteNode: (id: AnyNodeId) => void;
|
updateNode: (id: AnyNodeId, data: Partial<AnyNode>) => void
|
||||||
deleteNodes: (ids: AnyNodeId[]) => void;
|
updateNodes: (updates: { id: AnyNodeId; data: Partial<AnyNode> }[]) => void
|
||||||
|
|
||||||
|
deleteNode: (id: AnyNodeId) => void
|
||||||
|
deleteNodes: (ids: AnyNodeId[]) => void
|
||||||
|
|
||||||
// Zone actions
|
// Zone actions
|
||||||
createZone: (zone: Zone) => void;
|
createZone: (zone: Zone) => void
|
||||||
createZones: (zones: Zone[]) => void;
|
createZones: (zones: Zone[]) => void
|
||||||
updateZone: (id: Zone["id"], data: Partial<Zone>) => void;
|
updateZone: (id: Zone['id'], data: Partial<Zone>) => void
|
||||||
updateZones: (updates: { id: Zone["id"]; data: Partial<Zone> }[]) => void;
|
updateZones: (updates: { id: Zone['id']; data: Partial<Zone> }[]) => void
|
||||||
deleteZone: (id: Zone["id"]) => void;
|
deleteZone: (id: Zone['id']) => void
|
||||||
deleteZones: (ids: Zone["id"][]) => void;
|
deleteZones: (ids: Zone['id'][]) => void
|
||||||
};
|
}
|
||||||
|
|
||||||
// type PartializedStoreState = Pick<SceneState, 'rootNodeIds' | 'nodes'>;
|
// type PartializedStoreState = Pick<SceneState, 'rootNodeIds' | 'nodes'>;
|
||||||
|
|
||||||
const useScene = create<SceneState>()(
|
const useScene = create<SceneState>()(
|
||||||
temporal(
|
persist(
|
||||||
(set, get) => ({
|
temporal(
|
||||||
// 1. Flat dictionary of all nodes
|
(set, get) => ({
|
||||||
nodes: {},
|
// 1. Flat dictionary of all nodes
|
||||||
zones: {},
|
nodes: {},
|
||||||
|
zones: {},
|
||||||
|
|
||||||
// 2. Root node IDs
|
// 2. Root node IDs
|
||||||
rootNodeIds: [],
|
rootNodeIds: [],
|
||||||
zoneIds: [],
|
zoneIds: [],
|
||||||
|
|
||||||
// 3. Dirty set
|
// 3. Dirty set
|
||||||
dirtyNodes: new Set<AnyNodeId>(),
|
dirtyNodes: new Set<AnyNodeId>(),
|
||||||
|
|
||||||
loadScene: () => {
|
clearScene: () => {
|
||||||
const building = BuildingNode.parse({
|
set({
|
||||||
children: [],
|
nodes: {},
|
||||||
});
|
rootNodeIds: [],
|
||||||
|
zones: {},
|
||||||
|
zoneIds: [],
|
||||||
|
dirtyNodes: new Set<AnyNodeId>(),
|
||||||
|
})
|
||||||
|
get().loadScene() // Default scene
|
||||||
|
},
|
||||||
|
|
||||||
const level0 = LevelNode.parse({
|
loadScene: () => {
|
||||||
level: 0,
|
if (get().rootNodeIds.length > 0) {
|
||||||
children: [],
|
// Assign all nodes as dirty to force re-validation
|
||||||
});
|
Object.values(get().nodes).forEach((node) => {
|
||||||
const level1 = LevelNode.parse({
|
get().markDirty(node.id)
|
||||||
level: 1,
|
})
|
||||||
children: [],
|
return // Scene already loaded
|
||||||
});
|
}
|
||||||
const level2 = LevelNode.parse({
|
|
||||||
level: 2,
|
|
||||||
children: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const wall0 = WallNode.parse({
|
const building = BuildingNode.parse({
|
||||||
start: [0, 0],
|
children: [],
|
||||||
end: [5, 0],
|
})
|
||||||
children: [],
|
|
||||||
parentId: level0.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wall1 = WallNode.parse({
|
const level0 = LevelNode.parse({
|
||||||
start: [0, 0],
|
level: 0,
|
||||||
end: [0, 5],
|
children: [],
|
||||||
children: [],
|
})
|
||||||
parentId: level0.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wall2 = WallNode.parse({
|
building.children.push(level0.id)
|
||||||
start: [5, 5],
|
|
||||||
end: [0, 5],
|
|
||||||
children: [],
|
|
||||||
parentId: level0.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const wall3 = WallNode.parse({
|
// Define all nodes flat
|
||||||
start: [5, 5],
|
const nodes: Record<AnyNodeId, AnyNode> = {
|
||||||
end: [5, 0],
|
[building.id]: building,
|
||||||
children: [],
|
[level0.id]: level0,
|
||||||
parentId: level1.id,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const window1 = ItemNode.parse({
|
// Root nodes are the levels
|
||||||
type: "item",
|
const rootNodeIds = [building.id]
|
||||||
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",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
wall3.children.push(window1.id);
|
set({ nodes, rootNodeIds })
|
||||||
|
},
|
||||||
|
|
||||||
level0.children.push(wall0.id, wall1.id, wall2.id);
|
markDirty: (id) => {
|
||||||
level1.children.push(wall3.id);
|
get().dirtyNodes.add(id)
|
||||||
|
},
|
||||||
|
|
||||||
building.children.push(level0.id, level1.id, level2.id);
|
clearDirty: (id) => {
|
||||||
|
get().dirtyNodes.delete(id)
|
||||||
|
},
|
||||||
|
|
||||||
// Define all nodes flat
|
createNodes: (ops) => nodeActions.createNodesAction(set, get, ops),
|
||||||
const nodes: Record<AnyNodeId, AnyNode> = {
|
createNode: (node, parentId) =>
|
||||||
[building.id]: building,
|
nodeActions.createNodesAction(set, get, [{ node, parentId }]),
|
||||||
[level0.id]: level0,
|
|
||||||
[level1.id]: level1,
|
|
||||||
[level2.id]: level2,
|
|
||||||
[wall0.id]: wall0,
|
|
||||||
[wall1.id]: wall1,
|
|
||||||
[wall2.id]: wall2,
|
|
||||||
[wall3.id]: wall3,
|
|
||||||
[window1.id]: window1,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Root nodes are the levels
|
updateNodes: (updates) => nodeActions.updateNodesAction(set, get, updates),
|
||||||
const rootNodeIds = [building.id];
|
updateNode: (id, data) => nodeActions.updateNodesAction(set, get, [{ id, data }]),
|
||||||
|
|
||||||
get().dirtyNodes.add(wall0.id);
|
// --- DELETE ---
|
||||||
get().dirtyNodes.add(wall1.id);
|
|
||||||
get().dirtyNodes.add(wall2.id);
|
deleteNodes: (ids) => nodeActions.deleteNodesAction(set, get, ids),
|
||||||
get().dirtyNodes.add(wall3.id);
|
|
||||||
set({ nodes, rootNodeIds });
|
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) => {
|
name: 'editor-storage',
|
||||||
const { nodes, rootNodeIds } = state; // Only track nodes and rootNodeIds in history
|
partialize: (state) => ({
|
||||||
return { nodes, rootNodeIds };
|
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)
|
// Subscribe to the temporal store (Undo/Redo events)
|
||||||
useScene.temporal.subscribe((state, prevState) => {
|
useScene.temporal.subscribe((state, prevState) => {
|
||||||
// Check if we just jumped in time (Undo/Redo)
|
// Check if we just jumped in time (Undo/Redo)
|
||||||
// If the 'nodes' object changed but it wasn't a normal 'set'
|
// 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
|
// Trigger a full scene re-validation
|
||||||
Object.values(currentNodes).forEach((node) => {
|
Object.values(currentNodes).forEach((node) => {
|
||||||
if (node.type === "wall") {
|
if (node.type === 'wall') {
|
||||||
useScene.getState().markDirty(node.id);
|
useScene.getState().markDirty(node.id)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const ModelRenderer = ({ node }: { node: ItemNode }) => {
|
|||||||
if ((child as Mesh).isMesh) {
|
if ((child as Mesh).isMesh) {
|
||||||
child.castShadow = true
|
child.castShadow = true
|
||||||
child.receiveShadow = true
|
child.receiveShadow = true
|
||||||
|
// TODO: do it with a shared material
|
||||||
child.material = new MeshStandardNodeMaterial({
|
child.material = new MeshStandardNodeMaterial({
|
||||||
color: 0xffffff,
|
color: 0xffffff,
|
||||||
roughness: 0.8,
|
roughness: 0.8,
|
||||||
|
|||||||
Reference in New Issue
Block a user