From a1fb67df7b22f91daabf4eaac0da37744e91aa8e Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 16 Jul 2021 18:59:29 +1000 Subject: [PATCH] Typescript --- src/components/LoadingOverlay.tsx | 9 +- src/components/Modal.tsx | 3 +- src/components/Select.tsx | 2 +- src/components/banner/Banner.tsx | 16 +- src/components/banner/ErrorBanner.tsx | 13 +- src/components/dice/DiceInteraction.tsx | 18 +- src/components/image/GlobalImageDrop.tsx | 4 +- src/components/map/MapControls.tsx | 4 +- src/components/map/MapEditBar.tsx | 6 +- src/components/map/MapEditor.tsx | 4 +- src/components/map/MapFog.tsx | 210 +++++++++--------- src/components/map/MapImage.js | 18 -- .../{MapInteraction.js => MapInteraction.tsx} | 39 +++- .../map/{MapMeasure.js => MapMeasure.tsx} | 59 +++-- .../map/{MapMenu.js => MapMenu.tsx} | 27 ++- src/components/map/MapTile.js | 2 +- src/components/map/MapTileGroup.js | 2 +- src/components/map/MapTileImage.tsx | 23 ++ src/contexts/MapLoadingContext.tsx | 4 +- src/helpers/KonvaBridge.tsx | 14 +- src/helpers/grid.ts | 1 + src/hooks/useStageInteraction.ts | 39 +++- 22 files changed, 314 insertions(+), 203 deletions(-) delete mode 100644 src/components/map/MapImage.js rename src/components/map/{MapInteraction.js => MapInteraction.tsx} (85%) rename src/components/map/{MapMeasure.js => MapMeasure.tsx} (76%) rename src/components/map/{MapMenu.js => MapMenu.tsx} (75%) create mode 100644 src/components/map/MapTileImage.tsx diff --git a/src/components/LoadingOverlay.tsx b/src/components/LoadingOverlay.tsx index 55f2ce8..87b24ce 100644 --- a/src/components/LoadingOverlay.tsx +++ b/src/components/LoadingOverlay.tsx @@ -3,13 +3,12 @@ import { Box } from "theme-ui"; import Spinner from "./Spinner"; -function LoadingOverlay({ - bg, - children, -}: { +type LoadingOverlayProps = { bg: string; children?: React.ReactNode; -}) { +}; + +function LoadingOverlay({ bg, children }: LoadingOverlayProps) { return ( diff --git a/src/components/dice/DiceInteraction.tsx b/src/components/dice/DiceInteraction.tsx index 7b7fb68..cd16805 100644 --- a/src/components/dice/DiceInteraction.tsx +++ b/src/components/dice/DiceInteraction.tsx @@ -24,16 +24,16 @@ import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh"; const diceThrowSpeed = 2; +type SceneMountEvent = { + scene: Scene; + engine: Engine; + canvas: HTMLCanvasElement; +}; + +type SceneMountEventHandler = (event: SceneMountEvent) => void; + type DiceInteractionProps = { - onSceneMount?: ({ - scene, - engine, - canvas, - }: { - scene: Scene; - engine: Engine; - canvas: HTMLCanvasElement | WebGLRenderingContext; - }) => void; + onSceneMount?: SceneMountEventHandler; onPointerDown: () => void; onPointerUp: () => void; }; diff --git a/src/components/image/GlobalImageDrop.tsx b/src/components/image/GlobalImageDrop.tsx index 8cbd66d..d495f8d 100644 --- a/src/components/image/GlobalImageDrop.tsx +++ b/src/components/image/GlobalImageDrop.tsx @@ -112,7 +112,9 @@ function GlobalImageDrop({ // Change map if only 1 dropped if (maps.length === 1) { const mapState = await getMapState(maps[0].id); - onMapChange(maps[0], mapState); + if (mapState) { + onMapChange(maps[0], mapState); + } } setIsLoading(false); diff --git a/src/components/map/MapControls.tsx b/src/components/map/MapControls.tsx index 88e87ff..fdd72ff 100644 --- a/src/components/map/MapControls.tsx +++ b/src/components/map/MapControls.tsx @@ -208,7 +208,9 @@ function MapContols({ > + onSettingChange={( + change: Partial + ) => onToolSettingChange({ [selectedToolId]: { ...toolSettings[selectedToolId], diff --git a/src/components/map/MapEditBar.tsx b/src/components/map/MapEditBar.tsx index d944e05..eef70ee 100644 --- a/src/components/map/MapEditBar.tsx +++ b/src/components/map/MapEditBar.tsx @@ -38,7 +38,7 @@ function MapEditBar({ const { maps, mapStates, removeMaps, resetMap } = useMapData(); - const { activeGroups, selectedGroupIds, onGroupSelect } = useGroup(); + const { activeGroups, selectedGroupIds, onClearSelection } = useGroup(); useEffect(() => { const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups); @@ -75,7 +75,7 @@ function MapEditBar({ setIsMapsRemoveModalOpen(false); const selectedMaps = getSelectedMaps(); const selectedMapIds = selectedMaps.map((map) => map.id); - onGroupSelect(undefined); + onClearSelection(); await removeMaps(selectedMapIds); // Removed the map from the map screen if needed if (currentMap && selectedMapIds.includes(currentMap.id)) { @@ -136,7 +136,7 @@ function MapEditBar({ onGroupSelect(undefined)} + onClick={() => onClearSelection()} /> ( + "map.gridSnappingSensitivity" + ); + const [showFogGuides] = useSetting("fog.showGuides"); + const [editOpacity] = useSetting("fog.editOpacity"); const mapStageRef = useMapStage(); const [drawingShape, setDrawingShape] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); - const [editingShapes, setEditingShapes] = useState([]); + const [editingShapes, setEditingShapes] = useState([]); // Shapes that have been merged for fog const [fogShapes, setFogShapes] = useState(shapes); // Bounding boxes for guides - const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState([]); + const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState< + BoundingBox[] + >([]); const [guides, setGuides] = useState([]); const shouldHover = @@ -288,13 +292,7 @@ function MapFog({ if (Object.keys(state).length === shapes.length) { onShapeError("No fog to cut"); } else { - onShapesCut( - drawingShapes.map((shape) => ({ - id: shape.id, - type: shape.type, - data: shape.data, - })) - ); + onShapesCut(drawingShapes); } } else { onShapesAdd( @@ -319,29 +317,31 @@ function MapFog({ function handlePointerClick() { if (toolSettings.type === "polygon") { const brushPosition = getBrushPosition(); - setDrawingShape((prevDrawingShape) => { - if (prevDrawingShape) { - return { - ...prevDrawingShape, - data: { - ...prevDrawingShape.data, - points: [...prevDrawingShape.data.points, brushPosition], - }, - }; - } else { - return { - type: "fog", - data: { - points: [brushPosition, brushPosition], - holes: [], - }, - strokeWidth: 0.5, - color: toolSettings.useFogCut ? "red" : "black", - id: shortid.generate(), - visible: true, - }; - } - }); + if (brushPosition) { + setDrawingShape((prevDrawingShape) => { + if (prevDrawingShape) { + return { + ...prevDrawingShape, + data: { + ...prevDrawingShape.data, + points: [...prevDrawingShape.data.points, brushPosition], + }, + }; + } else { + return { + type: "fog", + data: { + points: [brushPosition, brushPosition], + holes: [], + }, + strokeWidth: 0.5, + color: toolSettings.useFogCut ? "red" : "black", + id: shortid.generate(), + visible: true, + }; + } + }); + } } } @@ -349,41 +349,43 @@ function MapFog({ if (shouldUseGuides) { let guides: Guide[] = []; const brushPosition = getBrushPosition(false); - const absoluteBrushPosition = Vector2.multiply(brushPosition, { - x: mapWidth, - y: mapHeight, - }); - if (map.snapToGrid) { + if (brushPosition) { + const absoluteBrushPosition = Vector2.multiply(brushPosition, { + x: mapWidth, + y: mapHeight, + }); + if (map.snapToGrid) { + guides.push( + ...getGuidesFromGridCell( + absoluteBrushPosition, + grid, + gridCellPixelSize, + gridOffset, + gridCellPixelOffset, + gridSnappingSensitivity, + { x: mapWidth, y: mapHeight } + ) + ); + } + guides.push( - ...getGuidesFromGridCell( - absoluteBrushPosition, - grid, - gridCellPixelSize, - gridOffset, - gridCellPixelOffset, - gridSnappingSensitivity, - { x: mapWidth, y: mapHeight } + ...getGuidesFromBoundingBoxes( + brushPosition, + fogShapeBoundingBoxes, + gridCellNormalizedSize, + gridSnappingSensitivity ) ); + + setGuides(findBestGuides(brushPosition, guides)); } - - guides.push( - ...getGuidesFromBoundingBoxes( - brushPosition, - fogShapeBoundingBoxes, - gridCellNormalizedSize, - gridSnappingSensitivity - ) - ); - - setGuides(findBestGuides(brushPosition, guides)); } if (toolSettings.type === "polygon") { const brushPosition = getBrushPosition(); - if (toolSettings.type === "polygon" && drawingShape) { + if (toolSettings.type === "polygon" && drawingShape && brushPosition) { setDrawingShape((prevShape) => { if (!prevShape) { - return; + return prevShape; } return { ...prevShape, @@ -401,32 +403,33 @@ function MapFog({ setGuides([]); } - interactionEmitter.on("dragStart", handleBrushDown); - interactionEmitter.on("drag", handleBrushMove); - interactionEmitter.on("dragEnd", handleBrushUp); + interactionEmitter?.on("dragStart", handleBrushDown); + interactionEmitter?.on("drag", handleBrushMove); + interactionEmitter?.on("dragEnd", handleBrushUp); // Use mouse events for polygon and erase to allow for single clicks - mapStage.on("mousedown touchstart", handlePointerMove); - mapStage.on("mousemove touchmove", handlePointerMove); - mapStage.on("click tap", handlePointerClick); - mapStage.on("touchend", handelTouchEnd); + mapStage?.on("mousedown touchstart", handlePointerMove); + mapStage?.on("mousemove touchmove", handlePointerMove); + mapStage?.on("click tap", handlePointerClick); + mapStage?.on("touchend", handelTouchEnd); return () => { - interactionEmitter.off("dragStart", handleBrushDown); - interactionEmitter.off("drag", handleBrushMove); - interactionEmitter.off("dragEnd", handleBrushUp); - mapStage.off("mousedown touchstart", handlePointerMove); - mapStage.off("mousemove touchmove", handlePointerMove); - mapStage.off("click tap", handlePointerClick); - mapStage.off("touchend", handelTouchEnd); + interactionEmitter?.off("dragStart", handleBrushDown); + interactionEmitter?.off("drag", handleBrushMove); + interactionEmitter?.off("dragEnd", handleBrushUp); + mapStage?.off("mousedown touchstart", handlePointerMove); + mapStage?.off("mousemove touchmove", handlePointerMove); + mapStage?.off("click tap", handlePointerClick); + mapStage?.off("touchend", handelTouchEnd); }; }); const finishDrawingPolygon = useCallback(() => { const cut = toolSettings.useFogCut; - + if (!drawingShape) { + return; + } let polygonShape = { - id: drawingShape.id, - type: drawingShape.type, + ...drawingShape, data: { ...drawingShape.data, // Remove the last point as it hasn't been placed yet @@ -489,7 +492,7 @@ function MapFog({ ]); // Add keyboard shortcuts - function handleKeyDown(event) { + function handleKeyDown(event: KeyboardEvent) { if ( shortcuts.fogFinishPolygon(event) && toolSettings.type === "polygon" && @@ -507,17 +510,22 @@ function MapFog({ toolSettings.type === "polygon" ) { if (drawingShape.data.points.length > 2) { - setDrawingShape((drawingShape) => ({ - ...drawingShape, - data: { - ...drawingShape.data, - points: [ - // Shift last point to previous point - ...drawingShape.data.points.slice(0, -2), - ...drawingShape.data.points.slice(-1), - ], - }, - })); + setDrawingShape((prevShape) => { + if (!prevShape) { + return prevShape; + } + return { + ...prevShape, + data: { + ...prevShape.data, + points: [ + // Shift last point to previous point + ...prevShape.data.points.slice(0, -2), + ...prevShape.data.points.slice(-1), + ], + }, + }; + }); } else { setDrawingShape(null); } @@ -530,7 +538,7 @@ function MapFog({ useEffect(() => { setDrawingShape((prevShape) => { if (!prevShape) { - return; + return prevShape; } return { ...prevShape, @@ -556,7 +564,7 @@ function MapFog({ } } - function handleShapeOver(shape, isDown) { + function handleShapeOver(shape: Fog, isDown: boolean) { if (shouldHover && isDown) { if (editingShapes.findIndex((s) => s.id === shape.id) === -1) { setEditingShapes((prevShapes) => [...prevShapes, shape]); @@ -564,11 +572,11 @@ function MapFog({ } } - function reducePoints(acc, point) { + function reducePoints(acc: number[], point: Vector2) { return [...acc, point.x * mapWidth, point.y * mapHeight]; } - function renderShape(shape) { + function renderShape(shape: Fog) { const points = shape.data.points.reduce(reducePoints, []); const holes = shape.data.holes && @@ -608,15 +616,15 @@ function MapFog({ ); } - function renderEditingShape(shape) { - const editingShape = { + function renderEditingShape(shape: Fog) { + const editingShape: Fog = { ...shape, - color: "#BB99FF", + color: "primary", }; return renderShape(editingShape); } - function renderPolygonAcceptTick(shape) { + function renderPolygonAcceptTick(shape: Fog) { if (shape.data.points.length === 0) { return null; } @@ -658,7 +666,7 @@ function MapFog({ } useEffect(() => { - function shapeVisible(shape) { + function shapeVisible(shape: Fog) { return (active && !toolSettings.preview) || shape.visible; } diff --git a/src/components/map/MapImage.js b/src/components/map/MapImage.js deleted file mode 100644 index c9c71f8..0000000 --- a/src/components/map/MapImage.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import { Image } from "theme-ui"; - -import { useDataURL } from "../../contexts/AssetsContext"; -import { mapSources as defaultMapSources } from "../../maps"; - -const MapTileImage = React.forwardRef(({ map, ...props }, ref) => { - const mapURL = useDataURL( - map, - defaultMapSources, - undefined, - map.type === "file" - ); - - return ; -}); - -export default MapTileImage; diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.tsx similarity index 85% rename from src/components/map/MapInteraction.js rename to src/components/map/MapInteraction.tsx index 61ba335..202445b 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.tsx @@ -18,6 +18,22 @@ import { GridProvider } from "../../contexts/GridContext"; import { useKeyboard } from "../../contexts/KeyboardContext"; import shortcuts from "../../shortcuts"; +import { Layer as LayerType } from "konva/types/Layer"; +import { Image as ImageType } from "konva/types/shapes/Image"; +import { Map, MapToolId } from "../../types/Map"; +import { MapState } from "../../types/MapState"; + +type SelectedToolChangeEventHanlder = (tool: MapToolId) => void; + +type MapInteractionProps = { + map: Map; + mapState: MapState; + children?: React.ReactNode; + controls: React.ReactNode; + selectedToolId: MapToolId; + onSelectedToolChange: SelectedToolChangeEventHanlder; + disabledControls: MapToolId[]; +}; function MapInteraction({ map, @@ -27,7 +43,7 @@ function MapInteraction({ selectedToolId, onSelectedToolChange, disabledControls, -}) { +}: MapInteractionProps) { const [mapImage, mapImageStatus] = useMapImage(map); const [mapLoaded, setMapLoaded] = useState(false); @@ -47,17 +63,17 @@ function MapInteraction({ // Avoid state udpates when panning the map by using a ref and updating the konva element directly const stageTranslateRef = useRef({ x: 0, y: 0 }); const mapStageRef = useMapStage(); - const mapLayerRef = useRef(); - const mapImageRef = useRef(); + const mapLayerRef = useRef(null); + const mapImageRef = useRef(null); - function handleResize(width, height) { - if (width > 0 && height > 0) { + function handleResize(width?: number, height?: number) { + if (width && height && width > 0 && height > 0) { setStageWidth(width); setStageHeight(height); } } - const containerRef = useRef(); + const containerRef = useRef(null); usePreventOverscroll(containerRef); const [mapWidth, mapHeight] = useImageCenter( @@ -76,11 +92,11 @@ function MapInteraction({ const [interactionEmitter] = useState(new EventEmitter()); useStageInteraction( - mapStageRef.current, + mapStageRef, stageScale, setStageScale, stageTranslateRef, - mapLayerRef.current, + mapLayerRef, getGridMaxZoom(map?.grid), selectedToolId, preventMapInteraction, @@ -105,7 +121,7 @@ function MapInteraction({ } ); - function handleKeyDown(event) { + function handleKeyDown(event: KeyboardEvent) { // Change to move tool when pressing space if (shortcuts.move(event) && selectedToolId === "move") { // Stop active state on move icon from being selected @@ -142,7 +158,7 @@ function MapInteraction({ } } - function handleKeyUp(event) { + function handleKeyUp(event: KeyboardEvent) { if (shortcuts.move(event) && selectedToolId === "move") { onSelectedToolChange(previousSelectedToolRef.current); } @@ -150,7 +166,7 @@ function MapInteraction({ useKeyboard(handleKeyDown, handleKeyUp); - function getCursorForTool(tool) { + function getCursorForTool(tool: MapToolId) { switch (tool) { case "move": return "move"; @@ -195,6 +211,7 @@ function MapInteraction({ ( (null); const [isBrushDown, setIsBrushDown] = useState(false); const gridScale = parseGridScale(active && grid.measurement.scale); @@ -57,8 +67,13 @@ function MapMeasure({ map, active }) { const mapImage = mapStage?.findOne("#mapImage"); function getBrushPosition() { - const mapImage = mapStage.findOne("#mapImage"); + if (!mapImage) { + return; + } let position = getRelativePointerPosition(mapImage); + if (!position) { + return; + } if (map.snapToGrid) { position = snapPositionToGrid(position); } @@ -70,7 +85,13 @@ function MapMeasure({ map, active }) { function handleBrushDown() { const brushPosition = getBrushPosition(); - const { points } = getDefaultShapeData("line", brushPosition); + if (!brushPosition) { + return; + } + const { points } = getDefaultShapeData( + "line", + brushPosition + ) as PointsData; const length = 0; setDrawingShapeData({ length, points }); setIsBrushDown(true); @@ -78,13 +99,15 @@ function MapMeasure({ map, active }) { function handleBrushMove() { const brushPosition = getBrushPosition(); - if (isBrushDown && drawingShapeData) { + if (isBrushDown && drawingShapeData && brushPosition && mapImage) { const { points } = getUpdatedShapeData( "line", drawingShapeData, brushPosition, - gridCellNormalizedSize - ); + gridCellNormalizedSize, + 1, + 1 + ) as PointsData; // Convert back to pixel values const a = Vector2.subtract( Vector2.multiply(points[0], { @@ -113,20 +136,24 @@ function MapMeasure({ map, active }) { setIsBrushDown(false); } - interactionEmitter.on("dragStart", handleBrushDown); - interactionEmitter.on("drag", handleBrushMove); - interactionEmitter.on("dragEnd", handleBrushUp); + interactionEmitter?.on("dragStart", handleBrushDown); + interactionEmitter?.on("drag", handleBrushMove); + interactionEmitter?.on("dragEnd", handleBrushUp); return () => { - interactionEmitter.off("dragStart", handleBrushDown); - interactionEmitter.off("drag", handleBrushMove); - interactionEmitter.off("dragEnd", handleBrushUp); + interactionEmitter?.off("dragStart", handleBrushDown); + interactionEmitter?.off("drag", handleBrushMove); + interactionEmitter?.off("dragEnd", handleBrushUp); }; }); - function renderShape(shapeData) { + function renderShape(shapeData: MeasureData) { const linePoints = shapeData.points.reduce( - (acc, point) => [...acc, point.x * mapWidth, point.y * mapHeight], + (acc: number[], point) => [ + ...acc, + point.x * mapWidth, + point.y * mapHeight, + ], [] ); diff --git a/src/components/map/MapMenu.js b/src/components/map/MapMenu.tsx similarity index 75% rename from src/components/map/MapMenu.js rename to src/components/map/MapMenu.tsx index c107f6b..a60409a 100644 --- a/src/components/map/MapMenu.js +++ b/src/components/map/MapMenu.tsx @@ -1,6 +1,22 @@ import React, { useEffect, useState } from "react"; import Modal from "react-modal"; import { useThemeUI } from "theme-ui"; +import CSS from "csstype"; + +import { RequestCloseEventHandler } from "../../types/Events"; + +type MapMenuProps = { + isOpen: boolean; + onRequestClose: RequestCloseEventHandler; + onModalContent: (instance: HTMLDivElement) => void; + top: number; + left: number; + bottom: number; + right: number; + children: React.ReactNode; + style: React.CSSProperties; + excludeNode: Node | null; +}; function MapMenu({ isOpen, @@ -14,17 +30,18 @@ function MapMenu({ style, // A node to exclude from the pointer event for closing excludeNode, -}) { +}: MapMenuProps) { // Save modal node in state to ensure that the pointer listeners // are removed if the open state changed not from the onRequestClose // callback - const [modalContentNode, setModalContentNode] = useState(null); + const [modalContentNode, setModalContentNode] = useState(null); useEffect(() => { // Close modal if interacting with any other element - function handleInteraction(event) { + function handleInteraction(event: Event) { const path = event.composedPath(); if ( + modalContentNode && !path.includes(modalContentNode) && !(excludeNode && path.includes(excludeNode)) && !(event.target instanceof HTMLTextAreaElement) @@ -48,7 +65,7 @@ function MapMenu({ }; }, [modalContentNode, excludeNode, onRequestClose]); - function handleModalContent(node) { + function handleModalContent(node: HTMLDivElement) { setModalContentNode(node); onModalContent(node); } @@ -62,7 +79,7 @@ function MapMenu({ style={{ overlay: { top: "0", bottom: "initial" }, content: { - backgroundColor: theme.colors.overlay, + backgroundColor: theme.colors?.overlay as CSS.Property.Color, top, left, right, diff --git a/src/components/map/MapTile.js b/src/components/map/MapTile.js index c061b19..a27a275 100644 --- a/src/components/map/MapTile.js +++ b/src/components/map/MapTile.js @@ -1,7 +1,7 @@ import React from "react"; import Tile from "../tile/Tile"; -import MapImage from "./MapImage"; +import MapImage from "./MapTileImage"; function MapTile({ map, diff --git a/src/components/map/MapTileGroup.js b/src/components/map/MapTileGroup.js index 753fc63..89df13b 100644 --- a/src/components/map/MapTileGroup.js +++ b/src/components/map/MapTileGroup.js @@ -2,7 +2,7 @@ import React from "react"; import { Grid } from "theme-ui"; import Tile from "../tile/Tile"; -import MapImage from "./MapImage"; +import MapImage from "./MapTileImage"; import useResponsiveLayout from "../../hooks/useResponsiveLayout"; diff --git a/src/components/map/MapTileImage.tsx b/src/components/map/MapTileImage.tsx new file mode 100644 index 0000000..e2d6888 --- /dev/null +++ b/src/components/map/MapTileImage.tsx @@ -0,0 +1,23 @@ +import { Image, ImageProps } from "theme-ui"; + +import { useDataURL } from "../../contexts/AssetsContext"; +import { mapSources as defaultMapSources } from "../../maps"; +import { Map } from "../../types/Map"; + +type MapTileImageProps = { + map: Map; +} & ImageProps; + +function MapTileImage({ map, ...props }: MapTileImageProps) { + const mapURL = useDataURL( + map, + defaultMapSources, + undefined, + map.type === "file" + ); + + return ; + } +); + +export default MapTileImage; diff --git a/src/contexts/MapLoadingContext.tsx b/src/contexts/MapLoadingContext.tsx index f752c54..6039326 100644 --- a/src/contexts/MapLoadingContext.tsx +++ b/src/contexts/MapLoadingContext.tsx @@ -13,7 +13,7 @@ type MapLoadingContext = { isLoading: boolean; assetLoadStart: (id: string) => void; assetProgressUpdate: (update: MapLoadingProgressUpdate) => void; - loadingProgressRef: React.MutableRefObject; + loadingProgressRef: React.MutableRefObject; }; const MapLoadingContext = @@ -28,7 +28,7 @@ export function MapLoadingProvider({ // Mapping from asset id to the count and total number of pieces loaded const assetProgressRef = useRef>({}); // Loading progress of all assets between 0 and 1 - const loadingProgressRef = useRef(null); + const loadingProgressRef = useRef(0); const assetLoadStart = useCallback((id) => { setIsLoading(true); diff --git a/src/helpers/KonvaBridge.tsx b/src/helpers/KonvaBridge.tsx index 464df4b..9f9b70d 100644 --- a/src/helpers/KonvaBridge.tsx +++ b/src/helpers/KonvaBridge.tsx @@ -1,4 +1,4 @@ -import { useContext } from "react"; +import React, { useContext } from "react"; import { InteractionEmitterContext, @@ -45,10 +45,20 @@ import { } from "../contexts/GridContext"; import DatabaseContext, { useDatabase } from "../contexts/DatabaseContext"; +type StageRender = (wrapped: React.ReactNode) => React.ReactElement; + +type KonvaBridgeProps = { + stageRender: StageRender; + children: React.ReactNode; +}; + /** * Provide a bridge for konva that forwards our contexts */ -function KonvaBridge({ stageRender, children }: { stageRender: any, children: any}) { +function KonvaBridge({ + stageRender, + children, +}: KonvaBridgeProps): React.ReactElement { const mapStageRef = useMapStage(); const userId = useUserId(); const settings = useSettings(); diff --git a/src/helpers/grid.ts b/src/helpers/grid.ts index 7c329f6..f6eeea5 100644 --- a/src/helpers/grid.ts +++ b/src/helpers/grid.ts @@ -350,6 +350,7 @@ export function gridDistance( ); } } + return 0; } /** diff --git a/src/hooks/useStageInteraction.ts b/src/hooks/useStageInteraction.ts index bba35a4..4ffe169 100644 --- a/src/hooks/useStageInteraction.ts +++ b/src/hooks/useStageInteraction.ts @@ -2,10 +2,10 @@ import { useRef, useEffect, useState } from "react"; import { useGesture } from "react-use-gesture"; import { Handlers } from "react-use-gesture/dist/types"; import normalizeWheel from "normalize-wheel"; -import { Stage } from "konva/types/Stage"; import { Layer } from "konva/types/Layer"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; +import { MapStage } from "../contexts/MapStageContext"; import shortcuts from "../shortcuts"; @@ -18,11 +18,11 @@ const minZoom = 0.1; type StageScaleChangeEventHandler = (newScale: number) => void; function useStageInteraction( - stage: Stage, + stageRef: MapStage, stageScale: number, onStageScaleChange: StageScaleChangeEventHandler, stageTranslateRef: React.MutableRefObject, - layer: Layer, + layerRef: React.RefObject, maxZoom = 10, tool = "move", preventInteraction = false, @@ -52,12 +52,18 @@ function useStageInteraction( ...gesture, onWheelStart: (props) => { const { event } = props; + const layer = layerRef.current; isInteractingWithCanvas.current = - layer && event.target === layer.getCanvas()._canvas; + layer !== null && event.target === layer.getCanvas()._canvas; gesture.onWheelStart && gesture.onWheelStart(props); }, onWheel: (props) => { - if (preventInteraction || !isInteractingWithCanvas.current) { + const stage = stageRef.current; + if ( + preventInteraction || + !isInteractingWithCanvas.current || + stage === null + ) { return; } const { event, last } = props; @@ -94,8 +100,9 @@ function useStageInteraction( }, onPinchStart: (props) => { const { event } = props; + const layer = layerRef.current; isInteractingWithCanvas.current = - layer && event.target === layer.getCanvas()._canvas; + layer !== null && event.target === layer.getCanvas()._canvas; const { da, origin } = props; const [distance] = da; const [originX, originY] = origin; @@ -104,9 +111,17 @@ function useStageInteraction( gesture.onPinchStart && gesture.onPinchStart(props); }, onPinch: (props) => { - if (preventInteraction || !isInteractingWithCanvas.current) { + const layer = layerRef.current; + const stage = stageRef.current; + if ( + preventInteraction || + !isInteractingWithCanvas.current || + layer === null || + stage === null + ) { return; } + const { da, origin } = props; const [distance] = da; const [originX, originY] = origin; @@ -157,16 +172,19 @@ function useStageInteraction( }, onDragStart: (props) => { const { event } = props; + const layer = layerRef.current; isInteractingWithCanvas.current = - layer && event.target === layer.getCanvas()._canvas; + layer !== null && event.target === layer.getCanvas()._canvas; gesture.onDragStart && gesture.onDragStart(props); }, onDrag: (props) => { const { delta, pinching } = props; + const stage = stageRef.current; if ( preventInteraction || pinching || - !isInteractingWithCanvas.current + !isInteractingWithCanvas.current || + stage === null ) { return; } @@ -198,7 +216,8 @@ function useStageInteraction( function handleKeyDown(event: KeyboardEvent) { // TODO: Find better way to detect whether keyboard event should fire. // This one fires on all open stages - if (preventInteraction) { + const stage = stageRef.current; + if (preventInteraction || stage === null) { return; } if (shortcuts.stageZoomIn(event) || shortcuts.stageZoomOut(event)) {