Move to global undo and combined map action state

This commit is contained in:
Mitchell McCaffrey 2021-07-20 21:45:18 +10:00
parent b703a08d2c
commit 54bc3502df
9 changed files with 128 additions and 190 deletions

View File

@ -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 })}
/>
<Divider vertical />
<UndoButton
onClick={() => onToolAction("mapUndo")}
disabled={disabledActions.includes("undo")}
/>
<RedoButton
onClick={() => onToolAction("mapRedo")}
disabled={disabledActions.includes("redo")}
/>
</Flex>
);
}

View File

@ -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<FogToolSettingsType>) => 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 })}
/>
<Divider vertical />
<UndoButton
onClick={() => onToolAction("fogUndo")}
disabled={disabledActions.includes("undo")}
/>
<RedoButton
onClick={() => onToolAction("fogRedo")}
disabled={disabledActions.includes("redo")}
/>
</Flex>
);
}

View File

@ -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<DrawingState>) => void;
onMapDrawUndo: () => void;
onMapDrawRedo: () => void;
onFogDraw: (action: Action<FogState>) => void;
onFogDrawUndo: () => void;
onFogDrawRedo: () => void;
onMapNoteCreate: NoteCreateEventHander;
onMapNoteChange: NoteChangeEventHandler;
onMapNoteRemove: NoteRemoveEventHander;
@ -65,22 +60,19 @@ type MapProps = {
allowNoteEditing: boolean;
disabledTokens: Record<string, boolean>;
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}

View File

@ -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<Record<keyof Settings, string[]>>;
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({
</RadioIconButton>
)),
},
{
id: "history",
component: (
<>
<UndoButton onClick={onUndo} />
<RedoButton onClick={onRedo} />
</>
),
},
];
let controls = null;

View File

@ -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<DrawingState | FogState>[],
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<DrawingState>) {
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<FogState>) {
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 }) {
<Map
map={currentMap}
mapState={currentMapState}
mapActions={mapActions}
onMapTokenStateChange={handleMapTokenStateChange}
onMapTokenStateRemove={handleMapTokenStateRemove}
onMapChange={handleMapChange}
onMapReset={handleMapReset}
onMapDraw={handleMapDraw}
onMapDrawUndo={handleMapDrawUndo}
onMapDrawRedo={handleMapDrawRedo}
onFogDraw={handleFogDraw}
onFogDrawUndo={handleFogDrawUndo}
onFogDrawRedo={handleFogDrawRedo}
onMapNoteCreate={handleNoteCreate}
onMapNoteChange={handleNoteChange}
onMapNoteRemove={handleNoteRemove}
@ -582,6 +567,8 @@ function NetworkedMapAndTokens({ session }: { session: Session }) {
allowNoteEditing={!!canEditNotes}
disabledTokens={disabledMapTokens}
session={session}
onUndo={handleUndo}
onRedo={handleRedo}
/>
<TokenBar onMapTokensStateCreate={handleMapTokensStateCreate} />
</GlobalImageDrop>

View File

@ -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<DrawingState>[];
mapDrawActionIndex: number;
fogDrawActions: Action<FogState>[];
fogDrawActionIndex: number;
export type DrawingsAction = {
type: "drawings";
action: Action<DrawingState>;
};
export type FogsAction = { type: "fogs"; action: Action<FogState> };
export type TokensAction = { type: "tokens"; action: Action<TokenStates> };
export type NotesAction = { type: "notes"; action: Action<Notes> };
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;
};

View File

@ -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<string, TokenState>;
tokens: TokenStates;
drawShapes: DrawingState;
fogShapes: FogState;
editFlags: Array<EditFlag>;
notes: Record<string, Note>;
notes: Notes;
mapId: string;
};

View File

@ -25,3 +25,5 @@ export type NoteDraggingOptions = {
noteId: string;
noteGroup: Konva.Node;
};
export type Notes = Record<string, Note>;

View File

@ -33,3 +33,5 @@ export type FileTokenState = BaseTokenState & {
};
export type TokenState = DefaultTokenState | FileTokenState;
export type TokenStates = Record<string, TokenState>;