diff --git a/src/components/controls/DrawingToolSettings.tsx b/src/components/controls/DrawingToolSettings.tsx index b9cd167..0ac1dff 100644 --- a/src/components/controls/DrawingToolSettings.tsx +++ b/src/components/controls/DrawingToolSettings.tsx @@ -17,9 +17,6 @@ import BrushTriangleIcon from "../../icons/BrushTriangleIcon"; import EraseAllIcon from "../../icons/EraseAllIcon"; import EraseIcon from "../../icons/EraseToolIcon"; -import UndoButton from "./shared/UndoButton"; -import RedoButton from "./shared/RedoButton"; - import Divider from "../Divider"; import { useKeyboard } from "../../contexts/KeyboardContext"; @@ -62,10 +59,6 @@ function DrawingToolSettings({ onSettingChange({ type: "erase" }); } else if (shortcuts.drawBlend(event)) { onSettingChange({ useBlending: !settings.useBlending }); - } else if (shortcuts.redo(event) && !disabledActions.includes("redo")) { - onToolAction("mapRedo"); - } else if (shortcuts.undo(event) && !disabledActions.includes("undo")) { - onToolAction("mapUndo"); } } useKeyboard(handleKeyDown); @@ -155,15 +148,6 @@ function DrawingToolSettings({ useBlending={settings.useBlending} onBlendingChange={(useBlending) => onSettingChange({ useBlending })} /> - - onToolAction("mapUndo")} - disabled={disabledActions.includes("undo")} - /> - onToolAction("mapRedo")} - disabled={disabledActions.includes("redo")} - /> ); } diff --git a/src/components/controls/FogToolSettings.tsx b/src/components/controls/FogToolSettings.tsx index f227d1f..79d4cc9 100644 --- a/src/components/controls/FogToolSettings.tsx +++ b/src/components/controls/FogToolSettings.tsx @@ -13,8 +13,6 @@ import FogRemoveIcon from "../../icons/FogRemoveIcon"; import FogToggleIcon from "../../icons/FogToggleIcon"; import FogRectangleIcon from "../../icons/FogRectangleIcon"; -import UndoButton from "./shared/UndoButton"; -import RedoButton from "./shared/RedoButton"; import ToolSection from "./shared/ToolSection"; import Divider from "../Divider"; @@ -31,16 +29,9 @@ import { type FogToolSettingsProps = { settings: FogToolSettingsType; onSettingChange: (change: Partial) => void; - onToolAction: (action: string) => void; - disabledActions: string[]; }; -function FogToolSettings({ - settings, - onSettingChange, - onToolAction, - disabledActions, -}: FogToolSettingsProps) { +function FogToolSettings({ settings, onSettingChange }: FogToolSettingsProps) { // Keyboard shortcuts function handleKeyDown(event: KeyboardEvent) { if (shortcuts.fogPolygon(event)) { @@ -59,10 +50,6 @@ function FogToolSettings({ onSettingChange({ useFogCut: !settings.useFogCut }); } else if (shortcuts.fogRectangle(event)) { onSettingChange({ type: "rectangle" }); - } else if (shortcuts.redo(event) && !disabledActions.includes("redo")) { - onToolAction("fogRedo"); - } else if (shortcuts.undo(event) && !disabledActions.includes("undo")) { - onToolAction("fogUndo"); } } @@ -134,15 +121,6 @@ function FogToolSettings({ useFogPreview={settings.preview} onFogPreviewChange={(preview) => onSettingChange({ preview })} /> - - onToolAction("fogUndo")} - disabled={disabledActions.includes("undo")} - /> - onToolAction("fogRedo")} - disabled={disabledActions.includes("redo")} - /> ); } diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index 25d1bb6..eb5f684 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -26,7 +26,7 @@ import Session from "../../network/Session"; import { Drawing, DrawingState } from "../../types/Drawing"; import { Fog, FogState } from "../../types/Fog"; -import { Map as MapType, MapActions, MapToolId } from "../../types/Map"; +import { Map as MapType, MapToolId } from "../../types/Map"; import { MapState } from "../../types/MapState"; import { Settings } from "../../types/Settings"; import { @@ -45,17 +45,12 @@ import useMapNotes from "../../hooks/useMapNotes"; type MapProps = { map: MapType | null; mapState: MapState | null; - mapActions: MapActions; onMapTokenStateChange: TokenStateChangeEventHandler; onMapTokenStateRemove: TokenStateRemoveHandler; onMapChange: MapChangeEventHandler; onMapReset: MapResetEventHandler; onMapDraw: (action: Action) => void; - onMapDrawUndo: () => void; - onMapDrawRedo: () => void; onFogDraw: (action: Action) => void; - onFogDrawUndo: () => void; - onFogDrawRedo: () => void; onMapNoteCreate: NoteCreateEventHander; onMapNoteChange: NoteChangeEventHandler; onMapNoteRemove: NoteRemoveEventHander; @@ -65,22 +60,19 @@ type MapProps = { allowNoteEditing: boolean; disabledTokens: Record; session: Session; + onUndo: () => void; + onRedo: () => void; }; function Map({ map, mapState, - mapActions, onMapTokenStateChange, onMapTokenStateRemove, onMapChange, onMapReset, onMapDraw, - onMapDrawUndo, - onMapDrawRedo, onFogDraw, - onFogDrawUndo, - onFogDrawRedo, onMapNoteCreate, onMapNoteChange, onMapNoteRemove, @@ -90,6 +82,8 @@ function Map({ allowNoteEditing, disabledTokens, session, + onUndo, + onRedo, }: MapProps) { const { addToast } = useToasts(); @@ -110,18 +104,6 @@ function Map({ if (action === "eraseAll") { onMapDraw(new RemoveStatesAction(drawShapes.map((s) => s.id))); } - if (action === "mapUndo") { - onMapDrawUndo(); - } - if (action === "mapRedo") { - onMapDrawRedo(); - } - if (action === "fogUndo") { - onFogDrawUndo(); - } - if (action === "fogRedo") { - onFogDrawRedo(); - } } function handleMapShapeAdd(shape: Drawing) { @@ -169,33 +151,13 @@ function Map({ } const disabledSettings: { - fog: string[]; drawing: string[]; } = { - fog: [], drawing: [], }; if (drawShapes.length === 0) { disabledSettings.drawing.push("erase"); } - if (!mapState || mapActions.mapDrawActionIndex < 0) { - disabledSettings.drawing.push("undo"); - } - if ( - !mapState || - mapActions.mapDrawActionIndex === mapActions.mapDrawActions.length - 1 - ) { - disabledSettings.drawing.push("redo"); - } - if (!mapState || mapActions.fogDrawActionIndex < 0) { - disabledSettings.fog.push("undo"); - } - if ( - !mapState || - mapActions.fogDrawActionIndex === mapActions.fogDrawActions.length - 1 - ) { - disabledSettings.fog.push("redo"); - } const { tokens, tokenMenu, tokenDragOverlay } = useMapTokens( map, @@ -235,6 +197,8 @@ function Map({ onToolAction={handleToolAction} disabledControls={disabledControls} disabledSettings={disabledSettings} + onUndo={onUndo} + onRedo={onRedo} /> {tokenMenu} {noteMenu} diff --git a/src/components/map/MapControls.tsx b/src/components/map/MapControls.tsx index 5e88a9e..68dc783 100644 --- a/src/components/map/MapControls.tsx +++ b/src/components/map/MapControls.tsx @@ -22,6 +22,9 @@ import FullScreenExitIcon from "../../icons/FullScreenExitIcon"; import NoteToolIcon from "../../icons/NoteToolIcon"; import SelectToolIcon from "../../icons/SelecToolIcon"; +import UndoButton from "../controls/shared/UndoButton"; +import RedoButton from "../controls/shared/RedoButton"; + import useSetting from "../../hooks/useSetting"; import { Map, MapTool, MapToolId } from "../../types/Map"; @@ -48,6 +51,8 @@ type MapControlsProps = { onToolAction: (actionId: string) => void; disabledControls: MapToolId[]; disabledSettings: Partial>; + onUndo: () => void; + onRedo: () => void; }; function MapContols({ @@ -62,6 +67,8 @@ function MapContols({ onToolAction, disabledControls, disabledSettings, + onUndo, + onRedo, }: MapControlsProps) { const [isExpanded, setIsExpanded] = useState(true); const [fullScreen, setFullScreen] = useSetting("map.fullScreen"); @@ -144,6 +151,15 @@ function MapContols({ )), }, + { + id: "history", + component: ( + <> + + + + ), + }, ]; let controls = null; diff --git a/src/network/NetworkedMapAndTokens.tsx b/src/network/NetworkedMapAndTokens.tsx index b0e0ddb..57c06e0 100644 --- a/src/network/NetworkedMapAndTokens.tsx +++ b/src/network/NetworkedMapAndTokens.tsx @@ -23,12 +23,7 @@ import TokenBar from "../components/token/TokenBar"; import GlobalImageDrop from "../components/image/GlobalImageDrop"; -import { - Map as MapType, - MapActions, - MapActionsIndexKey, - MapActionsKey, -} from "../types/Map"; +import { Map as MapType, MapActions, MapAction } from "../types/Map"; import { MapState } from "../types/MapState"; import { AssetManifest, @@ -41,10 +36,8 @@ import { FogState } from "../types/Fog"; import { Note } from "../types/Note"; const defaultMapActions: MapActions = { - mapDrawActions: [], - mapDrawActionIndex: -1, - fogDrawActions: [], - fogDrawActionIndex: -1, + actions: [], + actionIndex: -1, }; /** @@ -233,118 +226,115 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { const [mapActions, setMapActions] = useState(defaultMapActions); - function addMapActions( - actions: Action[], - indexKey: MapActionsIndexKey, - actionsKey: MapActionsKey, - shapesKey: "drawShapes" | "fogShapes" - ) { - setMapActions((prevMapActions) => { + function applyMapActionsToState( + mapState: MapState, + actions: MapAction[] + ): MapState { + for (let mapAction of actions) { + if (mapAction.type === "drawings") { + mapState.drawShapes = mapAction.action.execute(mapState.drawShapes); + } else if (mapAction.type === "fogs") { + mapState.fogShapes = mapAction.action.execute(mapState.fogShapes); + } else if (mapAction.type === "tokens") { + mapState.tokens = mapAction.action.execute(mapState.tokens); + } else if (mapAction.type === "notes") { + mapState.notes = mapAction.action.execute(mapState.notes); + } + } + return mapState; + } + + function undoMapActionsToState( + mapState: MapState, + actions: MapAction[] + ): MapState { + for (let mapAction of actions) { + if (mapAction.type === "drawings") { + mapState.drawShapes = mapAction.action.undo(mapState.drawShapes); + } else if (mapAction.type === "fogs") { + mapState.fogShapes = mapAction.action.undo(mapState.fogShapes); + } else if (mapAction.type === "tokens") { + mapState.tokens = mapAction.action.undo(mapState.tokens); + } else if (mapAction.type === "notes") { + mapState.notes = mapAction.action.undo(mapState.notes); + } + } + return mapState; + } + + function addActions(actions: MapAction[]) { + setMapActions((prevActions) => { const newActions = [ - ...prevMapActions[actionsKey].slice(0, prevMapActions[indexKey] + 1), - ...actions, + ...prevActions.actions.slice(0, prevActions.actionIndex + 1), + actions, ]; const newIndex = newActions.length - 1; return { - ...prevMapActions, - [actionsKey]: newActions, - [indexKey]: newIndex, + actions: newActions, + actionIndex: newIndex, }; }); + // Update map state by performing the actions on it setCurrentMapState((prevMapState) => { if (!prevMapState) { return prevMapState; } - let shapes = prevMapState[shapesKey]; - for (let action of actions) { - shapes = action.execute(shapes); - } - return { - ...prevMapState, - [shapesKey]: shapes, - }; + let state = { ...prevMapState }; + state = applyMapActionsToState(state, actions); + return state; }); } - function updateActionIndex( - change: number, - indexKey: MapActionsIndexKey, - actionsKey: MapActionsKey, - shapesKey: "drawShapes" | "fogShapes" - ) { - const prevIndex = mapActions[indexKey]; + function updateActionIndex(change: number) { + const prevIndex = mapActions.actionIndex; const newIndex = Math.min( - Math.max(mapActions[indexKey] + change, -1), - mapActions[actionsKey].length - 1 + Math.max(mapActions.actionIndex + change, -1), + mapActions.actions.length - 1 ); setMapActions((prevMapActions) => ({ ...prevMapActions, - [indexKey]: newIndex, + actionIndex: newIndex, })); // Update map state by either performing the actions or undoing them setCurrentMapState((prevMapState) => { - if (prevMapState) { - let shapes = prevMapState[shapesKey]; - if (prevIndex < newIndex) { - // Redo - for (let i = prevIndex + 1; i < newIndex + 1; i++) { - let action = mapActions[actionsKey][i]; - shapes = action.execute(shapes as any); - } - } else { - // Undo - for (let i = prevIndex; i > newIndex; i--) { - let action = mapActions[actionsKey][i]; - shapes = action.undo(shapes as any); - } - } - return { - ...prevMapState, - [shapesKey]: shapes, - }; - } else { + if (!prevMapState) { return prevMapState; } + let state = { ...prevMapState }; + if (prevIndex < newIndex) { + // Redo + for (let i = prevIndex + 1; i < newIndex + 1; i++) { + const actions = mapActions.actions[i]; + state = applyMapActionsToState(state, actions); + } + } else { + // Undo + for (let i = prevIndex; i > newIndex; i--) { + const actions = mapActions.actions[i]; + state = undoMapActionsToState(state, actions); + } + } + return state; }); - - return newIndex; } function handleMapDraw(action: Action) { - addMapActions( - [action], - "mapDrawActionIndex", - "mapDrawActions", - "drawShapes" - ); - } - - function handleMapDrawUndo() { - updateActionIndex(-1, "mapDrawActionIndex", "mapDrawActions", "drawShapes"); - } - - function handleMapDrawRedo() { - updateActionIndex(1, "mapDrawActionIndex", "mapDrawActions", "drawShapes"); + addActions([{ type: "drawings", action }]); } function handleFogDraw(action: Action) { - addMapActions( - [action], - "fogDrawActionIndex", - "fogDrawActions", - "fogShapes" - ); + addActions([{ type: "fogs", action }]); } - function handleFogDrawUndo() { - updateActionIndex(-1, "fogDrawActionIndex", "fogDrawActions", "fogShapes"); + function handleUndo() { + updateActionIndex(-1); } - function handleFogDrawRedo() { - updateActionIndex(1, "fogDrawActionIndex", "fogDrawActions", "fogShapes"); + function handleRedo() { + updateActionIndex(1); } // If map changes clear map actions @@ -562,17 +552,12 @@ function NetworkedMapAndTokens({ session }: { session: Session }) { diff --git a/src/types/Map.ts b/src/types/Map.ts index 73ed57e..9b0714c 100644 --- a/src/types/Map.ts +++ b/src/types/Map.ts @@ -3,6 +3,8 @@ import Action from "../actions/Action"; import { DrawingState } from "./Drawing"; import { FogState } from "./Fog"; import { Grid } from "./Grid"; +import { Notes } from "./Note"; +import { TokenStates } from "./TokenState"; export type MapToolId = | "map" @@ -59,18 +61,21 @@ export type FileMap = BaseMap & { export type Map = DefaultMap | FileMap; -export type MapActions = { - mapDrawActions: Action[]; - mapDrawActionIndex: number; - fogDrawActions: Action[]; - fogDrawActionIndex: number; +export type DrawingsAction = { + type: "drawings"; + action: Action; }; +export type FogsAction = { type: "fogs"; action: Action }; +export type TokensAction = { type: "tokens"; action: Action }; +export type NotesAction = { type: "notes"; action: Action }; -export type MapActionsKey = keyof Pick< - MapActions, - "mapDrawActions" | "fogDrawActions" ->; -export type MapActionsIndexKey = keyof Pick< - MapActions, - "mapDrawActionIndex" | "fogDrawActionIndex" ->; +export type MapAction = + | DrawingsAction + | FogsAction + | TokensAction + | NotesAction; + +export type MapActions = { + actions: MapAction[][]; + actionIndex: number; +}; diff --git a/src/types/MapState.ts b/src/types/MapState.ts index feae906..0caeed8 100644 --- a/src/types/MapState.ts +++ b/src/types/MapState.ts @@ -1,15 +1,15 @@ import { DrawingState } from "./Drawing"; import { FogState } from "./Fog"; -import { Note } from "./Note"; -import { TokenState } from "./TokenState"; +import { Notes } from "./Note"; +import { TokenStates } from "./TokenState"; export type EditFlag = "drawing" | "tokens" | "notes" | "fog"; export type MapState = { - tokens: Record; + tokens: TokenStates; drawShapes: DrawingState; fogShapes: FogState; editFlags: Array; - notes: Record; + notes: Notes; mapId: string; }; diff --git a/src/types/Note.ts b/src/types/Note.ts index 2af949b..923509d 100644 --- a/src/types/Note.ts +++ b/src/types/Note.ts @@ -25,3 +25,5 @@ export type NoteDraggingOptions = { noteId: string; noteGroup: Konva.Node; }; + +export type Notes = Record; diff --git a/src/types/TokenState.ts b/src/types/TokenState.ts index 2e8aded..7432685 100644 --- a/src/types/TokenState.ts +++ b/src/types/TokenState.ts @@ -33,3 +33,5 @@ export type FileTokenState = BaseTokenState & { }; export type TokenState = DefaultTokenState | FileTokenState; + +export type TokenStates = Record;