mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
149 lines
4.4 KiB
TypeScript
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');
|
|
};
|