Move to global undo and combined map action state
This commit is contained in:
parent
b703a08d2c
commit
54bc3502df
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -25,3 +25,5 @@ export type NoteDraggingOptions = {
|
||||
noteId: string;
|
||||
noteGroup: Konva.Node;
|
||||
};
|
||||
|
||||
export type Notes = Record<string, Note>;
|
||||
|
@ -33,3 +33,5 @@ export type FileTokenState = BaseTokenState & {
|
||||
};
|
||||
|
||||
export type TokenState = DefaultTokenState | FileTokenState;
|
||||
|
||||
export type TokenStates = Record<string, TokenState>;
|
||||
|
Loading…
Reference in New Issue
Block a user