Typescript
This commit is contained in:
parent
2ce9d2dd08
commit
a1fb67df7b
@ -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={{
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -208,7 +208,9 @@ function MapContols({
|
||||
>
|
||||
<Settings
|
||||
settings={toolSettings[selectedToolId]}
|
||||
onSettingChange={(change) =>
|
||||
onSettingChange={(
|
||||
change: Partial<Settings["fog" | "drawing" | "pointer"]>
|
||||
) =>
|
||||
onToolSettingChange({
|
||||
[selectedToolId]: {
|
||||
...toolSettings[selectedToolId],
|
||||
|
@ -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
|
||||
|
@ -73,11 +73,11 @@ function MapEditor({ map, onSettingsChange }: MapEditorProps) {
|
||||
);
|
||||
|
||||
useStageInteraction(
|
||||
mapStageRef.current,
|
||||
mapStageRef,
|
||||
stageScale,
|
||||
setStageScale,
|
||||
stageTranslateRef,
|
||||
mapLayerRef.current,
|
||||
mapLayerRef,
|
||||
getGridMaxZoom(map.grid),
|
||||
"move",
|
||||
preventMapInteraction
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
@ -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 }}
|
@ -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,
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
@ -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,
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import Tile from "../tile/Tile";
|
||||
import MapImage from "./MapImage";
|
||||
import MapImage from "./MapTileImage";
|
||||
|
||||
function MapTile({
|
||||
map,
|
||||
|
@ -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";
|
||||
|
||||
|
23
src/components/map/MapTileImage.tsx
Normal file
23
src/components/map/MapTileImage.tsx
Normal 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;
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -350,6 +350,7 @@ export function gridDistance(
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)) {
|
||||
|
Loading…
Reference in New Issue
Block a user