import { Dispatch, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { FloorplanAction, FloorplanState, EntryDir, ThicknessLevel } from '../state/types'; import { initialState, reducer } from '../state/reducer'; export type ServerFloorSettings = { tilemap: string; entryPoint: [number, number]; entryPointDir: number; thicknessWall: ThicknessLevel; thicknessFloor: ThicknessLevel; wallHeight: number; }; type Api = { state: FloorplanState; dispatch: Dispatch; loadFromServer: (s: ServerFloorSettings) => void; undo: () => void; redo: () => void; canUndo: boolean; canRedo: boolean; }; const isNonHistoryAction = (action: FloorplanAction): boolean => { switch(action.type) { case 'BRUSH_SET': case 'SELECT_ALL': case 'CLEAR_SELECTION': case 'SELECT_RECT': case 'SQUARE_SELECT_TOGGLE': return true; default: return false; } }; const isRemoteAction = (action: FloorplanAction): boolean => { if(action.type === 'APPLY_REMOTE_DIFF' || action.type === 'APPLY_REMOTE_SNAPSHOT') return true; return 'source' in action && action.source === 'remote'; }; const HISTORY_LIMIT = 100; export const useFloorplanReducer = (): Api => { const [ state, dispatch ] = useReducer(reducer, initialState); const pastRef = useRef([]); const futureRef = useRef([]); const [ canUndo, setCanUndo ] = useState(false); const [ canRedo, setCanRedo ] = useState(false); const stateRef = useRef(state); useEffect(() => { stateRef.current = state; }, [ state ]); const refreshCanFlags = useCallback(() => { setCanUndo(pastRef.current.length > 0); setCanRedo(futureRef.current.length > 0); }, []); const wrappedDispatch = useCallback>((action) => { if(isNonHistoryAction(action) || isRemoteAction(action)) { dispatch(action); return; } pastRef.current.push(stateRef.current); if(pastRef.current.length > HISTORY_LIMIT) pastRef.current.shift(); futureRef.current = []; dispatch(action); refreshCanFlags(); }, [ refreshCanFlags ]); const loadFromServer = useCallback((s: ServerFloorSettings) => { pastRef.current = []; futureRef.current = []; dispatch({ type: 'IMPORT_STRING', raw: s.tilemap, door: { x: s.entryPoint[0], y: s.entryPoint[1], dir: ((s.entryPointDir | 0) & 7) as EntryDir }, thickness: { wall: s.thicknessWall, floor: s.thicknessFloor }, wallHeight: s.wallHeight, source: 'remote' }); refreshCanFlags(); }, [ refreshCanFlags ]); const undo = useCallback(() => { const previous = pastRef.current.pop(); if(!previous) return; futureRef.current.push(stateRef.current); dispatch({ type: 'APPLY_REMOTE_SNAPSHOT', raw: serializeTilesForSnapshot(previous.tiles), door: previous.door, thickness: previous.thickness, wallHeight: previous.wallHeight, seq: previous.seq }); refreshCanFlags(); }, [ refreshCanFlags ]); const redo = useCallback(() => { const next = futureRef.current.pop(); if(!next) return; pastRef.current.push(stateRef.current); dispatch({ type: 'APPLY_REMOTE_SNAPSHOT', raw: serializeTilesForSnapshot(next.tiles), door: next.door, thickness: next.thickness, wallHeight: next.wallHeight, seq: next.seq }); refreshCanFlags(); }, [ refreshCanFlags ]); return useMemo(() => ({ state, dispatch: wrappedDispatch, loadFromServer, undo, redo, canUndo, canRedo }), [ state, wrappedDispatch, loadFromServer, undo, redo, canUndo, canRedo ]); }; const serializeTilesForSnapshot = (tiles: { h: number; blocked: boolean }[][]): string => { if(!tiles || tiles.length === 0) return ''; const scheme = 'x0123456789abcdefghijklmnopq'; return tiles.map(row => row.map(tile => { if(tile.blocked) return 'x'; const h = Number.isFinite(tile.h) ? Math.max(0, Math.min(scheme.length - 2, tile.h)) : 0; return scheme.charAt(h + 1); }).join('')).join('\r'); };