import { useState } from "react"; import { Box } from "theme-ui"; import { useToasts } from "react-toast-notifications"; import MapControls from "./MapControls"; import MapInteraction from "./MapInteraction"; import MapTokens from "./MapTokens"; import MapDrawing from "./MapDrawing"; import MapFog from "./MapFog"; import MapGrid from "./MapGrid"; import MapMeasure from "./MapMeasure"; import NetworkedMapPointer from "../../network/NetworkedMapPointer"; import MapNotes from "./MapNotes"; import { useSettings } from "../../contexts/SettingsContext"; import TokenMenu from "../token/TokenMenu"; import TokenDragOverlay from "../token/TokenDragOverlay"; import NoteMenu from "../note/NoteMenu"; import NoteDragOverlay from "../note/NoteDragOverlay"; import { AddStatesAction, CutFogAction, EditStatesAction, RemoveStatesAction, } from "../../actions"; 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 { MapState } from "../../types/MapState"; import { Settings } from "../../types/Settings"; import { MapChangeEventHandler, MapResetEventHandler, MapTokenStateRemoveHandler, NoteChangeEventHandler, NoteRemoveEventHander, TokenStateChangeEventHandler, } from "../../types/Events"; import Action from "../../actions/Action"; import Konva from "konva"; import { TokenDraggingOptions, TokenMenuOptions } from "../../types/Token"; import { Note, NoteDraggingOptions, NoteMenuOptions } from "../../types/Note"; import MapSelect from "./MapSelect"; type MapProps = { map: MapType | null; mapState: MapState | null; mapActions: MapActions; onMapTokenStateChange: TokenStateChangeEventHandler; onMapTokenStateRemove: MapTokenStateRemoveHandler; onMapChange: MapChangeEventHandler; onMapReset: MapResetEventHandler; onMapDraw: (action: Action) => void; onMapDrawUndo: () => void; onMapDrawRedo: () => void; onFogDraw: (action: Action) => void; onFogDrawUndo: () => void; onFogDrawRedo: () => void; onMapNoteChange: NoteChangeEventHandler; onMapNoteRemove: NoteRemoveEventHander; allowMapDrawing: boolean; allowFogDrawing: boolean; allowMapChange: boolean; allowNoteEditing: boolean; disabledTokens: Record; session: Session; }; function Map({ map, mapState, mapActions, onMapTokenStateChange, onMapTokenStateRemove, onMapChange, onMapReset, onMapDraw, onMapDrawUndo, onMapDrawRedo, onFogDraw, onFogDrawUndo, onFogDrawRedo, onMapNoteChange, onMapNoteRemove, allowMapDrawing, allowFogDrawing, allowMapChange, allowNoteEditing, disabledTokens, session, }: MapProps) { const { addToast } = useToasts(); const [selectedToolId, setSelectedToolId] = useState("move"); const { settings, setSettings } = useSettings(); function handleToolSettingChange(change: Partial) { setSettings((prevSettings) => ({ ...prevSettings, ...change, })); } const drawShapes = Object.values(mapState?.drawShapes || {}); const fogShapes = Object.values(mapState?.fogShapes || {}); function handleToolAction(action: string) { 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) { onMapDraw(new AddStatesAction([shape])); } function handleMapShapesRemove(shapeIds: string[]) { onMapDraw(new RemoveStatesAction(shapeIds)); } function handleFogShapesAdd(shapes: Fog[]) { onFogDraw(new AddStatesAction(shapes)); } function handleFogShapesCut(shapes: Fog[]) { onFogDraw(new CutFogAction(shapes)); } function handleFogShapesRemove(shapeIds: string[]) { onFogDraw(new RemoveStatesAction(shapeIds)); } function handleFogShapesEdit(shapes: Partial[]) { onFogDraw(new EditStatesAction(shapes)); } const disabledControls: MapToolId[] = []; if (!allowMapDrawing) { disabledControls.push("drawing"); } if (!map) { disabledControls.push("move"); disabledControls.push("measure"); disabledControls.push("pointer"); disabledControls.push("select"); } if (!allowFogDrawing) { disabledControls.push("fog"); } if (!allowMapChange) { disabledControls.push("map"); } if (!allowNoteEditing) { disabledControls.push("note"); } 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 mapControls = ( ); const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false); const [tokenMenuOptions, setTokenMenuOptions] = useState(); const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); function handleTokenMenuOpen(tokenStateId: string, tokenImage: Konva.Node) { setTokenMenuOptions({ tokenStateId, tokenImage }); setIsTokenMenuOpen(true); } const mapTokens = map && mapState && ( ); const tokenMenu = ( setIsTokenMenuOpen(false)} onTokenStateChange={onMapTokenStateChange} tokenState={ tokenMenuOptions && mapState?.tokens[tokenMenuOptions.tokenStateId] } tokenImage={tokenMenuOptions && tokenMenuOptions.tokenImage} map={map} /> ); const tokenDragOverlay = tokenDraggingOptions && ( { onMapTokenStateRemove(state); setTokenDraggingOptions(undefined); }} tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState} tokenNode={tokenDraggingOptions && tokenDraggingOptions.tokenNode} dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)} /> ); const mapDrawing = ( ); const mapFog = ( ); const mapGrid = map && map.showGrid && ; const mapMeasure = ( ); const mapPointer = ( ); const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); const [noteMenuOptions, setNoteMenuOptions] = useState(); const [noteDraggingOptions, setNoteDraggingOptions] = useState(); function handleNoteMenuOpen(noteId: string, noteNode: Konva.Node) { setNoteMenuOptions({ noteId, noteNode }); setIsNoteMenuOpen(true); } function sortNotes( a: Note, b: Note, noteDraggingOptions?: NoteDraggingOptions ) { if ( noteDraggingOptions && noteDraggingOptions.dragging && noteDraggingOptions.noteId === a.id ) { // If dragging token `a` move above return 1; } else if ( noteDraggingOptions && noteDraggingOptions.dragging && noteDraggingOptions.noteId === b.id ) { // If dragging token `b` move above return -1; } else { // Else sort so last modified is on top return a.lastModified - b.lastModified; } } const mapNotes = ( sortNotes(a, b, noteDraggingOptions) ) : [] } onNoteMenuOpen={handleNoteMenuOpen} draggable={ allowNoteEditing && (selectedToolId === "note" || selectedToolId === "move") } onNoteDragStart={(e, noteId) => setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) } onNoteDragEnd={() => noteDraggingOptions && setNoteDraggingOptions({ ...noteDraggingOptions, dragging: false }) } fadeOnHover={selectedToolId === "drawing"} /> ); const noteMenu = ( setIsNoteMenuOpen(false)} onNoteChange={onMapNoteChange} note={noteMenuOptions && mapState?.notes[noteMenuOptions.noteId]} noteNode={noteMenuOptions?.noteNode} map={map} /> ); const noteDragOverlay = noteDraggingOptions ? ( { onMapNoteRemove(noteId); setNoteDraggingOptions(undefined); }} /> ) : null; const mapSelect = ( ); return ( {mapControls} {tokenMenu} {noteMenu} {tokenDragOverlay} {noteDragOverlay} } selectedToolId={selectedToolId} onSelectedToolChange={setSelectedToolId} disabledControls={disabledControls} > {mapGrid} {mapDrawing} {mapNotes} {mapTokens} {mapFog} {mapPointer} {mapMeasure} {mapSelect} ); } export default Map;