Typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-16 18:59:29 +10:00
parent 2ce9d2dd08
commit a1fb67df7b
22 changed files with 314 additions and 203 deletions

View File

@ -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 (
<Box
sx={{

View File

@ -2,6 +2,7 @@ import React, { ReactChild } from "react";
import Modal, { Props } from "react-modal";
import { useThemeUI, Close } from "theme-ui";
import { useSpring, animated, config } from "react-spring";
import CSS from "csstype";
type ModalProps = Props & {
children: ReactChild | ReactChild[];
@ -38,7 +39,7 @@ function StyledModal({
...(style?.overlay || {}),
},
content: {
backgroundColor: theme.colors.background,
backgroundColor: theme.colors?.background as CSS.Property.Color,
top: "initial",
left: "initial",
bottom: "initial",

View File

@ -9,7 +9,7 @@ type SelectProps = {
function Select({ creatable, ...props }: SelectProps) {
const { theme } = useThemeUI();
const Component = creatable ? Creatable : (ReactSelect as any);
const Component: any = creatable ? Creatable : ReactSelect;
return (
<Component

View File

@ -3,19 +3,21 @@ import { useThemeUI, Close } from "theme-ui";
import { RequestCloseEventHandler } from "../../types/Events";
import CSS from "csstype";
type BannerProps = {
isOpen: boolean;
onRequestClose: RequestCloseEventHandler;
children: React.ReactNode;
allowClose: boolean;
backgroundColor?: CSS.Property.Color;
};
function Banner({
isOpen,
onRequestClose,
children,
allowClose,
backgroundColor,
}: {
isOpen: boolean;
onRequestClose: RequestCloseEventHandler;
children: React.ReactNode;
allowClose: boolean;
backgroundColor?: CSS.Property.Color;
}) {
}: BannerProps) {
const { theme } = useThemeUI();
return (

View File

@ -2,13 +2,14 @@ import { Box, Text } from "theme-ui";
import Banner from "./Banner";
function ErrorBanner({
error,
onRequestClose,
}: {
import { RequestCloseEventHandler } from "../../types/Events";
type ErrorBannerProps = {
error: Error | undefined;
onRequestClose;
}) {
onRequestClose: RequestCloseEventHandler;
};
function ErrorBanner({ error, onRequestClose }: ErrorBannerProps) {
return (
<Banner isOpen={!!error} onRequestClose={onRequestClose}>
<Box p={1}>

View File

@ -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;
};

View File

@ -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);

View File

@ -208,7 +208,9 @@ function MapContols({
>
<Settings
settings={toolSettings[selectedToolId]}
onSettingChange={(change) =>
onSettingChange={(
change: Partial<Settings["fog" | "drawing" | "pointer"]>
) =>
onToolSettingChange({
[selectedToolId]: {
...toolSettings[selectedToolId],

View File

@ -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({
<Close
title="Clear Selection"
aria-label="Clear Selection"
onClick={() => onGroupSelect(undefined)}
onClick={() => onClearSelection()}
/>
<Flex>
<IconButton

View File

@ -73,11 +73,11 @@ function MapEditor({ map, onSettingsChange }: MapEditorProps) {
);
useStageInteraction(
mapStageRef.current,
mapStageRef,
stageScale,
setStageScale,
stageTranslateRef,
mapLayerRef.current,
mapLayerRef,
getGridMaxZoom(map.grid),
"move",
preventMapInteraction

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback } from "react";
import shortid from "shortid";
import { Group, Line } from "react-konva";
import useImage from "use-image";
@ -23,7 +23,7 @@ import {
} from "../../contexts/GridContext";
import { useKeyboard } from "../../contexts/KeyboardContext";
import Vector2 from "../../helpers/Vector2";
import Vector2, { BoundingBox } from "../../helpers/Vector2";
import {
simplifyPoints,
mergeFogShapes,
@ -94,19 +94,23 @@ function MapFog({
const gridCellPixelOffset = useGridCellPixelOffset();
const gridOffset = useGridOffset();
const [gridSnappingSensitivity] = useSetting("map.gridSnappingSensitivity");
const [showFogGuides] = useSetting("fog.showGuides");
const [editOpacity] = useSetting("fog.editOpacity");
const [gridSnappingSensitivity] = useSetting<number>(
"map.gridSnappingSensitivity"
);
const [showFogGuides] = useSetting<boolean>("fog.showGuides");
const [editOpacity] = useSetting<number>("fog.editOpacity");
const mapStageRef = useMapStage();
const [drawingShape, setDrawingShape] = useState<Fog | null>(null);
const [isBrushDown, setIsBrushDown] = useState(false);
const [editingShapes, setEditingShapes] = useState([]);
const [editingShapes, setEditingShapes] = useState<Fog[]>([]);
// 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<Guide[]>([]);
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;
}

View File

@ -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 <Image src={mapURL} ref={ref} {...props} />;
});
export default MapTileImage;

View File

@ -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<LayerType>(null);
const mapImageRef = useRef<ImageType>(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<HTMLDivElement>(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({
<KonvaBridge
stageRender={(children) => (
<Stage
// @ts-ignore
width={stageWidth}
height={stageHeight}
scale={{ x: stageScale, y: stageScale }}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { Group, Line, Text, Label, Tag } from "react-konva";
import {
@ -25,8 +25,17 @@ import { getRelativePointerPosition } from "../../helpers/konva";
import { parseGridScale, gridDistance } from "../../helpers/grid";
import useGridSnapping from "../../hooks/useGridSnapping";
import { Map } from "../../types/Map";
import { PointsData } from "../../types/Drawing";
function MapMeasure({ map, active }) {
type MapMeasureProps = {
map: Map;
active: boolean;
};
type MeasureData = { length: number; points: Vector2[] };
function MapMeasure({ map, active }: MapMeasureProps) {
const stageScale = useDebouncedStageScale();
const mapWidth = useMapWidth();
const mapHeight = useMapHeight();
@ -39,7 +48,8 @@ function MapMeasure({ map, active }) {
const gridOffset = useGridOffset();
const mapStageRef = useMapStage();
const [drawingShapeData, setDrawingShapeData] = useState(null);
const [drawingShapeData, setDrawingShapeData] =
useState<MeasureData | null>(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,
],
[]
);

View File

@ -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<Node | null>(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,

View File

@ -1,7 +1,7 @@
import React from "react";
import Tile from "../tile/Tile";
import MapImage from "./MapImage";
import MapImage from "./MapTileImage";
function MapTile({
map,

View File

@ -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";

View File

@ -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 <Image src={mapURL} {...props} />;
}
);
export default MapTileImage;

View File

@ -13,7 +13,7 @@ type MapLoadingContext = {
isLoading: boolean;
assetLoadStart: (id: string) => void;
assetProgressUpdate: (update: MapLoadingProgressUpdate) => void;
loadingProgressRef: React.MutableRefObject<number | null>;
loadingProgressRef: React.MutableRefObject<number>;
};
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<Record<string, MapLoadingProgress>>({});
// Loading progress of all assets between 0 and 1
const loadingProgressRef = useRef<number | null>(null);
const loadingProgressRef = useRef<number>(0);
const assetLoadStart = useCallback((id) => {
setIsLoading(true);

View File

@ -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();

View File

@ -350,6 +350,7 @@ export function gridDistance(
);
}
}
return 0;
}
/**

View File

@ -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<Vector2>,
layer: Layer,
layerRef: React.RefObject<Layer>,
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)) {