zones as nodes
This commit is contained in:
@@ -1,74 +1,64 @@
|
||||
import type { ThreeEvent } from "@react-three/fiber";
|
||||
import mitt from "mitt";
|
||||
import type { BuildingNode, ItemNode, WallNode } from "../schema";
|
||||
import type { AnyNode } from "../schema/types";
|
||||
import type { Zone } from "../schema/zone";
|
||||
import type { ThreeEvent } from '@react-three/fiber'
|
||||
import mitt from 'mitt'
|
||||
import type { BuildingNode, ItemNode, WallNode, ZoneNode } from '../schema'
|
||||
import type { AnyNode } from '../schema/types'
|
||||
|
||||
// Base event interfaces
|
||||
export interface GridEvent {
|
||||
position: [number, number, number];
|
||||
nativeEvent: ThreeEvent<PointerEvent>;
|
||||
position: [number, number, number]
|
||||
nativeEvent: ThreeEvent<PointerEvent>
|
||||
}
|
||||
|
||||
export interface NodeEvent<T extends AnyNode = AnyNode> {
|
||||
node: T;
|
||||
position: [number, number, number];
|
||||
localPosition: [number, number, number];
|
||||
normal?: [number, number, number];
|
||||
stopPropagation: () => void;
|
||||
nativeEvent: ThreeEvent<PointerEvent>;
|
||||
node: T
|
||||
position: [number, number, number]
|
||||
localPosition: [number, number, number]
|
||||
normal?: [number, number, number]
|
||||
stopPropagation: () => void
|
||||
nativeEvent: ThreeEvent<PointerEvent>
|
||||
}
|
||||
|
||||
export interface ZoneEvent {
|
||||
zone: Zone;
|
||||
position: [number, number, number];
|
||||
stopPropagation: () => void;
|
||||
nativeEvent: ThreeEvent<PointerEvent>;
|
||||
}
|
||||
|
||||
export type WallEvent = NodeEvent<WallNode>;
|
||||
export type ItemEvent = NodeEvent<ItemNode>;
|
||||
export type BuildingEvent = NodeEvent<BuildingNode>;
|
||||
export type WallEvent = NodeEvent<WallNode>
|
||||
export type ItemEvent = NodeEvent<ItemNode>
|
||||
export type BuildingEvent = NodeEvent<BuildingNode>
|
||||
export type ZoneEvent = NodeEvent<ZoneNode>
|
||||
|
||||
// Event suffixes - exported for use in hooks
|
||||
export const eventSuffixes = [
|
||||
"click",
|
||||
"move",
|
||||
"enter",
|
||||
"leave",
|
||||
"pointerdown",
|
||||
"pointerup",
|
||||
"context-menu",
|
||||
"double-click",
|
||||
] as const;
|
||||
'click',
|
||||
'move',
|
||||
'enter',
|
||||
'leave',
|
||||
'pointerdown',
|
||||
'pointerup',
|
||||
'context-menu',
|
||||
'double-click',
|
||||
] as const
|
||||
|
||||
export type EventSuffix = (typeof eventSuffixes)[number];
|
||||
export type EventSuffix = (typeof eventSuffixes)[number]
|
||||
|
||||
type NodeEvents<T extends string, E> = {
|
||||
[K in `${T}:${EventSuffix}`]: E;
|
||||
};
|
||||
[K in `${T}:${EventSuffix}`]: E
|
||||
}
|
||||
|
||||
type GridEvents = {
|
||||
[K in `grid:${EventSuffix}`]: GridEvent;
|
||||
};
|
||||
[K in `grid:${EventSuffix}`]: GridEvent
|
||||
}
|
||||
|
||||
export interface CameraControlEvent {
|
||||
nodeId: AnyNode["id"];
|
||||
nodeId: AnyNode['id']
|
||||
}
|
||||
|
||||
type CameraControlEvents = {
|
||||
"camera-controls:view": CameraControlEvent;
|
||||
"camera-controls:capture": CameraControlEvent;
|
||||
};
|
||||
type ZoneEvents = {
|
||||
[K in `zone:${EventSuffix}`]: ZoneEvent;
|
||||
};
|
||||
'camera-controls:view': CameraControlEvent
|
||||
'camera-controls:capture': CameraControlEvent
|
||||
}
|
||||
|
||||
type EditorEvents = GridEvents &
|
||||
NodeEvents<"wall", WallEvent> &
|
||||
NodeEvents<"item", ItemEvent> &
|
||||
NodeEvents<"building", BuildingEvent> &
|
||||
ZoneEvents &
|
||||
CameraControlEvents;
|
||||
NodeEvents<'wall', WallEvent> &
|
||||
NodeEvents<'item', ItemEvent> &
|
||||
NodeEvents<'building', BuildingEvent> &
|
||||
NodeEvents<'zone', ZoneEvent> &
|
||||
CameraControlEvents
|
||||
|
||||
export const emitter = mitt<EditorEvents>();
|
||||
export const emitter = mitt<EditorEvents>()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Store
|
||||
|
||||
export type {
|
||||
BuildingEvent,
|
||||
CameraControlEvent,
|
||||
EventSuffix,
|
||||
GridEvent,
|
||||
@@ -27,4 +28,4 @@ export { default as useScene } from './store/use-scene'
|
||||
// Systems
|
||||
export { WallSystem } from './systems/wall/wall-system'
|
||||
|
||||
export { isObject } from './utils/types'
|
||||
export { isObject } from './utils/types'
|
||||
|
||||
@@ -9,10 +9,8 @@ export { LevelNode } from './nodes/level'
|
||||
// Nodes
|
||||
export { SiteNode } from './nodes/site'
|
||||
export { WallNode } from './nodes/wall'
|
||||
// Zones
|
||||
export type { ZoneNode, ZonePolygon } from './nodes/zone'
|
||||
export type { AnyNodeId, AnyNodeType } from './types'
|
||||
// Union types
|
||||
export { AnyNode } from './types'
|
||||
|
||||
// Zones
|
||||
export type { Zone, ZonePolygon } from './zone'
|
||||
export { ZoneSchema } from './zone'
|
||||
|
||||
@@ -2,11 +2,13 @@ import dedent from 'dedent'
|
||||
import { z } from 'zod'
|
||||
import { BaseNode, nodeType, objectId } from '../base'
|
||||
import { WallNode } from './wall'
|
||||
import { ZoneNode } from './zone'
|
||||
|
||||
|
||||
export const LevelNode = BaseNode.extend({
|
||||
id: objectId('level'),
|
||||
type: nodeType('level'),
|
||||
children: z.array(WallNode.shape.id).default([]),
|
||||
children: z.array(z.union([WallNode.shape.id, ZoneNode.shape.id])).default([]),
|
||||
// Specific props
|
||||
level: z.number().default(0),
|
||||
}).describe(
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import dedent from 'dedent'
|
||||
import { z } from 'zod'
|
||||
import { BaseNode, nodeType, objectId } from '../base'
|
||||
|
||||
// Polygon boundary for zone area - array of [x, z] coordinates
|
||||
export const ZonePolygon = z.array(z.tuple([z.number(), z.number()]))
|
||||
|
||||
export const ZoneNode = BaseNode.extend({
|
||||
id: objectId('zone'),
|
||||
type: nodeType('zone'),
|
||||
name: z.string(),
|
||||
// Polygon boundary - array of [x, z] coordinates defining the zone
|
||||
polygon: ZonePolygon,
|
||||
// Visual styling
|
||||
color: z.string().default('#3b82f6'), // Default blue
|
||||
metadata: z.json().optional().default({}),
|
||||
}).describe(
|
||||
dedent`
|
||||
Zone schema - a polygon zone attached to a level
|
||||
- object: "zone"
|
||||
- id: zone id
|
||||
- levelId: level this zone is attached to
|
||||
- name: zone name
|
||||
- polygon: array of [x, z] points defining the zone boundary
|
||||
- color: hex color for visual styling
|
||||
- metadata: zone metadata (optional)
|
||||
`,
|
||||
)
|
||||
|
||||
export type ZoneNode = z.infer<typeof ZoneNode>
|
||||
export type ZonePolygon = z.infer<typeof ZonePolygon>
|
||||
@@ -4,6 +4,7 @@ import { ItemNode } from './nodes/item'
|
||||
import { LevelNode } from './nodes/level'
|
||||
import { SiteNode } from './nodes/site'
|
||||
import { WallNode } from './nodes/wall'
|
||||
import { ZoneNode } from './nodes/zone'
|
||||
|
||||
export const AnyNode = z.discriminatedUnion('type', [
|
||||
SiteNode,
|
||||
@@ -11,6 +12,7 @@ export const AnyNode = z.discriminatedUnion('type', [
|
||||
LevelNode,
|
||||
WallNode,
|
||||
ItemNode,
|
||||
ZoneNode,
|
||||
])
|
||||
|
||||
export type AnyNode = z.infer<typeof AnyNode>
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import dedent from 'dedent'
|
||||
import { z } from 'zod'
|
||||
import { objectId } from './base'
|
||||
import { LevelNode } from './nodes/level'
|
||||
|
||||
// Polygon boundary for zone area - array of [x, z] coordinates
|
||||
export const ZonePolygon = z.array(z.tuple([z.number(), z.number()]))
|
||||
|
||||
export const ZoneSchema = z
|
||||
.object({
|
||||
id: objectId('zone'),
|
||||
object: z.literal('zone').default('zone'),
|
||||
levelId: LevelNode.shape.id, // Required - must be attached to a level
|
||||
name: z.string(),
|
||||
// Polygon boundary - array of [x, z] coordinates defining the zone
|
||||
polygon: ZonePolygon,
|
||||
// Visual styling
|
||||
color: z.string().default('#3b82f6'), // Default blue
|
||||
metadata: z.json().optional().default({}),
|
||||
})
|
||||
.describe(
|
||||
dedent`
|
||||
Zone schema - a polygon zone attached to a level
|
||||
- object: "zone"
|
||||
- id: zone id
|
||||
- levelId: level this zone is attached to
|
||||
- name: zone name
|
||||
- polygon: array of [x, z] points defining the zone boundary
|
||||
- color: hex color for visual styling
|
||||
- metadata: zone metadata (optional)
|
||||
`,
|
||||
)
|
||||
|
||||
export type Zone = z.infer<typeof ZoneSchema>
|
||||
export type ZonePolygon = z.infer<typeof ZonePolygon>
|
||||
@@ -1,60 +0,0 @@
|
||||
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 }
|
||||
})
|
||||
}
|
||||
@@ -3,21 +3,18 @@
|
||||
import { temporal } from 'zundo'
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { BuildingNode, type Zone } from '../schema'
|
||||
import { BuildingNode } from '../schema'
|
||||
import { LevelNode } from '../schema/nodes/level'
|
||||
import type { AnyNode, AnyNodeId } from '../schema/types'
|
||||
import { isObject } from '../utils/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<AnyNodeId, AnyNode>
|
||||
zones: Record<Zone['id'], Zone>
|
||||
|
||||
// 2. The Root: Which nodes are at the top level?
|
||||
rootNodeIds: AnyNodeId[]
|
||||
zoneIds: Zone['id'][]
|
||||
|
||||
// 3. The "Dirty" Set: For the Wall/Physics systems
|
||||
dirtyNodes: Set<AnyNodeId>
|
||||
@@ -37,14 +34,6 @@ export type SceneState = {
|
||||
|
||||
deleteNode: (id: 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'>;
|
||||
@@ -55,11 +44,9 @@ const useScene = create<SceneState>()(
|
||||
(set, get) => ({
|
||||
// 1. Flat dictionary of all nodes
|
||||
nodes: {},
|
||||
zones: {},
|
||||
|
||||
// 2. Root node IDs
|
||||
rootNodeIds: [],
|
||||
zoneIds: [],
|
||||
|
||||
// 3. Dirty set
|
||||
dirtyNodes: new Set<AnyNodeId>(),
|
||||
@@ -68,8 +55,6 @@ const useScene = create<SceneState>()(
|
||||
set({
|
||||
nodes: {},
|
||||
rootNodeIds: [],
|
||||
zones: {},
|
||||
zoneIds: [],
|
||||
dirtyNodes: new Set<AnyNodeId>(),
|
||||
})
|
||||
get().loadScene() // Default scene
|
||||
@@ -127,17 +112,6 @@ const useScene = create<SceneState>()(
|
||||
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) => {
|
||||
@@ -159,11 +133,8 @@ const useScene = create<SceneState>()(
|
||||
}),
|
||||
),
|
||||
rootNodeIds: state.rootNodeIds,
|
||||
zones: state.zones,
|
||||
zoneIds: state.zoneIds,
|
||||
}),
|
||||
onRehydrateStorage: (state) => {
|
||||
|
||||
console.log('hydrating...')
|
||||
|
||||
// optional
|
||||
|
||||
Reference in New Issue
Block a user