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"; import Spinner from "./Spinner";
function LoadingOverlay({ type LoadingOverlayProps = {
bg,
children,
}: {
bg: string; bg: string;
children?: React.ReactNode; children?: React.ReactNode;
}) { };
function LoadingOverlay({ bg, children }: LoadingOverlayProps) {
return ( return (
<Box <Box
sx={{ sx={{

View File

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

View File

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

View File

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

View File

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

View File

@ -24,16 +24,16 @@ import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh";
const diceThrowSpeed = 2; const diceThrowSpeed = 2;
type SceneMountEvent = {
scene: Scene;
engine: Engine;
canvas: HTMLCanvasElement;
};
type SceneMountEventHandler = (event: SceneMountEvent) => void;
type DiceInteractionProps = { type DiceInteractionProps = {
onSceneMount?: ({ onSceneMount?: SceneMountEventHandler;
scene,
engine,
canvas,
}: {
scene: Scene;
engine: Engine;
canvas: HTMLCanvasElement | WebGLRenderingContext;
}) => void;
onPointerDown: () => void; onPointerDown: () => void;
onPointerUp: () => void; onPointerUp: () => void;
}; };

View File

@ -112,7 +112,9 @@ function GlobalImageDrop({
// Change map if only 1 dropped // Change map if only 1 dropped
if (maps.length === 1) { if (maps.length === 1) {
const mapState = await getMapState(maps[0].id); const mapState = await getMapState(maps[0].id);
onMapChange(maps[0], mapState); if (mapState) {
onMapChange(maps[0], mapState);
}
} }
setIsLoading(false); setIsLoading(false);

View File

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

View File

@ -38,7 +38,7 @@ function MapEditBar({
const { maps, mapStates, removeMaps, resetMap } = useMapData(); const { maps, mapStates, removeMaps, resetMap } = useMapData();
const { activeGroups, selectedGroupIds, onGroupSelect } = useGroup(); const { activeGroups, selectedGroupIds, onClearSelection } = useGroup();
useEffect(() => { useEffect(() => {
const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups); const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups);
@ -75,7 +75,7 @@ function MapEditBar({
setIsMapsRemoveModalOpen(false); setIsMapsRemoveModalOpen(false);
const selectedMaps = getSelectedMaps(); const selectedMaps = getSelectedMaps();
const selectedMapIds = selectedMaps.map((map) => map.id); const selectedMapIds = selectedMaps.map((map) => map.id);
onGroupSelect(undefined); onClearSelection();
await removeMaps(selectedMapIds); await removeMaps(selectedMapIds);
// Removed the map from the map screen if needed // Removed the map from the map screen if needed
if (currentMap && selectedMapIds.includes(currentMap.id)) { if (currentMap && selectedMapIds.includes(currentMap.id)) {
@ -136,7 +136,7 @@ function MapEditBar({
<Close <Close
title="Clear Selection" title="Clear Selection"
aria-label="Clear Selection" aria-label="Clear Selection"
onClick={() => onGroupSelect(undefined)} onClick={() => onClearSelection()}
/> />
<Flex> <Flex>
<IconButton <IconButton

View File

@ -73,11 +73,11 @@ function MapEditor({ map, onSettingsChange }: MapEditorProps) {
); );
useStageInteraction( useStageInteraction(
mapStageRef.current, mapStageRef,
stageScale, stageScale,
setStageScale, setStageScale,
stageTranslateRef, stageTranslateRef,
mapLayerRef.current, mapLayerRef,
getGridMaxZoom(map.grid), getGridMaxZoom(map.grid),
"move", "move",
preventMapInteraction 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 shortid from "shortid";
import { Group, Line } from "react-konva"; import { Group, Line } from "react-konva";
import useImage from "use-image"; import useImage from "use-image";
@ -23,7 +23,7 @@ import {
} from "../../contexts/GridContext"; } from "../../contexts/GridContext";
import { useKeyboard } from "../../contexts/KeyboardContext"; import { useKeyboard } from "../../contexts/KeyboardContext";
import Vector2 from "../../helpers/Vector2"; import Vector2, { BoundingBox } from "../../helpers/Vector2";
import { import {
simplifyPoints, simplifyPoints,
mergeFogShapes, mergeFogShapes,
@ -94,19 +94,23 @@ function MapFog({
const gridCellPixelOffset = useGridCellPixelOffset(); const gridCellPixelOffset = useGridCellPixelOffset();
const gridOffset = useGridOffset(); const gridOffset = useGridOffset();
const [gridSnappingSensitivity] = useSetting("map.gridSnappingSensitivity"); const [gridSnappingSensitivity] = useSetting<number>(
const [showFogGuides] = useSetting("fog.showGuides"); "map.gridSnappingSensitivity"
const [editOpacity] = useSetting("fog.editOpacity"); );
const [showFogGuides] = useSetting<boolean>("fog.showGuides");
const [editOpacity] = useSetting<number>("fog.editOpacity");
const mapStageRef = useMapStage(); const mapStageRef = useMapStage();
const [drawingShape, setDrawingShape] = useState<Fog | null>(null); const [drawingShape, setDrawingShape] = useState<Fog | null>(null);
const [isBrushDown, setIsBrushDown] = useState(false); const [isBrushDown, setIsBrushDown] = useState(false);
const [editingShapes, setEditingShapes] = useState([]); const [editingShapes, setEditingShapes] = useState<Fog[]>([]);
// Shapes that have been merged for fog // Shapes that have been merged for fog
const [fogShapes, setFogShapes] = useState(shapes); const [fogShapes, setFogShapes] = useState(shapes);
// Bounding boxes for guides // Bounding boxes for guides
const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState([]); const [fogShapeBoundingBoxes, setFogShapeBoundingBoxes] = useState<
BoundingBox[]
>([]);
const [guides, setGuides] = useState<Guide[]>([]); const [guides, setGuides] = useState<Guide[]>([]);
const shouldHover = const shouldHover =
@ -288,13 +292,7 @@ function MapFog({
if (Object.keys(state).length === shapes.length) { if (Object.keys(state).length === shapes.length) {
onShapeError("No fog to cut"); onShapeError("No fog to cut");
} else { } else {
onShapesCut( onShapesCut(drawingShapes);
drawingShapes.map((shape) => ({
id: shape.id,
type: shape.type,
data: shape.data,
}))
);
} }
} else { } else {
onShapesAdd( onShapesAdd(
@ -319,29 +317,31 @@ function MapFog({
function handlePointerClick() { function handlePointerClick() {
if (toolSettings.type === "polygon") { if (toolSettings.type === "polygon") {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
setDrawingShape((prevDrawingShape) => { if (brushPosition) {
if (prevDrawingShape) { setDrawingShape((prevDrawingShape) => {
return { if (prevDrawingShape) {
...prevDrawingShape, return {
data: { ...prevDrawingShape,
...prevDrawingShape.data, data: {
points: [...prevDrawingShape.data.points, brushPosition], ...prevDrawingShape.data,
}, points: [...prevDrawingShape.data.points, brushPosition],
}; },
} else { };
return { } else {
type: "fog", return {
data: { type: "fog",
points: [brushPosition, brushPosition], data: {
holes: [], points: [brushPosition, brushPosition],
}, holes: [],
strokeWidth: 0.5, },
color: toolSettings.useFogCut ? "red" : "black", strokeWidth: 0.5,
id: shortid.generate(), color: toolSettings.useFogCut ? "red" : "black",
visible: true, id: shortid.generate(),
}; visible: true,
} };
}); }
});
}
} }
} }
@ -349,41 +349,43 @@ function MapFog({
if (shouldUseGuides) { if (shouldUseGuides) {
let guides: Guide[] = []; let guides: Guide[] = [];
const brushPosition = getBrushPosition(false); const brushPosition = getBrushPosition(false);
const absoluteBrushPosition = Vector2.multiply(brushPosition, { if (brushPosition) {
x: mapWidth, const absoluteBrushPosition = Vector2.multiply(brushPosition, {
y: mapHeight, x: mapWidth,
}); y: mapHeight,
if (map.snapToGrid) { });
if (map.snapToGrid) {
guides.push(
...getGuidesFromGridCell(
absoluteBrushPosition,
grid,
gridCellPixelSize,
gridOffset,
gridCellPixelOffset,
gridSnappingSensitivity,
{ x: mapWidth, y: mapHeight }
)
);
}
guides.push( guides.push(
...getGuidesFromGridCell( ...getGuidesFromBoundingBoxes(
absoluteBrushPosition, brushPosition,
grid, fogShapeBoundingBoxes,
gridCellPixelSize, gridCellNormalizedSize,
gridOffset, gridSnappingSensitivity
gridCellPixelOffset,
gridSnappingSensitivity,
{ x: mapWidth, y: mapHeight }
) )
); );
setGuides(findBestGuides(brushPosition, guides));
} }
guides.push(
...getGuidesFromBoundingBoxes(
brushPosition,
fogShapeBoundingBoxes,
gridCellNormalizedSize,
gridSnappingSensitivity
)
);
setGuides(findBestGuides(brushPosition, guides));
} }
if (toolSettings.type === "polygon") { if (toolSettings.type === "polygon") {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
if (toolSettings.type === "polygon" && drawingShape) { if (toolSettings.type === "polygon" && drawingShape && brushPosition) {
setDrawingShape((prevShape) => { setDrawingShape((prevShape) => {
if (!prevShape) { if (!prevShape) {
return; return prevShape;
} }
return { return {
...prevShape, ...prevShape,
@ -401,32 +403,33 @@ function MapFog({
setGuides([]); setGuides([]);
} }
interactionEmitter.on("dragStart", handleBrushDown); interactionEmitter?.on("dragStart", handleBrushDown);
interactionEmitter.on("drag", handleBrushMove); interactionEmitter?.on("drag", handleBrushMove);
interactionEmitter.on("dragEnd", handleBrushUp); interactionEmitter?.on("dragEnd", handleBrushUp);
// Use mouse events for polygon and erase to allow for single clicks // Use mouse events for polygon and erase to allow for single clicks
mapStage.on("mousedown touchstart", handlePointerMove); mapStage?.on("mousedown touchstart", handlePointerMove);
mapStage.on("mousemove touchmove", handlePointerMove); mapStage?.on("mousemove touchmove", handlePointerMove);
mapStage.on("click tap", handlePointerClick); mapStage?.on("click tap", handlePointerClick);
mapStage.on("touchend", handelTouchEnd); mapStage?.on("touchend", handelTouchEnd);
return () => { return () => {
interactionEmitter.off("dragStart", handleBrushDown); interactionEmitter?.off("dragStart", handleBrushDown);
interactionEmitter.off("drag", handleBrushMove); interactionEmitter?.off("drag", handleBrushMove);
interactionEmitter.off("dragEnd", handleBrushUp); interactionEmitter?.off("dragEnd", handleBrushUp);
mapStage.off("mousedown touchstart", handlePointerMove); mapStage?.off("mousedown touchstart", handlePointerMove);
mapStage.off("mousemove touchmove", handlePointerMove); mapStage?.off("mousemove touchmove", handlePointerMove);
mapStage.off("click tap", handlePointerClick); mapStage?.off("click tap", handlePointerClick);
mapStage.off("touchend", handelTouchEnd); mapStage?.off("touchend", handelTouchEnd);
}; };
}); });
const finishDrawingPolygon = useCallback(() => { const finishDrawingPolygon = useCallback(() => {
const cut = toolSettings.useFogCut; const cut = toolSettings.useFogCut;
if (!drawingShape) {
return;
}
let polygonShape = { let polygonShape = {
id: drawingShape.id, ...drawingShape,
type: drawingShape.type,
data: { data: {
...drawingShape.data, ...drawingShape.data,
// Remove the last point as it hasn't been placed yet // Remove the last point as it hasn't been placed yet
@ -489,7 +492,7 @@ function MapFog({
]); ]);
// Add keyboard shortcuts // Add keyboard shortcuts
function handleKeyDown(event) { function handleKeyDown(event: KeyboardEvent) {
if ( if (
shortcuts.fogFinishPolygon(event) && shortcuts.fogFinishPolygon(event) &&
toolSettings.type === "polygon" && toolSettings.type === "polygon" &&
@ -507,17 +510,22 @@ function MapFog({
toolSettings.type === "polygon" toolSettings.type === "polygon"
) { ) {
if (drawingShape.data.points.length > 2) { if (drawingShape.data.points.length > 2) {
setDrawingShape((drawingShape) => ({ setDrawingShape((prevShape) => {
...drawingShape, if (!prevShape) {
data: { return prevShape;
...drawingShape.data, }
points: [ return {
// Shift last point to previous point ...prevShape,
...drawingShape.data.points.slice(0, -2), data: {
...drawingShape.data.points.slice(-1), ...prevShape.data,
], points: [
}, // Shift last point to previous point
})); ...prevShape.data.points.slice(0, -2),
...prevShape.data.points.slice(-1),
],
},
};
});
} else { } else {
setDrawingShape(null); setDrawingShape(null);
} }
@ -530,7 +538,7 @@ function MapFog({
useEffect(() => { useEffect(() => {
setDrawingShape((prevShape) => { setDrawingShape((prevShape) => {
if (!prevShape) { if (!prevShape) {
return; return prevShape;
} }
return { return {
...prevShape, ...prevShape,
@ -556,7 +564,7 @@ function MapFog({
} }
} }
function handleShapeOver(shape, isDown) { function handleShapeOver(shape: Fog, isDown: boolean) {
if (shouldHover && isDown) { if (shouldHover && isDown) {
if (editingShapes.findIndex((s) => s.id === shape.id) === -1) { if (editingShapes.findIndex((s) => s.id === shape.id) === -1) {
setEditingShapes((prevShapes) => [...prevShapes, shape]); 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]; return [...acc, point.x * mapWidth, point.y * mapHeight];
} }
function renderShape(shape) { function renderShape(shape: Fog) {
const points = shape.data.points.reduce(reducePoints, []); const points = shape.data.points.reduce(reducePoints, []);
const holes = const holes =
shape.data.holes && shape.data.holes &&
@ -608,15 +616,15 @@ function MapFog({
); );
} }
function renderEditingShape(shape) { function renderEditingShape(shape: Fog) {
const editingShape = { const editingShape: Fog = {
...shape, ...shape,
color: "#BB99FF", color: "primary",
}; };
return renderShape(editingShape); return renderShape(editingShape);
} }
function renderPolygonAcceptTick(shape) { function renderPolygonAcceptTick(shape: Fog) {
if (shape.data.points.length === 0) { if (shape.data.points.length === 0) {
return null; return null;
} }
@ -658,7 +666,7 @@ function MapFog({
} }
useEffect(() => { useEffect(() => {
function shapeVisible(shape) { function shapeVisible(shape: Fog) {
return (active && !toolSettings.preview) || shape.visible; 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 { useKeyboard } from "../../contexts/KeyboardContext";
import shortcuts from "../../shortcuts"; 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({ function MapInteraction({
map, map,
@ -27,7 +43,7 @@ function MapInteraction({
selectedToolId, selectedToolId,
onSelectedToolChange, onSelectedToolChange,
disabledControls, disabledControls,
}) { }: MapInteractionProps) {
const [mapImage, mapImageStatus] = useMapImage(map); const [mapImage, mapImageStatus] = useMapImage(map);
const [mapLoaded, setMapLoaded] = useState(false); 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 // 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 stageTranslateRef = useRef({ x: 0, y: 0 });
const mapStageRef = useMapStage(); const mapStageRef = useMapStage();
const mapLayerRef = useRef(); const mapLayerRef = useRef<LayerType>(null);
const mapImageRef = useRef(); const mapImageRef = useRef<ImageType>(null);
function handleResize(width, height) { function handleResize(width?: number, height?: number) {
if (width > 0 && height > 0) { if (width && height && width > 0 && height > 0) {
setStageWidth(width); setStageWidth(width);
setStageHeight(height); setStageHeight(height);
} }
} }
const containerRef = useRef(); const containerRef = useRef<HTMLDivElement>(null);
usePreventOverscroll(containerRef); usePreventOverscroll(containerRef);
const [mapWidth, mapHeight] = useImageCenter( const [mapWidth, mapHeight] = useImageCenter(
@ -76,11 +92,11 @@ function MapInteraction({
const [interactionEmitter] = useState(new EventEmitter()); const [interactionEmitter] = useState(new EventEmitter());
useStageInteraction( useStageInteraction(
mapStageRef.current, mapStageRef,
stageScale, stageScale,
setStageScale, setStageScale,
stageTranslateRef, stageTranslateRef,
mapLayerRef.current, mapLayerRef,
getGridMaxZoom(map?.grid), getGridMaxZoom(map?.grid),
selectedToolId, selectedToolId,
preventMapInteraction, preventMapInteraction,
@ -105,7 +121,7 @@ function MapInteraction({
} }
); );
function handleKeyDown(event) { function handleKeyDown(event: KeyboardEvent) {
// Change to move tool when pressing space // Change to move tool when pressing space
if (shortcuts.move(event) && selectedToolId === "move") { if (shortcuts.move(event) && selectedToolId === "move") {
// Stop active state on move icon from being selected // 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") { if (shortcuts.move(event) && selectedToolId === "move") {
onSelectedToolChange(previousSelectedToolRef.current); onSelectedToolChange(previousSelectedToolRef.current);
} }
@ -150,7 +166,7 @@ function MapInteraction({
useKeyboard(handleKeyDown, handleKeyUp); useKeyboard(handleKeyDown, handleKeyUp);
function getCursorForTool(tool) { function getCursorForTool(tool: MapToolId) {
switch (tool) { switch (tool) {
case "move": case "move":
return "move"; return "move";
@ -195,6 +211,7 @@ function MapInteraction({
<KonvaBridge <KonvaBridge
stageRender={(children) => ( stageRender={(children) => (
<Stage <Stage
// @ts-ignore
width={stageWidth} width={stageWidth}
height={stageHeight} height={stageHeight}
scale={{ x: stageScale, y: stageScale }} 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 { Group, Line, Text, Label, Tag } from "react-konva";
import { import {
@ -25,8 +25,17 @@ import { getRelativePointerPosition } from "../../helpers/konva";
import { parseGridScale, gridDistance } from "../../helpers/grid"; import { parseGridScale, gridDistance } from "../../helpers/grid";
import useGridSnapping from "../../hooks/useGridSnapping"; 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 stageScale = useDebouncedStageScale();
const mapWidth = useMapWidth(); const mapWidth = useMapWidth();
const mapHeight = useMapHeight(); const mapHeight = useMapHeight();
@ -39,7 +48,8 @@ function MapMeasure({ map, active }) {
const gridOffset = useGridOffset(); const gridOffset = useGridOffset();
const mapStageRef = useMapStage(); const mapStageRef = useMapStage();
const [drawingShapeData, setDrawingShapeData] = useState(null); const [drawingShapeData, setDrawingShapeData] =
useState<MeasureData | null>(null);
const [isBrushDown, setIsBrushDown] = useState(false); const [isBrushDown, setIsBrushDown] = useState(false);
const gridScale = parseGridScale(active && grid.measurement.scale); const gridScale = parseGridScale(active && grid.measurement.scale);
@ -57,8 +67,13 @@ function MapMeasure({ map, active }) {
const mapImage = mapStage?.findOne("#mapImage"); const mapImage = mapStage?.findOne("#mapImage");
function getBrushPosition() { function getBrushPosition() {
const mapImage = mapStage.findOne("#mapImage"); if (!mapImage) {
return;
}
let position = getRelativePointerPosition(mapImage); let position = getRelativePointerPosition(mapImage);
if (!position) {
return;
}
if (map.snapToGrid) { if (map.snapToGrid) {
position = snapPositionToGrid(position); position = snapPositionToGrid(position);
} }
@ -70,7 +85,13 @@ function MapMeasure({ map, active }) {
function handleBrushDown() { function handleBrushDown() {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
const { points } = getDefaultShapeData("line", brushPosition); if (!brushPosition) {
return;
}
const { points } = getDefaultShapeData(
"line",
brushPosition
) as PointsData;
const length = 0; const length = 0;
setDrawingShapeData({ length, points }); setDrawingShapeData({ length, points });
setIsBrushDown(true); setIsBrushDown(true);
@ -78,13 +99,15 @@ function MapMeasure({ map, active }) {
function handleBrushMove() { function handleBrushMove() {
const brushPosition = getBrushPosition(); const brushPosition = getBrushPosition();
if (isBrushDown && drawingShapeData) { if (isBrushDown && drawingShapeData && brushPosition && mapImage) {
const { points } = getUpdatedShapeData( const { points } = getUpdatedShapeData(
"line", "line",
drawingShapeData, drawingShapeData,
brushPosition, brushPosition,
gridCellNormalizedSize gridCellNormalizedSize,
); 1,
1
) as PointsData;
// Convert back to pixel values // Convert back to pixel values
const a = Vector2.subtract( const a = Vector2.subtract(
Vector2.multiply(points[0], { Vector2.multiply(points[0], {
@ -113,20 +136,24 @@ function MapMeasure({ map, active }) {
setIsBrushDown(false); setIsBrushDown(false);
} }
interactionEmitter.on("dragStart", handleBrushDown); interactionEmitter?.on("dragStart", handleBrushDown);
interactionEmitter.on("drag", handleBrushMove); interactionEmitter?.on("drag", handleBrushMove);
interactionEmitter.on("dragEnd", handleBrushUp); interactionEmitter?.on("dragEnd", handleBrushUp);
return () => { return () => {
interactionEmitter.off("dragStart", handleBrushDown); interactionEmitter?.off("dragStart", handleBrushDown);
interactionEmitter.off("drag", handleBrushMove); interactionEmitter?.off("drag", handleBrushMove);
interactionEmitter.off("dragEnd", handleBrushUp); interactionEmitter?.off("dragEnd", handleBrushUp);
}; };
}); });
function renderShape(shapeData) { function renderShape(shapeData: MeasureData) {
const linePoints = shapeData.points.reduce( 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 React, { useEffect, useState } from "react";
import Modal from "react-modal"; import Modal from "react-modal";
import { useThemeUI } from "theme-ui"; 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({ function MapMenu({
isOpen, isOpen,
@ -14,17 +30,18 @@ function MapMenu({
style, style,
// A node to exclude from the pointer event for closing // A node to exclude from the pointer event for closing
excludeNode, excludeNode,
}) { }: MapMenuProps) {
// Save modal node in state to ensure that the pointer listeners // Save modal node in state to ensure that the pointer listeners
// are removed if the open state changed not from the onRequestClose // are removed if the open state changed not from the onRequestClose
// callback // callback
const [modalContentNode, setModalContentNode] = useState(null); const [modalContentNode, setModalContentNode] = useState<Node | null>(null);
useEffect(() => { useEffect(() => {
// Close modal if interacting with any other element // Close modal if interacting with any other element
function handleInteraction(event) { function handleInteraction(event: Event) {
const path = event.composedPath(); const path = event.composedPath();
if ( if (
modalContentNode &&
!path.includes(modalContentNode) && !path.includes(modalContentNode) &&
!(excludeNode && path.includes(excludeNode)) && !(excludeNode && path.includes(excludeNode)) &&
!(event.target instanceof HTMLTextAreaElement) !(event.target instanceof HTMLTextAreaElement)
@ -48,7 +65,7 @@ function MapMenu({
}; };
}, [modalContentNode, excludeNode, onRequestClose]); }, [modalContentNode, excludeNode, onRequestClose]);
function handleModalContent(node) { function handleModalContent(node: HTMLDivElement) {
setModalContentNode(node); setModalContentNode(node);
onModalContent(node); onModalContent(node);
} }
@ -62,7 +79,7 @@ function MapMenu({
style={{ style={{
overlay: { top: "0", bottom: "initial" }, overlay: { top: "0", bottom: "initial" },
content: { content: {
backgroundColor: theme.colors.overlay, backgroundColor: theme.colors?.overlay as CSS.Property.Color,
top, top,
left, left,
right, right,

View File

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

View File

@ -2,7 +2,7 @@ import React from "react";
import { Grid } from "theme-ui"; import { Grid } from "theme-ui";
import Tile from "../tile/Tile"; import Tile from "../tile/Tile";
import MapImage from "./MapImage"; import MapImage from "./MapTileImage";
import useResponsiveLayout from "../../hooks/useResponsiveLayout"; 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; isLoading: boolean;
assetLoadStart: (id: string) => void; assetLoadStart: (id: string) => void;
assetProgressUpdate: (update: MapLoadingProgressUpdate) => void; assetProgressUpdate: (update: MapLoadingProgressUpdate) => void;
loadingProgressRef: React.MutableRefObject<number | null>; loadingProgressRef: React.MutableRefObject<number>;
}; };
const MapLoadingContext = const MapLoadingContext =
@ -28,7 +28,7 @@ export function MapLoadingProvider({
// Mapping from asset id to the count and total number of pieces loaded // Mapping from asset id to the count and total number of pieces loaded
const assetProgressRef = useRef<Record<string, MapLoadingProgress>>({}); const assetProgressRef = useRef<Record<string, MapLoadingProgress>>({});
// Loading progress of all assets between 0 and 1 // Loading progress of all assets between 0 and 1
const loadingProgressRef = useRef<number | null>(null); const loadingProgressRef = useRef<number>(0);
const assetLoadStart = useCallback((id) => { const assetLoadStart = useCallback((id) => {
setIsLoading(true); setIsLoading(true);

View File

@ -1,4 +1,4 @@
import { useContext } from "react"; import React, { useContext } from "react";
import { import {
InteractionEmitterContext, InteractionEmitterContext,
@ -45,10 +45,20 @@ import {
} from "../contexts/GridContext"; } from "../contexts/GridContext";
import DatabaseContext, { useDatabase } from "../contexts/DatabaseContext"; 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 * 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 mapStageRef = useMapStage();
const userId = useUserId(); const userId = useUserId();
const settings = useSettings(); 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 { useGesture } from "react-use-gesture";
import { Handlers } from "react-use-gesture/dist/types"; import { Handlers } from "react-use-gesture/dist/types";
import normalizeWheel from "normalize-wheel"; import normalizeWheel from "normalize-wheel";
import { Stage } from "konva/types/Stage";
import { Layer } from "konva/types/Layer"; import { Layer } from "konva/types/Layer";
import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext";
import { MapStage } from "../contexts/MapStageContext";
import shortcuts from "../shortcuts"; import shortcuts from "../shortcuts";
@ -18,11 +18,11 @@ const minZoom = 0.1;
type StageScaleChangeEventHandler = (newScale: number) => void; type StageScaleChangeEventHandler = (newScale: number) => void;
function useStageInteraction( function useStageInteraction(
stage: Stage, stageRef: MapStage,
stageScale: number, stageScale: number,
onStageScaleChange: StageScaleChangeEventHandler, onStageScaleChange: StageScaleChangeEventHandler,
stageTranslateRef: React.MutableRefObject<Vector2>, stageTranslateRef: React.MutableRefObject<Vector2>,
layer: Layer, layerRef: React.RefObject<Layer>,
maxZoom = 10, maxZoom = 10,
tool = "move", tool = "move",
preventInteraction = false, preventInteraction = false,
@ -52,12 +52,18 @@ function useStageInteraction(
...gesture, ...gesture,
onWheelStart: (props) => { onWheelStart: (props) => {
const { event } = props; const { event } = props;
const layer = layerRef.current;
isInteractingWithCanvas.current = isInteractingWithCanvas.current =
layer && event.target === layer.getCanvas()._canvas; layer !== null && event.target === layer.getCanvas()._canvas;
gesture.onWheelStart && gesture.onWheelStart(props); gesture.onWheelStart && gesture.onWheelStart(props);
}, },
onWheel: (props) => { onWheel: (props) => {
if (preventInteraction || !isInteractingWithCanvas.current) { const stage = stageRef.current;
if (
preventInteraction ||
!isInteractingWithCanvas.current ||
stage === null
) {
return; return;
} }
const { event, last } = props; const { event, last } = props;
@ -94,8 +100,9 @@ function useStageInteraction(
}, },
onPinchStart: (props) => { onPinchStart: (props) => {
const { event } = props; const { event } = props;
const layer = layerRef.current;
isInteractingWithCanvas.current = isInteractingWithCanvas.current =
layer && event.target === layer.getCanvas()._canvas; layer !== null && event.target === layer.getCanvas()._canvas;
const { da, origin } = props; const { da, origin } = props;
const [distance] = da; const [distance] = da;
const [originX, originY] = origin; const [originX, originY] = origin;
@ -104,9 +111,17 @@ function useStageInteraction(
gesture.onPinchStart && gesture.onPinchStart(props); gesture.onPinchStart && gesture.onPinchStart(props);
}, },
onPinch: (props) => { onPinch: (props) => {
if (preventInteraction || !isInteractingWithCanvas.current) { const layer = layerRef.current;
const stage = stageRef.current;
if (
preventInteraction ||
!isInteractingWithCanvas.current ||
layer === null ||
stage === null
) {
return; return;
} }
const { da, origin } = props; const { da, origin } = props;
const [distance] = da; const [distance] = da;
const [originX, originY] = origin; const [originX, originY] = origin;
@ -157,16 +172,19 @@ function useStageInteraction(
}, },
onDragStart: (props) => { onDragStart: (props) => {
const { event } = props; const { event } = props;
const layer = layerRef.current;
isInteractingWithCanvas.current = isInteractingWithCanvas.current =
layer && event.target === layer.getCanvas()._canvas; layer !== null && event.target === layer.getCanvas()._canvas;
gesture.onDragStart && gesture.onDragStart(props); gesture.onDragStart && gesture.onDragStart(props);
}, },
onDrag: (props) => { onDrag: (props) => {
const { delta, pinching } = props; const { delta, pinching } = props;
const stage = stageRef.current;
if ( if (
preventInteraction || preventInteraction ||
pinching || pinching ||
!isInteractingWithCanvas.current !isInteractingWithCanvas.current ||
stage === null
) { ) {
return; return;
} }
@ -198,7 +216,8 @@ function useStageInteraction(
function handleKeyDown(event: KeyboardEvent) { function handleKeyDown(event: KeyboardEvent) {
// TODO: Find better way to detect whether keyboard event should fire. // TODO: Find better way to detect whether keyboard event should fire.
// This one fires on all open stages // This one fires on all open stages
if (preventInteraction) { const stage = stageRef.current;
if (preventInteraction || stage === null) {
return; return;
} }
if (shortcuts.stageZoomIn(event) || shortcuts.stageZoomOut(event)) { if (shortcuts.stageZoomIn(event) || shortcuts.stageZoomOut(event)) {