draft zone

This commit is contained in:
wass08
2026-01-22 09:32:28 +09:00
parent 1f23792f9c
commit 3ab5077ea0
3 changed files with 154 additions and 63 deletions
@@ -0,0 +1,3 @@
export const ZoneTool: React.FC = () => {
return null;
};
@@ -0,0 +1,60 @@
import type { Zone } from '../../schema'
import type { SceneState } from '../use-scene'
export const createZonesAction = (
set: (fn: (state: SceneState) => Partial<SceneState>) => 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<SceneState>) => void,
_get: () => SceneState,
updates: { id: Zone['id']; data: Partial<Zone> }[],
) => {
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<SceneState>) => 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 }
})
}
+91 -63
View File
@@ -1,37 +1,48 @@
'use client' "use client";
import { temporal } from 'zundo' import { temporal } from "zundo";
import { create } from 'zustand' import { create } from "zustand";
import { BuildingNode, ItemNode } from '../schema' import { BuildingNode, ItemNode, type Zone } from "../schema";
import { LevelNode } from '../schema/nodes/level' import { LevelNode } from "../schema/nodes/level";
import { WallNode } from '../schema/nodes/wall' import { WallNode } from "../schema/nodes/wall";
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";
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>;
// 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"][];
// 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 markDirty: (id: AnyNodeId) => void;
clearDirty: (id: AnyNodeId) => void clearDirty: (id: AnyNodeId) => void;
createNode: (node: AnyNode, parentId?: AnyNodeId) => void createNode: (node: AnyNode, parentId?: AnyNodeId) => void;
createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void;
updateNode: (id: AnyNodeId, data: Partial<AnyNode>) => void updateNode: (id: AnyNodeId, data: Partial<AnyNode>) => void;
updateNodes: (updates: { id: AnyNodeId; data: Partial<AnyNode> }[]) => void updateNodes: (updates: { id: AnyNodeId; data: Partial<AnyNode> }[]) => void;
deleteNode: (id: AnyNodeId) => void deleteNode: (id: AnyNodeId) => void;
deleteNodes: (ids: AnyNodeId[]) => void deleteNodes: (ids: AnyNodeId[]) => void;
}
// Zone actions
createZone: (zone: Zone) => void;
createZones: (zones: Zone[]) => void;
updateZone: (id: Zone["id"], data: Partial<Zone>) => void;
updateZones: (updates: { id: Zone["id"]; data: Partial<Zone> }[]) => void;
deleteZone: (id: Zone["id"]) => void;
deleteZones: (ids: Zone["id"][]) => void;
};
// type PartializedStoreState = Pick<SceneState, 'rootNodeIds' | 'nodes'>; // type PartializedStoreState = Pick<SceneState, 'rootNodeIds' | 'nodes'>;
@@ -40,9 +51,11 @@ const useScene = create<SceneState>()(
(set, get) => ({ (set, get) => ({
// 1. Flat dictionary of all nodes // 1. Flat dictionary of all nodes
nodes: {}, nodes: {},
zones: {},
// 2. Root node IDs // 2. Root node IDs
rootNodeIds: [], rootNodeIds: [],
zoneIds: [],
// 3. Dirty set // 3. Dirty set
dirtyNodes: new Set<AnyNodeId>(), dirtyNodes: new Set<AnyNodeId>(),
@@ -50,70 +63,70 @@ const useScene = create<SceneState>()(
loadScene: () => { loadScene: () => {
const building = BuildingNode.parse({ const building = BuildingNode.parse({
children: [], children: [],
}) });
const level0 = LevelNode.parse({ const level0 = LevelNode.parse({
level: 0, level: 0,
children: [], children: [],
}) });
const level1 = LevelNode.parse({ const level1 = LevelNode.parse({
level: 1, level: 1,
children: [], children: [],
}) });
const level2 = LevelNode.parse({ const level2 = LevelNode.parse({
level: 2, level: 2,
children: [], children: [],
}) });
const wall0 = WallNode.parse({ const wall0 = WallNode.parse({
start: [0, 0], start: [0, 0],
end: [5, 0], end: [5, 0],
children: [], children: [],
parentId: level0.id, parentId: level0.id,
}) });
const wall1 = WallNode.parse({ const wall1 = WallNode.parse({
start: [0, 0], start: [0, 0],
end: [0, 5], end: [0, 5],
children: [], children: [],
parentId: level0.id, parentId: level0.id,
}) });
const wall2 = WallNode.parse({ const wall2 = WallNode.parse({
start: [5, 5], start: [5, 5],
end: [0, 5], end: [0, 5],
children: [], children: [],
parentId: level0.id, parentId: level0.id,
}) });
const wall3 = WallNode.parse({ const wall3 = WallNode.parse({
start: [5, 5], start: [5, 5],
end: [5, 0], end: [5, 0],
children: [], children: [],
parentId: level1.id, parentId: level1.id,
}) });
const window1 = ItemNode.parse({ const window1 = ItemNode.parse({
type: 'item', type: "item",
name: 'Window', name: "Window",
position: [2.5, 0.5, 0], position: [2.5, 0.5, 0],
parentId: wall3.id, parentId: wall3.id,
asset: { asset: {
id: 'window-round', id: "window-round",
name: 'Round Window', name: "Round Window",
thumbnail: '/items/window-small/thumbnail.png', thumbnail: "/items/window-small/thumbnail.png",
category: 'windows', category: "windows",
attachTo: 'wall', attachTo: "wall",
src: '/items/window-small/model.glb', 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) level0.children.push(wall0.id, wall1.id, wall2.id);
level1.children.push(wall3.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 // Define all nodes flat
const nodes: Record<AnyNodeId, AnyNode> = { const nodes: Record<AnyNodeId, AnyNode> = {
@@ -126,60 +139,75 @@ const useScene = create<SceneState>()(
[wall2.id]: wall2, [wall2.id]: wall2,
[wall3.id]: wall3, [wall3.id]: wall3,
[window1.id]: window1, [window1.id]: window1,
} };
// Root nodes are the levels // Root nodes are the levels
const rootNodeIds = [building.id] const rootNodeIds = [building.id];
get().dirtyNodes.add(wall0.id) get().dirtyNodes.add(wall0.id);
get().dirtyNodes.add(wall1.id) get().dirtyNodes.add(wall1.id);
get().dirtyNodes.add(wall2.id) get().dirtyNodes.add(wall2.id);
get().dirtyNodes.add(wall3.id) get().dirtyNodes.add(wall3.id);
set({ nodes, rootNodeIds }) set({ nodes, rootNodeIds });
}, },
markDirty: (id) => { markDirty: (id) => {
get().dirtyNodes.add(id) get().dirtyNodes.add(id);
}, },
clearDirty: (id) => { clearDirty: (id) => {
get().dirtyNodes.delete(id) get().dirtyNodes.delete(id);
}, },
createNodes: (ops) => nodeActions.createNodesAction(set, get, ops), 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), updateNodes: (updates) =>
updateNode: (id, data) => nodeActions.updateNodesAction(set, get, [{ id, data }]), nodeActions.updateNodesAction(set, get, updates),
updateNode: (id, data) =>
nodeActions.updateNodesAction(set, get, [{ id, data }]),
// --- DELETE --- // --- DELETE ---
deleteNodes: (ids) => nodeActions.deleteNodesAction(set, get, ids), deleteNodes: (ids) => nodeActions.deleteNodesAction(set, get, ids),
deleteNode: (id) => nodeActions.deleteNodesAction(set, get, [id]), 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) => { partialize: (state) => {
const { nodes, rootNodeIds } = state // Only track nodes and rootNodeIds in history const { nodes, rootNodeIds } = state; // Only track nodes and rootNodeIds in history
return { nodes, rootNodeIds } return { nodes, rootNodeIds };
}, },
limit: 50, // Limit to last 50 actions 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);
} }
}) });
}) });