Files
Nitro-V3/src/components/floorplan-editor/hooks/useFloorplanReducer.ts
T
2026-05-26 16:38:01 +02:00

149 lines
4.4 KiB
TypeScript

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<FloorplanAction>;
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<FloorplanState[]>([]);
const futureRef = useRef<FloorplanState[]>([]);
const [ canUndo, setCanUndo ] = useState(false);
const [ canRedo, setCanRedo ] = useState(false);
const stateRef = useRef<FloorplanState>(state);
useEffect(() =>
{
stateRef.current = state;
}, [ state ]);
const refreshCanFlags = useCallback(() =>
{
setCanUndo(pastRef.current.length > 0);
setCanRedo(futureRef.current.length > 0);
}, []);
const wrappedDispatch = useCallback<Dispatch<FloorplanAction>>((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');
};