import { clampSceneTokenSizeN, type SceneTokensSessionEvent, type SceneTokensSessionPlacement, type SceneTokensSessionState, } from '../../shared/types'; import { rotateMapNormPointByCwSteps } from '../../shared/types/scenePreviewRotation'; function emptyState(): SceneTokensSessionState { return { revision: 1, byPlacementId: {}, }; } function withMove( prev: SceneTokensSessionPlacement | undefined, nx: number, ny: number, ): SceneTokensSessionPlacement { return { nx, ny, ...(prev?.sizeN !== undefined ? { sizeN: prev.sizeN } : {}), }; } export class SceneTokensSessionStore { private state: SceneTokensSessionState = emptyState(); getState(): SceneTokensSessionState { return this.state; } reset(): SceneTokensSessionState { if (Object.keys(this.state.byPlacementId).length === 0) return this.state; this.state = { revision: this.state.revision + 1, byPlacementId: {}, }; return this.state; } /** Rotate session position overrides with the scene preview (CSS rotate steps). */ rotateMapCwSteps(steps: number): SceneTokensSessionState { const n = ((steps % 4) + 4) % 4; if (n === 0) return this.state; const ids = Object.keys(this.state.byPlacementId); if (ids.length === 0) return this.state; const byPlacementId: SceneTokensSessionState['byPlacementId'] = {}; for (const id of ids) { const prev = this.state.byPlacementId[id]; if (!prev) continue; if (typeof prev.nx === 'number' && typeof prev.ny === 'number') { const next = rotateMapNormPointByCwSteps(prev.nx, prev.ny, n); byPlacementId[id] = { ...prev, nx: next.nx, ny: next.ny }; } else { byPlacementId[id] = prev; } } this.state = { revision: this.state.revision + 1, byPlacementId, }; return this.state; } dispatch(event: SceneTokensSessionEvent): SceneTokensSessionState { switch (event.kind) { case 'clear': return this.reset(); case 'move': { const placementId = String(event.placementId ?? ''); if (!placementId) return this.state; const nx = Math.max(0, Math.min(1, event.nx)); const ny = Math.max(0, Math.min(1, event.ny)); if (!Number.isFinite(nx) || !Number.isFinite(ny)) return this.state; const prev = this.state.byPlacementId[placementId]; if (prev && prev.nx === nx && prev.ny === ny) return this.state; this.state = { revision: this.state.revision + 1, byPlacementId: { ...this.state.byPlacementId, [placementId]: withMove(prev, nx, ny), }, }; return this.state; } case 'setSize': { const placementId = String(event.placementId ?? ''); if (!placementId) return this.state; const sizeN = clampSceneTokenSizeN(event.sizeN); const prev = this.state.byPlacementId[placementId]; if (prev?.sizeN === sizeN) return this.state; this.state = { revision: this.state.revision + 1, byPlacementId: { ...this.state.byPlacementId, [placementId]: { ...(typeof prev?.nx === 'number' ? { nx: prev.nx } : {}), ...(typeof prev?.ny === 'number' ? { ny: prev.ny } : {}), sizeN, }, }, }; return this.state; } default: { const _exhaustive: never = event; void _exhaustive; return this.state; } } } }