110 lines
2.9 KiB
TypeScript
110 lines
2.9 KiB
TypeScript
'use client'
|
|
|
|
import { create } from 'zustand'
|
|
import type { Interactive } from '../schema/nodes/item'
|
|
import type { AnyNodeId } from '../schema/types'
|
|
|
|
// Runtime value for each control (matches discriminated union kinds)
|
|
export type ControlValue = boolean | number
|
|
|
|
export type ItemInteractiveState = {
|
|
// Indexed by control position in asset.interactive.controls[]
|
|
controlValues: ControlValue[]
|
|
}
|
|
|
|
export type DoorInteractiveState = {
|
|
operationState?: number
|
|
swingAngle?: number
|
|
}
|
|
|
|
type InteractiveStore = {
|
|
items: Record<AnyNodeId, ItemInteractiveState>
|
|
doors: Record<AnyNodeId, DoorInteractiveState>
|
|
|
|
/** Initialize a node's interactive state from its asset definition (idempotent) */
|
|
initItem: (itemId: AnyNodeId, interactive: Interactive) => void
|
|
|
|
/** Set a single control value */
|
|
setControlValue: (itemId: AnyNodeId, index: number, value: ControlValue) => void
|
|
|
|
/** Remove a node's state (e.g. on unmount) */
|
|
removeItem: (itemId: AnyNodeId) => void
|
|
|
|
/** Set transient door open state without committing it to the scene node */
|
|
setDoorOpenState: (doorId: AnyNodeId, value: DoorInteractiveState) => void
|
|
|
|
/** Clear transient door open state */
|
|
removeDoorOpenState: (doorId: AnyNodeId) => void
|
|
}
|
|
|
|
const defaultControlValue = (interactive: Interactive, index: number): ControlValue => {
|
|
const control = interactive.controls[index]
|
|
if (!control) return false
|
|
switch (control.kind) {
|
|
case 'toggle':
|
|
return control.default ?? false
|
|
case 'slider':
|
|
return control.default ?? control.min
|
|
case 'temperature':
|
|
return control.default ?? control.min
|
|
}
|
|
}
|
|
|
|
export const useInteractive = create<InteractiveStore>((set, get) => ({
|
|
items: {},
|
|
doors: {},
|
|
|
|
initItem: (itemId, interactive) => {
|
|
const { controls } = interactive
|
|
if (controls.length === 0) return
|
|
|
|
// Don't overwrite existing state (idempotent)
|
|
if (get().items[itemId]) return
|
|
|
|
set((state) => ({
|
|
items: {
|
|
...state.items,
|
|
[itemId]: {
|
|
controlValues: controls.map((_, i) => defaultControlValue(interactive, i)),
|
|
},
|
|
},
|
|
}))
|
|
},
|
|
|
|
setControlValue: (itemId, index, value) => {
|
|
set((state) => {
|
|
const item = state.items[itemId]
|
|
if (!item) return state
|
|
const next = [...item.controlValues]
|
|
next[index] = value
|
|
return { items: { ...state.items, [itemId]: { controlValues: next } } }
|
|
})
|
|
},
|
|
|
|
removeItem: (itemId) => {
|
|
set((state) => {
|
|
const { [itemId]: _, ...rest } = state.items
|
|
return { items: rest }
|
|
})
|
|
},
|
|
|
|
setDoorOpenState: (doorId, value) => {
|
|
set((state) => ({
|
|
doors: {
|
|
...state.doors,
|
|
[doorId]: {
|
|
...state.doors[doorId],
|
|
...value,
|
|
},
|
|
},
|
|
}))
|
|
},
|
|
|
|
removeDoorOpenState: (doorId) => {
|
|
set((state) => {
|
|
const { [doorId]: _, ...rest } = state.doors
|
|
return { doors: rest }
|
|
})
|
|
},
|
|
}))
|