diff --git a/apps/editor/components/tools/zone/zone-tool.tsx b/apps/editor/components/tools/zone/zone-tool.tsx new file mode 100644 index 00000000..1b3b3715 --- /dev/null +++ b/apps/editor/components/tools/zone/zone-tool.tsx @@ -0,0 +1,3 @@ +export const ZoneTool: React.FC = () => { + return null; +}; diff --git a/packages/core/src/store/actions/zone-actions.ts b/packages/core/src/store/actions/zone-actions.ts new file mode 100644 index 00000000..9ad10b9c --- /dev/null +++ b/packages/core/src/store/actions/zone-actions.ts @@ -0,0 +1,60 @@ +import type { Zone } from '../../schema' +import type { SceneState } from '../use-scene' + +export const createZonesAction = ( + set: (fn: (state: SceneState) => Partial) => void, + _get: () => SceneState, + zones: Zone[], +) => { + set((state) => { + const nextZones = { ...state.zones } + const nextZoneIds = [...state.zoneIds] + + for (const zone of zones) { + nextZones[zone.id] = zone + + if (!nextZoneIds.includes(zone.id)) { + nextZoneIds.push(zone.id) + } + } + + return { zones: nextZones, zoneIds: nextZoneIds } + }) +} + +export const updateZonesAction = ( + set: (fn: (state: SceneState) => Partial) => void, + _get: () => SceneState, + updates: { id: Zone['id']; data: Partial }[], +) => { + set((state) => { + const nextZones = { ...state.zones } + + for (const { id, data } of updates) { + const currentZone = nextZones[id] + if (!currentZone) continue + + nextZones[id] = { ...currentZone, ...data } + } + + return { zones: nextZones } + }) +} + +export const deleteZonesAction = ( + set: (fn: (state: SceneState) => Partial) => void, + _get: () => SceneState, + ids: Zone['id'][], +) => { + set((state) => { + const nextZones = { ...state.zones } + let nextZoneIds = [...state.zoneIds] + + for (const id of ids) { + delete nextZones[id] + nextZoneIds = nextZoneIds.filter((zid) => zid !== id) + } + + return { zones: nextZones, zoneIds: nextZoneIds } + }) +} diff --git a/packages/core/src/store/use-scene.ts b/packages/core/src/store/use-scene.ts index a4bb0c38..8b762806 100644 --- a/packages/core/src/store/use-scene.ts +++ b/packages/core/src/store/use-scene.ts @@ -1,37 +1,48 @@ -'use client' +"use client"; -import { temporal } from 'zundo' -import { create } from 'zustand' -import { BuildingNode, ItemNode } 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 { 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"; export type SceneState = { // 1. The Data: A flat dictionary of all nodes - nodes: Record + nodes: Record; + zones: Record; // 2. The Root: Which nodes are at the top level? - rootNodeIds: AnyNodeId[] + 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; + markDirty: (id: AnyNodeId) => void; + clearDirty: (id: AnyNodeId) => void; - createNode: (node: AnyNode, parentId?: AnyNodeId) => void - createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void + createNode: (node: AnyNode, parentId?: AnyNodeId) => void; + createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void; - updateNode: (id: AnyNodeId, data: Partial) => void - updateNodes: (updates: { id: AnyNodeId; data: Partial }[]) => void + updateNode: (id: AnyNodeId, data: Partial) => void; + updateNodes: (updates: { id: AnyNodeId; data: Partial }[]) => void; - deleteNode: (id: AnyNodeId) => void - deleteNodes: (ids: AnyNodeId[]) => 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; +}; // type PartializedStoreState = Pick; @@ -40,9 +51,11 @@ const useScene = create()( (set, get) => ({ // 1. Flat dictionary of all nodes nodes: {}, + zones: {}, // 2. Root node IDs rootNodeIds: [], + zoneIds: [], // 3. Dirty set dirtyNodes: new Set(), @@ -50,70 +63,70 @@ const useScene = create()( loadScene: () => { const building = BuildingNode.parse({ children: [], - }) + }); const level0 = LevelNode.parse({ level: 0, children: [], - }) + }); const level1 = LevelNode.parse({ level: 1, children: [], - }) + }); const level2 = LevelNode.parse({ level: 2, children: [], - }) + }); const wall0 = WallNode.parse({ start: [0, 0], end: [5, 0], children: [], parentId: level0.id, - }) + }); const wall1 = WallNode.parse({ start: [0, 0], end: [0, 5], children: [], parentId: level0.id, - }) + }); const wall2 = WallNode.parse({ start: [5, 5], end: [0, 5], children: [], parentId: level0.id, - }) + }); const wall3 = WallNode.parse({ start: [5, 5], end: [5, 0], children: [], parentId: level1.id, - }) + }); const window1 = ItemNode.parse({ - type: 'item', - name: 'Window', + 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', + 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) + wall3.children.push(window1.id); - level0.children.push(wall0.id, wall1.id, wall2.id) - level1.children.push(wall3.id) + level0.children.push(wall0.id, wall1.id, wall2.id); + level1.children.push(wall3.id); - building.children.push(level0.id, level1.id, level2.id) + building.children.push(level0.id, level1.id, level2.id); // Define all nodes flat const nodes: Record = { @@ -126,60 +139,75 @@ const useScene = create()( [wall2.id]: wall2, [wall3.id]: wall3, [window1.id]: window1, - } + }; // Root nodes are the levels - const rootNodeIds = [building.id] + const rootNodeIds = [building.id]; - get().dirtyNodes.add(wall0.id) - get().dirtyNodes.add(wall1.id) - get().dirtyNodes.add(wall2.id) - get().dirtyNodes.add(wall3.id) - set({ nodes, rootNodeIds }) + get().dirtyNodes.add(wall0.id); + get().dirtyNodes.add(wall1.id); + get().dirtyNodes.add(wall2.id); + get().dirtyNodes.add(wall3.id); + set({ nodes, rootNodeIds }); }, markDirty: (id) => { - get().dirtyNodes.add(id) + get().dirtyNodes.add(id); }, clearDirty: (id) => { - get().dirtyNodes.delete(id) + get().dirtyNodes.delete(id); }, createNodes: (ops) => nodeActions.createNodesAction(set, get, ops), - createNode: (node, parentId) => nodeActions.createNodesAction(set, get, [{ node, parentId }]), + 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 }]), + 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 } + const { nodes, rootNodeIds } = state; // Only track nodes and rootNodeIds in history + return { nodes, rootNodeIds }; }, 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); } - }) -}) + }); +});