More typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-09 16:22:35 +10:00
parent 45a4443dd1
commit ecfab87aa0
66 changed files with 1350 additions and 1233 deletions

View File

@ -47,7 +47,7 @@
"react-markdown": "4",
"react-media": "^2.0.0-rc.1",
"react-modal": "^3.12.1",
"react-resize-detector": "4.2.3",
"react-resize-detector": "^6.7.4",
"react-router-dom": "^5.1.2",
"react-router-hash-link": "^2.2.2",
"react-scripts": "^4.0.3",
@ -63,7 +63,7 @@
"socket.io-client": "^4.1.2",
"socket.io-msgpack-parser": "^3.0.1",
"source-map-explorer": "^2.5.2",
"theme-ui": "^0.8.4",
"theme-ui": "^0.10.0",
"use-image": "^1.0.7",
"uuid": "^8.3.2",
"webrtc-adapter": "^7.7.1"
@ -107,6 +107,7 @@
"@types/react-router-dom": "^5.1.7",
"@types/shortid": "^0.0.29",
"@types/simple-peer": "^9.6.3",
"@types/uuid": "^8.3.1",
"typescript": "^4.2.4",
"worker-loader": "^3.0.8"
}

View File

@ -1,9 +1,8 @@
import React from "react";
import { Box, Input } from "theme-ui";
import { Box, Input, InputProps } from "theme-ui";
import SearchIcon from "../icons/SearchIcon";
function Search(props) {
function Search(props: InputProps) {
return (
<Box sx={{ position: "relative", flexGrow: 1 }}>
<Input

View File

@ -3,7 +3,21 @@ import { IconButton } from "theme-ui";
import Count from "./DiceButtonCount";
function DiceButton({ title, children, count, onClick, disabled }) {
type DiceButtonProps = {
title: string;
children: React.ReactNode;
count?: number;
onClick: React.MouseEventHandler<HTMLButtonElement>;
disabled: boolean;
};
function DiceButton({
title,
children,
count,
onClick,
disabled,
}: DiceButtonProps) {
return (
<IconButton
title={title}

View File

@ -1,7 +1,7 @@
import React from "react";
import { Box, Text } from "theme-ui";
function DiceButtonCount({ children }) {
function DiceButtonCount({ children }: { children: React.ReactNode }) {
return (
<Box
sx={{

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { Flex, IconButton, Box } from "theme-ui";
import SimpleBar from "simplebar-react";
@ -21,6 +21,20 @@ import Divider from "../Divider";
import { dice } from "../../dice";
import useSetting from "../../hooks/useSetting";
import { DefaultDice, DiceRoll, DiceType } from "../../types/Dice";
import Dice from "../../dice/Dice";
type DiceButtonsProps = {
diceRolls: DiceRoll[];
onDiceAdd: (style: typeof Dice, type: DiceType) => void;
onDiceLoad: (dice: DefaultDice) => void;
diceTraySize: "single" | "double";
onDiceTraySizeChange: (newSize: "single" | "double") => void;
shareDice: boolean;
onShareDiceChange: (value: boolean) => void;
loading: boolean;
};
function DiceButtons({
diceRolls,
onDiceAdd,
@ -30,29 +44,32 @@ function DiceButtons({
shareDice,
onShareDiceChange,
loading,
}) {
}: DiceButtonsProps) {
const [currentDiceStyle, setCurrentDiceStyle] = useSetting("dice.style");
const [currentDice, setCurrentDice] = useState(
dice.find((d) => d.key === currentDiceStyle)
const [currentDice, setCurrentDice] = useState<DefaultDice>(
dice.find((d) => d.key === currentDiceStyle) || dice[0]
);
useEffect(() => {
const initialDice = dice.find((d) => d.key === currentDiceStyle);
onDiceLoad(initialDice);
setCurrentDice(initialDice);
if (initialDice) {
onDiceLoad(initialDice);
setCurrentDice(initialDice);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const diceCounts = {};
const diceCounts: Partial<Record<DiceType, number>> = {};
for (let dice of diceRolls) {
if (dice.type in diceCounts) {
diceCounts[dice.type] += 1;
// TODO: Check type
diceCounts[dice.type]! += 1;
} else {
diceCounts[dice.type] = 1;
}
}
async function handleDiceChange(dice) {
async function handleDiceChange(dice: DefaultDice) {
await onDiceLoad(dice);
setCurrentDice(dice);
setCurrentDiceStyle(dice.key);

View File

@ -1,9 +1,10 @@
import React, { useRef, useEffect, useState } from "react";
import { useRef, useEffect, useState } from "react";
import { Engine } from "@babylonjs/core/Engines/engine";
import { Scene } from "@babylonjs/core/scene";
import { Vector3, Color4, Matrix } from "@babylonjs/core/Maths/math";
import { AmmoJSPlugin } from "@babylonjs/core/Physics/Plugins/ammoJSPlugin";
import { TargetCamera } from "@babylonjs/core/Cameras/targetCamera";
//@ts-ignore
import * as AMMO from "ammo.js";
import "@babylonjs/core/Physics/physicsEngineComponent";
@ -19,20 +20,44 @@ import ReactResizeDetector from "react-resize-detector";
import usePreventTouch from "../../hooks/usePreventTouch";
import ErrorBanner from "../banner/ErrorBanner";
import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh";
const diceThrowSpeed = 2;
function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
const [error, setError] = useState();
type DiceInteractionProps = {
onSceneMount?: ({
scene,
engine,
canvas,
}: {
scene: Scene;
engine: Engine;
canvas: HTMLCanvasElement | WebGLRenderingContext;
}) => void;
onPointerDown: () => void;
onPointerUp: () => any;
};
const sceneRef = useRef();
const engineRef = useRef();
const canvasRef = useRef();
const containerRef = useRef();
function DiceInteraction({
onSceneMount,
onPointerDown,
onPointerUp,
}: DiceInteractionProps) {
const [error, setError] = useState<Error | undefined>();
const sceneRef = useRef<Scene>();
const engineRef = useRef<Engine>();
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) {
return;
}
try {
const canvas = canvasRef.current;
const engine = new Engine(canvas, true, {
preserveDrawingBuffer: true,
stencil: true,
@ -67,13 +92,14 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
const delta = newPosition.subtract(currentPosition);
selectedMesh.setAbsolutePosition(newPosition);
const velocity = delta.scale(1000 / scene.deltaTime);
selectedMeshVelocityWindowRef.current = selectedMeshVelocityWindowRef.current.slice(
Math.max(
selectedMeshVelocityWindowRef.current.length -
selectedMeshVelocityWindowSize,
0
)
);
selectedMeshVelocityWindowRef.current =
selectedMeshVelocityWindowRef.current.slice(
Math.max(
selectedMeshVelocityWindowRef.current.length -
selectedMeshVelocityWindowSize,
0
)
);
selectedMeshVelocityWindowRef.current.push(velocity);
}
});
@ -82,21 +108,27 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
}
}, [onSceneMount]);
const selectedMeshRef = useRef();
const selectedMeshVelocityWindowRef = useRef([]);
const selectedMeshRef = useRef<AbstractMesh | null>(null);
const selectedMeshVelocityWindowRef = useRef<Vector3[]>([]);
const selectedMeshVelocityWindowSize = 4;
const selectedMeshMassRef = useRef();
const selectedMeshMassRef = useRef<number>(0);
function handlePointerDown() {
const scene = sceneRef.current;
if (scene) {
const pickInfo = scene.pick(scene.pointerX, scene.pointerY);
if (pickInfo.hit && pickInfo.pickedMesh.name !== "dice_tray") {
pickInfo.pickedMesh.physicsImpostor.setLinearVelocity(Vector3.Zero());
pickInfo.pickedMesh.physicsImpostor.setAngularVelocity(Vector3.Zero());
if (
pickInfo &&
pickInfo.hit &&
pickInfo.pickedMesh &&
pickInfo.pickedMesh.name !== "dice_tray"
) {
pickInfo.pickedMesh.physicsImpostor?.setLinearVelocity(Vector3.Zero());
pickInfo.pickedMesh.physicsImpostor?.setAngularVelocity(Vector3.Zero());
// Save the meshes mass and set it to 0 so we can pick it up
selectedMeshMassRef.current = pickInfo.pickedMesh.physicsImpostor.mass;
pickInfo.pickedMesh.physicsImpostor.setMass(0);
selectedMeshMassRef.current =
pickInfo.pickedMesh.physicsImpostor?.mass || 0;
pickInfo.pickedMesh.physicsImpostor?.setMass(0);
selectedMeshRef.current = pickInfo.pickedMesh;
}
@ -119,27 +151,29 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
}
// Re-apply the meshes mass
selectedMesh.physicsImpostor.setMass(selectedMeshMassRef.current);
selectedMesh.physicsImpostor.forceUpdate();
selectedMesh.physicsImpostor?.setMass(selectedMeshMassRef.current);
selectedMesh.physicsImpostor?.forceUpdate();
selectedMesh.physicsImpostor.applyImpulse(
selectedMesh.physicsImpostor?.applyImpulse(
velocity.scale(diceThrowSpeed * selectedMesh.physicsImpostor.mass),
selectedMesh.physicsImpostor.getObjectCenter()
);
}
selectedMeshRef.current = null;
selectedMeshVelocityWindowRef.current = [];
selectedMeshMassRef.current = null;
selectedMeshMassRef.current = 0;
onPointerUp();
}
function handleResize(width, height) {
const engine = engineRef.current;
if (engine) {
engine.resize();
canvasRef.current.width = width;
canvasRef.current.height = height;
function handleResize(width?: number, height?: number) {
if (width && height) {
const engine = engineRef.current;
if (engine && canvasRef.current) {
engine.resize();
canvasRef.current.width = width;
canvasRef.current.height = height;
}
}
}
@ -165,7 +199,7 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
style={{ outline: "none" }}
/>
</ReactResizeDetector>
<ErrorBanner error={error} onRequestClose={() => setError()} />
<ErrorBanner error={error} onRequestClose={() => setError(undefined)} />
</div>
);
}

View File

@ -5,17 +5,28 @@ import ClearDiceIcon from "../../icons/ClearDiceIcon";
import RerollDiceIcon from "../../icons/RerollDiceIcon";
import { getDiceRollTotal } from "../../helpers/dice";
import { DiceRoll } from "../../types/Dice";
const maxDiceRollsShown = 6;
function DiceResults({ diceRolls, onDiceClear, onDiceReroll }) {
type DiceResultsProps = {
diceRolls: DiceRoll[];
onDiceClear: () => void;
onDiceReroll: () => void;
};
function DiceResults({
diceRolls,
onDiceClear,
onDiceReroll,
}: DiceResultsProps) {
const [isExpanded, setIsExpanded] = useState(false);
if (diceRolls.length === 0) {
return null;
}
let rolls = [];
let rolls: React.ReactChild[] = [];
if (diceRolls.length > 1) {
rolls = diceRolls
.filter((dice) => dice.roll !== "unknown")

View File

@ -1,9 +1,17 @@
import React from "react";
import { Image } from "theme-ui";
import Tile from "../tile/Tile";
function DiceTile({ dice, isSelected, onDiceSelect, onDone }) {
import { DefaultDice } from "../../types/Dice";
type DiceTileProps = {
dice: DefaultDice;
isSelected: boolean;
onDiceSelect: (dice: DefaultDice) => void;
onDone: (dice: DefaultDice) => void;
};
function DiceTile({ dice, isSelected, onDiceSelect, onDone }: DiceTileProps) {
return (
<div style={{ cursor: "pointer" }}>
<Tile

View File

@ -1,12 +1,24 @@
import React from "react";
import { Grid } from "theme-ui";
import SimpleBar from "simplebar-react";
import DiceTile from "./DiceTile";
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
import { DefaultDice } from "../../types/Dice";
function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
type DiceTileProps = {
dice: DefaultDice[];
onDiceSelect: (dice: DefaultDice) => void;
selectedDice: DefaultDice;
onDone: (dice: DefaultDice) => void;
};
function DiceTiles({
dice,
onDiceSelect,
selectedDice,
onDone,
}: DiceTileProps) {
const layout = useResponsiveLayout();
return (
@ -29,7 +41,6 @@ function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
isSelected={selectedDice && dice.key === selectedDice.key}
onDiceSelect={onDiceSelect}
onDone={onDone}
size={layout.tileSize}
/>
))}
</Grid>

View File

@ -1,10 +1,11 @@
import React, { useRef, useCallback, useEffect, useState } from "react";
import { useRef, useCallback, useEffect, useState } from "react";
import { Vector3 } from "@babylonjs/core/Maths/math";
import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight";
import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator";
import { CubeTexture } from "@babylonjs/core/Materials/Textures/cubeTexture";
import { Box } from "theme-ui";
// @ts-ignore
import environment from "../../dice/environment.dds";
import DiceInteraction from "./DiceInteraction";
@ -19,6 +20,16 @@ import { useDiceLoading } from "../../contexts/DiceLoadingContext";
import { getDiceRoll } from "../../helpers/dice";
import useSetting from "../../hooks/useSetting";
import { DefaultDice, DiceMesh, DiceRoll, DiceType } from "../../types/Dice";
import { Scene } from "@babylonjs/core";
type DiceTrayOverlayProps = {
isOpen: boolean;
shareDice: boolean;
onShareDiceChange: () => void;
diceRolls: DiceRoll[];
onDiceRollsChange: (newRolls: DiceRoll[]) => void;
};
function DiceTrayOverlay({
isOpen,
@ -26,17 +37,18 @@ function DiceTrayOverlay({
onShareDiceChange,
diceRolls,
onDiceRollsChange,
}) {
const sceneRef = useRef();
const shadowGeneratorRef = useRef();
const diceRefs = useRef([]);
}: DiceTrayOverlayProps) {
const sceneRef = useRef<Scene>();
const shadowGeneratorRef = useRef<ShadowGenerator>();
const diceRefs = useRef<DiceMesh[]>([]);
const sceneVisibleRef = useRef(false);
const sceneInteractionRef = useRef(false);
// Add to the counter to ingore sleep values
const sceneKeepAwakeRef = useRef(0);
const diceTrayRef = useRef();
const diceTrayRef = useRef<DiceTray>();
const [diceTraySize, setDiceTraySize] = useState("single");
const [diceTraySize, setDiceTraySize] =
useState<"single" | "double">("single");
const { assetLoadStart, assetLoadFinish, isLoading } = useDiceLoading();
const [fullScreen] = useSetting("map.fullScreen");
@ -50,7 +62,7 @@ function DiceTrayOverlay({
}
// Forces rendering for 1 second
function forceRender() {
function forceRender(): () => void {
// Force rerender
sceneKeepAwakeRef.current++;
let triggered = false;
@ -97,7 +109,7 @@ function DiceTrayOverlay({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function initializeScene(scene) {
async function initializeScene(scene: Scene) {
handleAssetLoadStart();
let light = new DirectionalLight(
"DirectionalLight",
@ -124,16 +136,14 @@ function DiceTrayOverlay({
handleAssetLoadFinish();
}
function update(scene) {
function getDiceSpeed(dice) {
const diceSpeed = dice.instance.physicsImpostor
.getLinearVelocity()
.length();
function update(scene: Scene) {
function getDiceSpeed(dice: DiceMesh) {
const diceSpeed =
dice.instance.physicsImpostor?.getLinearVelocity()?.length() || 0;
// If the dice is a d100 check the d10 as well
if (dice.type === "d100") {
const d10Speed = dice.d10Instance.physicsImpostor
.getLinearVelocity()
.length();
if (dice.d10Instance) {
const d10Speed =
dice.d10Instance.physicsImpostor?.getLinearVelocity()?.length() || 0;
return Math.max(diceSpeed, d10Speed);
} else {
return diceSpeed;
@ -157,14 +167,14 @@ function DiceTrayOverlay({
const dice = die[i];
const speed = getDiceSpeed(dice);
// If the speed has been below 0.01 for 1s set dice to sleep
if (speed < 0.01 && !dice.sleepTimout) {
dice.sleepTimout = setTimeout(() => {
if (speed < 0.01 && !dice.sleepTimeout) {
dice.sleepTimeout = setTimeout(() => {
dice.asleep = true;
}, 1000);
} else if (speed > 0.5 && (dice.asleep || dice.sleepTimout)) {
} else if (speed > 0.5 && (dice.asleep || dice.sleepTimeout)) {
dice.asleep = false;
clearTimeout(dice.sleepTimout);
dice.sleepTimout = null;
dice.sleepTimeout && clearTimeout(dice.sleepTimeout);
dice.sleepTimeout = undefined;
}
}
@ -173,14 +183,14 @@ function DiceTrayOverlay({
}
}
function handleDiceAdd(style, type) {
function handleDiceAdd(style: typeof Dice, type: DiceType) {
const scene = sceneRef.current;
const shadowGenerator = shadowGeneratorRef.current;
if (scene && shadowGenerator) {
const instance = style.createInstance(type, scene);
shadowGenerator.addShadowCaster(instance);
Dice.roll(instance);
let dice = { type, instance, asleep: false };
style.roll(instance);
let dice: DiceMesh = { type, instance, asleep: false };
// If we have a d100 add a d10 as well
if (type === "d100") {
const d10Instance = style.createInstance("d10", scene);
@ -196,7 +206,7 @@ function DiceTrayOverlay({
const die = diceRefs.current;
for (let dice of die) {
dice.instance.dispose();
if (dice.type === "d100") {
if (dice.d10Instance) {
dice.d10Instance.dispose();
}
}
@ -208,14 +218,14 @@ function DiceTrayOverlay({
const die = diceRefs.current;
for (let dice of die) {
Dice.roll(dice.instance);
if (dice.type === "d100") {
if (dice.d10Instance) {
Dice.roll(dice.d10Instance);
}
dice.asleep = false;
}
}
async function handleDiceLoad(dice) {
async function handleDiceLoad(dice: DefaultDice) {
handleAssetLoadStart();
const scene = sceneRef.current;
if (scene) {
@ -230,10 +240,13 @@ function DiceTrayOverlay({
});
useEffect(() => {
let renderTimeout;
let renderCleanup;
let renderTimeout: NodeJS.Timeout;
let renderCleanup: () => void;
function handleResize() {
const map = document.querySelector(".map");
if (!map) {
return;
}
const mapRect = map.getBoundingClientRect();
const availableWidth = mapRect.width - 108; // Subtract padding
@ -283,7 +296,7 @@ function DiceTrayOverlay({
return;
}
let newRolls = [];
let newRolls: DiceRoll[] = [];
for (let i = 0; i < die.length; i++) {
const dice = die[i];
let roll = getDiceRoll(dice);

View File

@ -1,10 +1,22 @@
import React, { useState } from "react";
import { useState } from "react";
import { IconButton } from "theme-ui";
import SelectDiceIcon from "../../icons/SelectDiceIcon";
import SelectDiceModal from "../../modals/SelectDiceModal";
function SelectDiceButton({ onDiceChange, currentDice, disabled }) {
import { DefaultDice } from "../../types/Dice";
type SelectDiceButtonProps = {
onDiceChange: (dice: DefaultDice) => void;
currentDice: DefaultDice;
disabled: boolean;
};
function SelectDiceButton({
onDiceChange,
currentDice,
disabled,
}: SelectDiceButtonProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
function openModal() {
@ -14,7 +26,7 @@ function SelectDiceButton({ onDiceChange, currentDice, disabled }) {
setIsModalOpen(false);
}
function handleDone(dice) {
function handleDone(dice: DefaultDice) {
onDiceChange(dice);
closeModal();
}
@ -39,4 +51,8 @@ function SelectDiceButton({ onDiceChange, currentDice, disabled }) {
);
}
SelectDiceButton.defaultProps = {
disabled: false,
};
export default SelectDiceButton;

View File

@ -1,7 +1,14 @@
import React from "react";
import { useDraggable } from "@dnd-kit/core";
import { Data } from "@dnd-kit/core/dist/store/types";
function Draggable({ id, children, data }) {
type DraggableProps = {
id: string;
children: React.ReactNode;
data: Data;
};
function Draggable({ id, children, data }: DraggableProps) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id,
data,

View File

@ -1,7 +1,12 @@
import React from "react";
import { useDroppable } from "@dnd-kit/core";
function Droppable({ id, children, disabled, ...props }) {
type DroppableProps = React.HTMLAttributes<HTMLDivElement> & {
id: string;
disabled: boolean;
};
function Droppable({ id, children, disabled, ...props }: DroppableProps) {
const { setNodeRef } = useDroppable({ id, disabled });
return (

View File

@ -26,75 +26,7 @@ import {
EditShapeAction,
RemoveShapeAction,
} from "../../actions";
import { Fog, Path, Shape } from "../../helpers/drawing";
import Session from "../../network/Session";
import { Grid } from "../../helpers/grid";
import { ImageFile } from "../../helpers/image";
export type Resolutions = Record<string, ImageFile>;
export type Map = {
id: string;
name: string;
owner: string;
file?: Uint8Array;
quality?: string;
resolutions?: Resolutions;
grid: Grid;
group: string;
width: number;
height: number;
type: string;
lastUsed: number;
lastModified: number;
created: number;
showGrid: boolean;
snapToGrid: boolean;
thumbnail?: ImageFile;
};
export type Note = {
id: string;
color: string;
lastModified: number;
lastModifiedBy: string;
locked: boolean;
size: number;
text: string;
textOnly: boolean;
visible: boolean;
x: number;
y: number;
};
export type TokenState = {
id: string;
tokenId: string;
owner: string;
size: number;
category: string;
label: string;
statuses: string[];
x: number;
y: number;
lastModifiedBy: string;
lastModified: number;
rotation: number;
locked: boolean;
visible: boolean;
type: "default" | "file";
outline: any;
width: number;
height: number;
};
export type MapState = {
tokens: Record<string, TokenState>;
drawShapes: Record<string, Path | Shape>;
fogShapes: Record<string, Fog>;
editFlags: ["drawing", "tokens", "notes", "fog"];
notes: Record<string, Note>;
mapId: string;
};
function Map({
map,

View File

@ -1,11 +1,11 @@
import React from "react";
import { Box } from "theme-ui";
import { Box, ThemeUIStyleObject } from "theme-ui";
import { useInView } from "react-intersection-observer";
function LazyTile({ children }) {
function LazyTile({ children }: { children: React.ReactNode }) {
const [ref, inView] = useInView({ triggerOnce: false });
const sx = inView
const sx: ThemeUIStyleObject = inView
? {}
: { width: "100%", height: "0", paddingTop: "100%", position: "relative" };

View File

@ -6,6 +6,16 @@ import { animated, useSpring } from "react-spring";
import { GROUP_ID_PREFIX } from "../../contexts/TileDragContext";
type SortableTileProps = {
id: string;
disableGrouping: boolean;
disableSorting: boolean;
hidden: boolean;
children: React.ReactNode;
isDragging: boolean;
cursor: string;
};
function SortableTile({
id,
disableGrouping,
@ -14,7 +24,7 @@ function SortableTile({
children,
isDragging,
cursor,
}) {
}: SortableTileProps) {
const {
attributes,
listeners,
@ -35,7 +45,7 @@ function SortableTile({
};
// Sort div left aligned
const sortDropStyle = {
const sortDropStyle: React.CSSProperties = {
position: "absolute",
left: "-5px",
top: 0,
@ -46,7 +56,7 @@ function SortableTile({
};
// Group div center aligned
const groupDropStyle = {
const groupDropStyle: React.CSSProperties = {
position: "absolute",
top: 0,
left: 0,
@ -55,7 +65,7 @@ function SortableTile({
borderWidth: "4px",
borderRadius: "4px",
borderStyle:
over?.id === `${GROUP_ID_PREFIX}${id}` && active.id !== id
over?.id === `${GROUP_ID_PREFIX}${id}` && active?.id !== id
? "solid"
: "none",
};

View File

@ -15,8 +15,14 @@ import {
GROUP_SORTABLE_ID,
} from "../../contexts/TileDragContext";
import { useGroup } from "../../contexts/GroupContext";
import { Group } from "../../types/Group";
function SortableTiles({ renderTile, subgroup }) {
type SortableTilesProps = {
renderTile: (group: Group) => React.ReactNode;
subgroup: boolean;
};
function SortableTiles({ renderTile, subgroup }: SortableTilesProps) {
const dragId = useTileDragId();
const dragCursor = useTileDragCursor();
const overGroupId = useTileOverGroupId();
@ -38,14 +44,14 @@ function SortableTiles({ renderTile, subgroup }) {
const sortableId = subgroup ? GROUP_SORTABLE_ID : BASE_SORTABLE_ID;
// Only populate selected groups if needed
let selectedGroupIds = [];
let selectedGroupIds: string[] = [];
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
selectedGroupIds = allSelectedIds;
}
const disableSorting = (openGroupId && !subgroup) || filter;
const disableGrouping = subgroup || disableSorting || filter;
const disableSorting = !!((openGroupId && !subgroup) || filter);
const disableGrouping = !!(subgroup || disableSorting || filter);
function renderSortableGroup(group, selectedGroups) {
function renderSortableGroup(group: Group, selectedGroups: Group[]) {
if (overGroupId === group.id && dragId && group.id !== dragId) {
// If dragging over a group render a preview of that group
const previewGroup = moveGroupsInto(
@ -61,7 +67,7 @@ function SortableTiles({ renderTile, subgroup }) {
function renderTiles() {
const groupsByIds = keyBy(activeGroups, "id");
const selectedGroupIdsSet = new Set(selectedGroupIds);
let selectedGroups = [];
let selectedGroups: Group[] = [];
let hasSelectedContainerGroup = false;
for (let groupId of selectedGroupIds) {
const group = groupsByIds[groupId];
@ -72,8 +78,8 @@ function SortableTiles({ renderTile, subgroup }) {
}
}
}
return activeGroups.map((group) => {
const isDragging = dragId && selectedGroupIdsSet.has(group.id);
return activeGroups.map((group: Group) => {
const isDragging = dragId !== null && selectedGroupIdsSet.has(group.id);
const disableTileGrouping =
disableGrouping || isDragging || hasSelectedContainerGroup;
return (
@ -84,7 +90,7 @@ function SortableTiles({ renderTile, subgroup }) {
disableSorting={disableSorting}
hidden={group.id === openGroupId}
isDragging={isDragging}
cursor={dragCursor}
cursor={dragCursor || ""}
>
{renderSortableGroup(group, selectedGroups)}
</SortableTile>

View File

@ -8,8 +8,17 @@ import Vector2 from "../../helpers/Vector2";
import { useTileDragId } from "../../contexts/TileDragContext";
import { useGroup } from "../../contexts/GroupContext";
import { Group } from "../../types/Group";
function SortableTilesDragOverlay({ renderTile, subgroup }) {
type SortableTilesDragOverlayProps = {
renderTile: (group: Group) => React.ReactNode;
subgroup: boolean;
};
function SortableTilesDragOverlay({
renderTile,
subgroup,
}: SortableTilesDragOverlayProps) {
const dragId = useTileDragId();
const {
groups,
@ -27,7 +36,7 @@ function SortableTilesDragOverlay({ renderTile, subgroup }) {
: groups;
// Only populate selected groups if needed
let selectedGroupIds = [];
let selectedGroupIds: string[] = [];
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
selectedGroupIds = allSelectedIds;
}

View File

@ -3,6 +3,18 @@ import { Flex, IconButton, Box, Text, Badge } from "theme-ui";
import EditTileIcon from "../../icons/EditTileIcon";
type TileProps = {
title: string;
isSelected: boolean;
onSelect: () => void;
onEdit: () => void;
onDoubleClick: () => void;
canEdit: boolean;
badges: React.ReactChild[];
editTitle: string;
children: React.ReactNode;
};
function Tile({
title,
isSelected,
@ -13,7 +25,7 @@ function Tile({
badges,
editTitle,
children,
}) {
}: TileProps) {
return (
<Box
sx={{

View File

@ -1,4 +1,3 @@
import React from "react";
import { Flex, IconButton } from "theme-ui";
import AddIcon from "../../icons/AddIcon";
@ -10,7 +9,12 @@ import RadioIconButton from "../RadioIconButton";
import { useGroup } from "../../contexts/GroupContext";
function TileActionBar({ onAdd, addTitle }) {
type TileActionBarProps = {
onAdd: () => void;
addTitle: string;
};
function TileActionBar({ onAdd, addTitle }: TileActionBarProps) {
const {
selectMode,
onSelectModeChange,
@ -33,7 +37,7 @@ function TileActionBar({ onAdd, addTitle }) {
outlineOffset: "0px",
},
}}
onFocus={() => onGroupSelect()}
onFocus={() => onGroupSelect(undefined)}
>
<Search value={filter} onChange={(e) => onFilterChange(e.target.value)} />
<Flex

View File

@ -9,7 +9,7 @@ import useResponsiveLayout from "../../hooks/useResponsiveLayout";
import Droppable from "../drag/Droppable";
function TilesContainer({ children }) {
function TilesContainer({ children }: { children: React.ReactNode }) {
const { onGroupSelect } = useGroup();
const { theme } = useThemeUI();
@ -21,9 +21,9 @@ function TilesContainer({ children }) {
<SimpleBar
style={{
height: layout.tileContainerHeight,
backgroundColor: theme.colors.muted,
backgroundColor: theme.colors?.muted as string,
}}
onClick={() => onGroupSelect()}
onClick={() => onGroupSelect(undefined)}
>
<Grid
p={3}

View File

@ -16,15 +16,16 @@ import GroupNameModal from "../../modals/GroupNameModal";
import { renameGroup } from "../../helpers/group";
import Droppable from "../drag/Droppable";
import { Group } from "../../types/Group";
function TilesOverlay({ modalSize, children }) {
const {
groups,
openGroupId,
onGroupClose,
onGroupSelect,
onGroupsChange,
} = useGroup();
type TilesOverlayProps = {
modalSize: { width: number; height: number };
children: React.ReactNode;
};
function TilesOverlay({ modalSize, children }: TilesOverlayProps) {
const { groups, openGroupId, onGroupClose, onGroupSelect, onGroupsChange } =
useGroup();
const { theme } = useThemeUI();
@ -37,23 +38,29 @@ function TilesOverlay({ modalSize, children }) {
});
const [containerSize, setContinerSize] = useState({ width: 0, height: 0 });
function handleContainerResize(width, height) {
const size = Math.min(width, height) - 16;
setContinerSize({ width: size, height: size });
function handleContainerResize(width?: number, height?: number) {
if (width && height) {
const size = Math.min(width, height) - 16;
setContinerSize({ width: size, height: size });
}
}
const [isGroupNameModalOpen, setIsGroupNameModalOpen] = useState(false);
function handleGroupNameChange(name) {
onGroupsChange(renameGroup(groups, openGroupId, name));
setIsGroupNameModalOpen(false);
function handleGroupNameChange(name: string) {
if (openGroupId) {
onGroupsChange(renameGroup(groups, openGroupId, name), undefined);
setIsGroupNameModalOpen(false);
}
}
const group = groups.find((group) => group.id === openGroupId);
const group = groups.find((group: Group) => group.id === openGroupId);
if (!openGroupId) {
return null;
}
const groupName = group && group.type === "group" && group.name;
return (
<>
<Box
@ -104,14 +111,14 @@ function TilesOverlay({ modalSize, children }) {
>
<Flex my={1} sx={{ position: "relative" }}>
<Text as="p" my="2px">
{group?.name}
{groupName}
</Text>
<IconButton
sx={{
width: "24px",
height: "24px",
position: group?.name ? "absolute" : "relative",
left: group?.name ? "100%" : 0,
position: groupName ? "absolute" : "relative",
left: groupName ? "100%" : 0,
}}
title="Edit Group"
aria-label="Edit Group"
@ -125,9 +132,9 @@ function TilesOverlay({ modalSize, children }) {
width: containerSize.width - 16,
height: containerSize.height - 48,
marginBottom: "8px",
backgroundColor: theme.colors.muted,
backgroundColor: theme.colors?.muted as string,
}}
onClick={() => onGroupSelect()}
onClick={() => onGroupSelect(undefined)}
>
<Grid
sx={{
@ -179,7 +186,7 @@ function TilesOverlay({ modalSize, children }) {
</ReactResizeDetector>
<GroupNameModal
isOpen={isGroupNameModalOpen}
name={group?.name}
name={groupName || ""}
onSubmit={handleGroupNameChange}
onRequestClose={() => setIsGroupNameModalOpen(false)}
/>

View File

@ -8,49 +8,20 @@ import { useDatabase } from "./DatabaseContext";
import useDebounce from "../hooks/useDebounce";
import { omit } from "../helpers/shared";
import { Asset } from "../types/Asset";
/**
* @typedef Asset
* @property {string} id
* @property {number} width
* @property {number} height
* @property {Uint8Array} file
* @property {string} mime
* @property {string} owner
*/
type AssetsContext = {
getAsset: (assetId: string) => Promise<Asset | undefined>;
addAssets: (assets: Asset[]) => void;
putAsset: (asset: Asset) => void;
};
/**
* @callback getAsset
* @param {string} assetId
* @returns {Promise<Asset|undefined>}
*/
/**
* @callback addAssets
* @param {Asset[]} assets
*/
/**
* @callback putAsset
* @param {Asset} asset
*/
/**
* @typedef AssetsContext
* @property {getAsset} getAsset
* @property {addAssets} addAssets
* @property {putAsset} putAsset
*/
/**
* @type {React.Context<undefined|AssetsContext>}
*/
const AssetsContext = React.createContext();
const AssetsContext = React.createContext<AssetsContext | undefined>(undefined);
// 100 MB max cache size
const maxCacheSize = 1e8;
export function AssetsProvider({ children }) {
export function AssetsProvider({ children }: { children: React.ReactNode }) {
const { worker, database, databaseStatus } = useDatabase();
useEffect(() => {
@ -61,33 +32,39 @@ export function AssetsProvider({ children }) {
const getAsset = useCallback(
async (assetId) => {
return await database.table("assets").get(assetId);
if (database) {
return await database.table("assets").get(assetId);
}
},
[database]
);
const addAssets = useCallback(
async (assets) => {
await database.table("assets").bulkAdd(assets);
if (database) {
await database.table("assets").bulkAdd(assets);
}
},
[database]
);
const putAsset = useCallback(
async (asset) => {
// Check for broadcast channel and attempt to use worker to put map to avoid UI lockup
// Safari doesn't support BC so fallback to single thread
if (window.BroadcastChannel) {
const packedAsset = encode(asset);
const success = await worker.putData(
Comlink.transfer(packedAsset, [packedAsset.buffer]),
"assets"
);
if (!success) {
if (database) {
// Check for broadcast channel and attempt to use worker to put map to avoid UI lockup
// Safari doesn't support BC so fallback to single thread
if (window.BroadcastChannel) {
const packedAsset = encode(asset);
const success = await worker.putData(
Comlink.transfer(packedAsset, [packedAsset.buffer]),
"assets"
);
if (!success) {
await database.table("assets").put(asset);
}
} else {
await database.table("assets").put(asset);
}
} else {
await database.table("assets").put(asset);
}
},
[database, worker]
@ -119,35 +96,38 @@ export function useAssets() {
* @property {number} references
*/
/**
* @type React.Context<undefined|Object.<string, AssetURL>>
*/
export const AssetURLsStateContext = React.createContext();
type AssetURL = {
url: string | null;
id: string;
references: number;
};
/**
* @type React.Context<undefined|React.Dispatch<React.SetStateAction<{}>>>
*/
export const AssetURLsUpdaterContext = React.createContext();
type AssetURLs = Record<string, AssetURL>;
export const AssetURLsStateContext =
React.createContext<AssetURLs | undefined>(undefined);
export const AssetURLsUpdaterContext =
React.createContext<
React.Dispatch<React.SetStateAction<AssetURLs>> | undefined
>(undefined);
/**
* Helper to manage sharing of custom image sources between uses of useAssetURL
*/
export function AssetURLsProvider({ children }) {
const [assetURLs, setAssetURLs] = useState({});
export function AssetURLsProvider({ children }: { children: React.ReactNode }) {
const [assetURLs, setAssetURLs] = useState<AssetURLs>({});
const { database } = useDatabase();
// Keep track of the assets that need to be loaded
const [assetKeys, setAssetKeys] = useState([]);
const [assetKeys, setAssetKeys] = useState<string[]>([]);
// Load assets after 100ms
const loadingDebouncedAssetURLs = useDebounce(assetURLs, 100);
// Update the asset keys to load when a url is added without an asset attached
useEffect(() => {
if (!loadingDebouncedAssetURLs) {
return;
}
let keysToLoad = [];
let keysToLoad: string[] = [];
for (let url of Object.values(loadingDebouncedAssetURLs)) {
if (url.url === null) {
keysToLoad.push(url.id);
@ -159,8 +139,9 @@ export function AssetURLsProvider({ children }) {
}, [loadingDebouncedAssetURLs]);
// Get the new assets whenever the keys change
const assets = useLiveQuery(
() => database?.table("assets").where("id").anyOf(assetKeys).toArray(),
const assets = useLiveQuery<Asset[]>(
() =>
database?.table("assets").where("id").anyOf(assetKeys).toArray() || [],
[database, assetKeys]
);
@ -197,7 +178,7 @@ export function AssetURLsProvider({ children }) {
let urlsToCleanup = [];
for (let url of Object.values(prevURLs)) {
if (url.references <= 0) {
URL.revokeObjectURL(url.url);
url.url && URL.revokeObjectURL(url.url);
urlsToCleanup.push(url.id);
}
}
@ -220,13 +201,13 @@ export function AssetURLsProvider({ children }) {
/**
* Helper function to load either file or default asset into a URL
* @param {string} assetId
* @param {"file"|"default"} type
* @param {Object.<string, string>} defaultSources
* @param {string|undefined} unknownSource
* @returns {string|undefined}
*/
export function useAssetURL(assetId, type, defaultSources, unknownSource) {
export function useAssetURL(
assetId: string,
type: "file" | "default",
defaultSources: Record<string, string>,
unknownSource?: string
) {
const assetURLs = useContext(AssetURLsStateContext);
if (assetURLs === undefined) {
throw new Error("useAssetURL must be used within a AssetURLsProvider");
@ -242,7 +223,7 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
}
function updateAssetURL() {
function increaseReferences(prevURLs) {
function increaseReferences(prevURLs: AssetURLs): AssetURLs {
return {
...prevURLs,
[assetId]: {
@ -252,13 +233,13 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
};
}
function createReference(prevURLs) {
function createReference(prevURLs: AssetURLs): AssetURLs {
return {
...prevURLs,
[assetId]: { url: null, id: assetId, references: 1 },
};
}
setAssetURLs((prevURLs) => {
setAssetURLs?.((prevURLs) => {
if (assetId in prevURLs) {
// Check if the asset url is already added and increase references
return increaseReferences(prevURLs);
@ -303,36 +284,29 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
return unknownSource;
}
/**
* @typedef FileData
* @property {string} file
* @property {"file"} type
* @property {string} thumbnail
* @property {string=} quality
* @property {Object.<string, string>=} resolutions
*/
type FileData = {
file: string;
type: "file";
thumbnail: string;
quality?: string;
resolutions?: Record<string, string>;
};
/**
* @typedef DefaultData
* @property {string} key
* @property {"default"} type
*/
type DefaultData = {
key: string;
type: "default";
};
/**
* Load a map or token into a URL taking into account a thumbnail and multiple resolutions
* @param {FileData|DefaultData} data
* @param {Object.<string, string>} defaultSources
* @param {string|undefined} unknownSource
* @param {boolean} thumbnail
* @returns {string|undefined}
*/
export function useDataURL(
data,
defaultSources,
unknownSource,
data: FileData | DefaultData,
defaultSources: Record<string, string>,
unknownSource: string | undefined,
thumbnail = false
) {
const [assetId, setAssetId] = useState();
const [assetId, setAssetId] = useState<string>();
useEffect(() => {
if (!data) {
@ -344,7 +318,11 @@ export function useDataURL(
} else {
if (thumbnail) {
setAssetId(data.thumbnail);
} else if (data.resolutions && data.quality !== "original") {
} else if (
data.resolutions &&
data.quality &&
data.quality !== "original"
) {
setAssetId(data.resolutions[data.quality]);
} else {
setAssetId(data.file);
@ -356,7 +334,7 @@ export function useDataURL(
}, [data, thumbnail]);
const assetURL = useAssetURL(
assetId,
assetId || "",
data?.type,
defaultSources,
unknownSource

View File

@ -2,9 +2,11 @@ import React, { useState, useEffect, useContext } from "react";
import FakeStorage from "../helpers/FakeStorage";
type AuthContext = { password: string; setPassword: React.Dispatch<any> };
type AuthContext = {
password: string;
setPassword: React.Dispatch<React.SetStateAction<string>>;
};
// TODO: check what default value we want here
const AuthContext = React.createContext<AuthContext | undefined>(undefined);
let storage: Storage | FakeStorage;

View File

@ -1,75 +0,0 @@
// eslint-disable-next-line no-unused-vars
import React, { useRef, ReactNode } from "react";
import {
DndContext,
useDndContext,
useDndMonitor,
// eslint-disable-next-line no-unused-vars
DragEndEvent,
} from "@dnd-kit/core";
/**
* Wrap a dnd-kit DndContext with a position monitor to get the
* active drag element on drag end
* TODO: use look into fixing this upstream
* Related: https://github.com/clauderic/dnd-kit/issues/238
*/
/**
* @typedef DragEndOverlayEvent
* @property {DOMRect} overlayNodeClientRect
*
* @typedef {DragEndEvent & DragEndOverlayEvent} DragEndWithOverlayProps
*/
/**
* @callback DragEndWithOverlayEvent
* @param {DragEndWithOverlayProps} props
*/
/**
* @typedef CustomDragProps
* @property {DragEndWithOverlayEvent=} onDragEnd
* @property {ReactNode} children
*/
/**
* @param {CustomDragProps} props
*/
function DragPositionMonitor({ children, onDragEnd }) {
const { overlayNode } = useDndContext();
const overlayNodeClientRectRef = useRef();
function handleDragMove() {
if (overlayNode?.nodeRef?.current) {
overlayNodeClientRectRef.current = overlayNode.nodeRef.current.getBoundingClientRect();
}
}
function handleDragEnd(props) {
onDragEnd &&
onDragEnd({
...props,
overlayNodeClientRect: overlayNodeClientRectRef.current,
});
}
useDndMonitor({ onDragEnd: handleDragEnd, onDragMove: handleDragMove });
return children;
}
/**
* TODO: Import Props interface from dnd-kit with conversion to Typescript
* @param {CustomDragProps} props
*/
function DragContext({ children, onDragEnd, ...props }) {
return (
<DndContext {...props}>
<DragPositionMonitor onDragEnd={onDragEnd}>
{children}
</DragPositionMonitor>
</DndContext>
);
}
export default DragContext;

View File

@ -0,0 +1,68 @@
import { useRef } from "react";
import {
DndContext,
useDndContext,
useDndMonitor,
DragEndEvent,
} from "@dnd-kit/core";
import { Props } from "@dnd-kit/core/dist/components/DndContext/DndContext";
/**
* Wrap a dnd-kit DndContext with a position monitor to get the
* active drag element on drag end
* TODO: use look into fixing this upstream
* Related: https://github.com/clauderic/dnd-kit/issues/238
*/
type DragEndWithOverlayEvent = {
overlayNodeClientRect?: DOMRect;
};
export type CustomDragEndEvent = DragEndWithOverlayEvent & DragEndEvent;
type CustomDragProps = {
onDragEnd?: (event: CustomDragEndEvent) => void;
;
};
function DragPositionMonitor({ onDragEnd }: CustomDragProps) {
const { overlayNode } = useDndContext();
const overlayNodeClientRectRef = useRef<DOMRect>();
function handleDragMove() {
if (overlayNode?.nodeRef?.current) {
overlayNodeClientRectRef.current =
overlayNode.nodeRef.current.getBoundingClientRect();
}
}
function handleDragEnd(props: DragEndEvent) {
onDragEnd &&
onDragEnd({
...props,
overlayNodeClientRect: overlayNodeClientRectRef.current,
});
}
useDndMonitor({ onDragEnd: handleDragEnd, onDragMove: handleDragMove });
return null;
}
/**
* @param {CustomDragProps} props
*/
function DragContext({
children,
onDragEnd,
...props
}: CustomDragProps & Props) {
return (
<DndContext {...props}>
<DragPositionMonitor onDragEnd={onDragEnd} />
{children}
</DndContext>
);
}
export default DragContext;

View File

@ -7,8 +7,40 @@ import { useKeyboard, useBlur } from "./KeyboardContext";
import { getGroupItems, groupsFromIds } from "../helpers/group";
import shortcuts from "../shortcuts";
import { Group, GroupContainer, GroupItem } from "../types/Group";
const GroupContext = React.createContext();
type GroupContext = {
groups: Group[];
activeGroups: Group[];
openGroupId: string | undefined;
openGroupItems: Group[];
filter: string | undefined;
filteredGroupItems: GroupItem[];
selectedGroupIds: string[];
selectMode: any;
onSelectModeChange: React.Dispatch<
React.SetStateAction<"single" | "multiple" | "range">
>;
onGroupOpen: (groupId: string) => void;
onGroupClose: () => void;
onGroupsChange: (
newGroups: Group[] | GroupItem[],
groupId: string | undefined
) => void;
onGroupSelect: (groupId: string | undefined) => void;
onFilterChange: React.Dispatch<React.SetStateAction<string | undefined>>;
};
const GroupContext = React.createContext<GroupContext | undefined>(undefined);
type GroupProviderProps = {
groups: Group[];
itemNames: Record<string, string>;
onGroupsChange: (groups: Group[]) => void;
onGroupsSelect: (groupIds: string[]) => void;
disabled: boolean;
children: React.ReactNode;
};
export function GroupProvider({
groups,
@ -17,16 +49,17 @@ export function GroupProvider({
onGroupsSelect,
disabled,
children,
}) {
const [selectedGroupIds, setSelectedGroupIds] = useState([]);
}: GroupProviderProps) {
const [selectedGroupIds, setSelectedGroupIds] = useState<string[]>([]);
// Either single, multiple or range
const [selectMode, setSelectMode] = useState("single");
const [selectMode, setSelectMode] =
useState<"single" | "multiple" | "range">("single");
/**
* Group Open
*/
const [openGroupId, setOpenGroupId] = useState();
const [openGroupItems, setOpenGroupItems] = useState([]);
const [openGroupId, setOpenGroupId] = useState<string>();
const [openGroupItems, setOpenGroupItems] = useState<Group[]>([]);
useEffect(() => {
if (openGroupId) {
const openGroups = groupsFromIds([openGroupId], groups);
@ -37,29 +70,29 @@ export function GroupProvider({
// Close group if we can't find it
// This can happen if it was deleted or all it's items were deleted
setOpenGroupItems([]);
setOpenGroupId();
setOpenGroupId(undefined);
}
} else {
setOpenGroupItems([]);
}
}, [openGroupId, groups]);
function handleGroupOpen(groupId) {
function handleGroupOpen(groupId: string) {
setSelectedGroupIds([]);
setOpenGroupId(groupId);
}
function handleGroupClose() {
setSelectedGroupIds([]);
setOpenGroupId();
setOpenGroupId(undefined);
}
/**
* Search
*/
const [filter, setFilter] = useState();
const [filteredGroupItems, setFilteredGroupItems] = useState([]);
const [fuse, setFuse] = useState();
const [filter, setFilter] = useState<string>();
const [filteredGroupItems, setFilteredGroupItems] = useState<GroupItem[]>([]);
const [fuse, setFuse] = useState<Fuse<GroupItem & { name: string }>>();
// Update search index when items change
useEffect(() => {
let items = [];
@ -76,10 +109,10 @@ export function GroupProvider({
// Perform search when search changes
useEffect(() => {
if (filter) {
if (filter && fuse) {
const query = fuse.search(filter);
setFilteredGroupItems(query.map((result) => result.item));
setOpenGroupId();
setOpenGroupId(undefined);
} else {
setFilteredGroupItems([]);
}
@ -96,23 +129,30 @@ export function GroupProvider({
: groups;
/**
* @param {Group[] | GroupItem[]} newGroups
* @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object
*/
function handleGroupsChange(newGroups, groupId) {
function handleGroupsChange(
newGroups: Group[] | GroupItem[],
groupId: string | undefined
) {
if (groupId) {
// If a group is specidifed then update that group with the new items
const groupIndex = groups.findIndex((group) => group.id === groupId);
let updatedGroups = cloneDeep(groups);
const group = updatedGroups[groupIndex];
updatedGroups[groupIndex] = { ...group, items: newGroups };
updatedGroups[groupIndex] = {
...group,
items: newGroups,
} as GroupContainer;
onGroupsChange(updatedGroups);
} else {
onGroupsChange(newGroups);
}
}
function handleGroupSelect(groupId) {
let groupIds = [];
function handleGroupSelect(groupId: string | undefined) {
let groupIds: string[] = [];
if (groupId) {
switch (selectMode) {
case "single":
@ -133,8 +173,8 @@ export function GroupProvider({
const lastIndex = activeGroups.findIndex(
(g) => g.id === selectedGroupIds[selectedGroupIds.length - 1]
);
let idsToAdd = [];
let idsToRemove = [];
let idsToAdd: string[] = [];
let idsToRemove: string[] = [];
const direction = currentIndex > lastIndex ? 1 : -1;
for (
let i = lastIndex + direction;
@ -166,7 +206,7 @@ export function GroupProvider({
/**
* Shortcuts
*/
function handleKeyDown(event) {
function handleKeyDown(event: React.KeyboardEvent) {
if (disabled) {
return;
}
@ -178,7 +218,7 @@ export function GroupProvider({
}
}
function handleKeyUp(event) {
function handleKeyUp(event: React.KeyboardEvent) {
if (disabled) {
return;
}

View File

@ -9,7 +9,9 @@ import { useLiveQuery } from "dexie-react-hooks";
import { useDatabase } from "./DatabaseContext";
import { Map, MapState, Note } from "../components/map/Map";
import { Map } from "../types/Map";
import { MapState } from "../types/MapState";
import { Note } from "../types/Note";
import { removeGroupsItems } from "../helpers/group";

View File

@ -6,19 +6,26 @@ import {
useSensor,
useSensors,
closestCenter,
RectEntry,
} from "@dnd-kit/core";
import DragContext from "./DragContext";
import DragContext, { CustomDragEndEvent } from "./DragContext";
import { DragStartEvent, DragOverEvent, ViewRect } from "@dnd-kit/core";
import { DragCancelEvent } from "@dnd-kit/core/dist/types";
import { useGroup } from "./GroupContext";
import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
import Vector2 from "../helpers/Vector2";
import usePreventSelect from "../hooks/usePreventSelect";
const TileDragIdContext = React.createContext();
const TileOverGroupIdContext = React.createContext();
const TileDragCursorContext = React.createContext();
const TileDragIdContext =
React.createContext<string | undefined | null>(undefined);
const TileOverGroupIdContext =
React.createContext<string | undefined | null>(undefined);
const TileDragCursorContext =
React.createContext<string | undefined | null>(undefined);
export const BASE_SORTABLE_ID = "__base__";
export const GROUP_SORTABLE_ID = "__group__";
@ -27,7 +34,7 @@ export const UNGROUP_ID = "__ungroup__";
export const ADD_TO_MAP_ID = "__add__";
// Custom rectIntersect that takes a point
function rectIntersection(rects, point) {
function rectIntersection(rects: RectEntry[], point: Vector2) {
for (let rect of rects) {
const [id, bounds] = rect;
if (
@ -44,13 +51,21 @@ function rectIntersection(rects, point) {
return null;
}
type TileDragProviderProps = {
onDragAdd?: (selectedGroupIds: string[], rect: DOMRect) => void;
onDragStart?: (event: DragStartEvent) => void;
onDragEnd?: (event: CustomDragEndEvent) => void;
onDragCancel?: (event: DragCancelEvent) => void;
children?: React.ReactNode;
};
export function TileDragProvider({
onDragAdd,
onDragStart,
onDragEnd,
onDragCancel,
children,
}) {
}: TileDragProviderProps) {
const {
groups,
activeGroups,
@ -71,23 +86,23 @@ export function TileDragProvider({
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
const [dragId, setDragId] = useState(null);
const [overId, setOverId] = useState(null);
const [dragId, setDragId] = useState<string | null>(null);
const [overId, setOverId] = useState<string | null>(null);
const [dragCursor, setDragCursor] = useState("pointer");
const [preventSelect, resumeSelect] = usePreventSelect();
const [overGroupId, setOverGroupId] = useState(null);
const [overGroupId, setOverGroupId] = useState<string | null>(null);
useEffect(() => {
setOverGroupId(
(overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9)) || null
);
}, [overId]);
function handleDragStart(event) {
const { active, over } = event;
function handleDragStart(event: DragStartEvent) {
const { active } = event;
setDragId(active.id);
setOverId(over?.id || null);
setOverId(null);
if (!selectedGroupIds.includes(active.id)) {
onGroupSelect(active.id);
}
@ -98,7 +113,7 @@ export function TileDragProvider({
preventSelect();
}
function handleDragOver(event) {
function handleDragOver(event: DragOverEvent) {
const { over } = event;
setOverId(over?.id || null);
@ -116,7 +131,7 @@ export function TileDragProvider({
}
}
function handleDragEnd(event) {
function handleDragEnd(event: CustomDragEndEvent) {
const { active, over, overlayNodeClientRect } = event;
setDragId(null);
@ -130,7 +145,7 @@ export function TileDragProvider({
selectedIndices = selectedIndices.sort((a, b) => a - b);
if (over.id.startsWith(GROUP_ID_PREFIX)) {
onGroupSelect();
onGroupSelect(undefined);
// Handle tile group
const overId = over.id.slice(9);
if (overId !== active.id) {
@ -143,10 +158,12 @@ export function TileDragProvider({
);
}
} else if (over.id === UNGROUP_ID) {
onGroupSelect();
// Handle tile ungroup
const newGroups = ungroup(groups, openGroupId, selectedIndices);
onGroupsChange(newGroups);
if (openGroupId) {
onGroupSelect(undefined);
// Handle tile ungroup
const newGroups = ungroup(groups, openGroupId, selectedIndices);
onGroupsChange(newGroups, undefined);
}
} else if (over.id === ADD_TO_MAP_ID) {
onDragAdd &&
overlayNodeClientRect &&
@ -168,7 +185,7 @@ export function TileDragProvider({
onDragEnd && onDragEnd(event);
}
function handleDragCancel(event) {
function handleDragCancel(event: DragCancelEvent) {
setDragId(null);
setOverId(null);
setDragCursor("pointer");
@ -178,7 +195,7 @@ export function TileDragProvider({
onDragCancel && onDragCancel(event);
}
function customCollisionDetection(rects, rect) {
function customCollisionDetection(rects: RectEntry[], rect: ViewRect) {
const rectCenter = {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,

View File

@ -1,12 +1,10 @@
import React, { useEffect, useState, useContext } from "react";
import { useDatabase } from "./DatabaseContext";
/**
* @type {React.Context<string|undefined>}
*/
const UserIdContext = React.createContext();
export function UserIdProvider({ children }) {
const UserIdContext = React.createContext<string | undefined>(undefined);
export function UserIdProvider({ children }: { children: React.ReactNode }) {
const { database, databaseStatus } = useDatabase();
const [userId, setUserId] = useState();
@ -15,9 +13,11 @@ export function UserIdProvider({ children }) {
return;
}
async function loadUserId() {
const storedUserId = await database.table("user").get("userId");
if (storedUserId) {
setUserId(storedUserId.value);
if (database) {
const storedUserId = await database.table("user").get("userId");
if (storedUserId) {
setUserId(storedUserId.value);
}
}
}

View File

@ -1,7 +1,10 @@
import { Vector3 } from "@babylonjs/core/Maths/math";
import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader";
import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial";
import { PhysicsImpostor, PhysicsImpostorParameters } from "@babylonjs/core/Physics/physicsImpostor";
import {
PhysicsImpostor,
PhysicsImpostorParameters,
} from "@babylonjs/core/Physics/physicsImpostor";
import d4Source from "./shared/d4.glb";
import d6Source from "./shared/d6.glb";
@ -13,7 +16,15 @@ import d100Source from "./shared/d100.glb";
import { lerp } from "../helpers/shared";
import { importTextureAsync } from "../helpers/babylon";
import { BaseTexture, InstancedMesh, Material, Mesh, Scene, Texture } from "@babylonjs/core";
import {
BaseTexture,
InstancedMesh,
Material,
Mesh,
Scene,
Texture,
} from "@babylonjs/core";
import { DiceType } from "../types/Dice";
const minDiceRollSpeed = 600;
const maxDiceRollSpeed = 800;
@ -21,10 +32,16 @@ const maxDiceRollSpeed = 800;
class Dice {
static instanceCount = 0;
static async loadMeshes(material: Material, scene: Scene, sourceOverrides?: any): Promise<Record<string, Mesh>> {
static async loadMeshes(
material: Material,
scene: Scene,
sourceOverrides?: any
): Promise<Record<string, Mesh>> {
let meshes: any = {};
const addToMeshes = async (type: string | number, defaultSource: any) => {
let source: string = sourceOverrides ? sourceOverrides[type] : defaultSource;
let source: string = sourceOverrides
? sourceOverrides[type]
: defaultSource;
const mesh = await this.loadMesh(source, material, scene);
meshes[type] = mesh;
};
@ -54,7 +71,11 @@ class Dice {
static async loadMaterial(materialName: string, textures: any, scene: Scene) {
let pbr = new PBRMaterial(materialName, scene);
let [albedo, normal, metalRoughness]: [albedo: BaseTexture, normal: Texture, metalRoughness: Texture] = await Promise.all([
let [albedo, normal, metalRoughness]: [
albedo: BaseTexture,
normal: Texture,
metalRoughness: Texture
] = await Promise.all([
importTextureAsync(textures.albedo),
importTextureAsync(textures.normal),
importTextureAsync(textures.metalRoughness),
@ -69,7 +90,12 @@ class Dice {
return pbr;
}
static createInstanceFromMesh(mesh: Mesh, name: string, physicalProperties: PhysicsImpostorParameters, scene: Scene) {
static createInstanceFromMesh(
mesh: Mesh,
name: string,
physicalProperties: PhysicsImpostorParameters,
scene: Scene
) {
let instance = mesh.createInstance(name);
instance.position = mesh.position;
for (let child of mesh.getChildTransformNodes()) {
@ -77,7 +103,7 @@ class Dice {
const locator: any = child.clone(child.name, instance);
// TODO: handle possible null value
if (!locator) {
throw Error
throw Error;
}
locator.setAbsolutePosition(child.getAbsolutePosition());
locator.name = child.name;
@ -114,7 +140,7 @@ class Dice {
}
}
static roll(instance: Mesh) {
static roll(instance: InstancedMesh) {
instance.physicsImpostor?.setLinearVelocity(Vector3.Zero());
instance.physicsImpostor?.setAngularVelocity(Vector3.Zero());
@ -156,7 +182,11 @@ class Dice {
);
}
static createInstanceMesh(mesh: Mesh, physicalProperties: PhysicsImpostorParameters, scene: Scene): InstancedMesh {
static createInstanceMesh(
mesh: Mesh,
physicalProperties: PhysicsImpostorParameters,
scene: Scene
): InstancedMesh {
this.instanceCount++;
return this.createInstanceFromMesh(
@ -166,6 +196,14 @@ class Dice {
scene
);
}
static async load(scene: Scene) {
throw new Error(`Unable to load ${scene}`);
}
static createInstance(diceType: DiceType, scene: Scene): InstancedMesh {
throw new Error(`No instance available for ${diceType} in ${scene}`);
}
}
export default Dice;

View File

@ -19,7 +19,9 @@ import GlassPreview from "./glass/preview.png";
import GemstonePreview from "./gemstone/preview.png";
import Dice from "./Dice";
type DiceClasses = Record<string, Dice>;
import { DefaultDice } from "../types/Dice";
type DiceClasses = Record<string, typeof Dice>;
export const diceClasses: DiceClasses = {
galaxy: GalaxyDice,
@ -45,7 +47,7 @@ export const dicePreviews: DicePreview = {
gemstone: GemstonePreview,
};
export const dice = Object.keys(diceClasses).map((key) => ({
export const dice: DefaultDice[] = Object.keys(diceClasses).map((key) => ({
key,
name: Case.capital(key),
class: diceClasses[key],

View File

@ -12,6 +12,7 @@ import d12Source from "./d12.glb";
import d20Source from "./d20.glb";
import d100Source from "./d100.glb";
import { Material, Mesh, Scene } from "@babylonjs/core";
import { DiceType } from "../../types/Dice";
const sourceOverrides = {
d4: d4Source,
@ -24,7 +25,7 @@ const sourceOverrides = {
};
class WalnutDice extends Dice {
static meshes: Record<string, Mesh>;
static meshes: Record<DiceType, Mesh>;
static material: Material;
static getDicePhysicalProperties(diceType: string) {
@ -49,7 +50,7 @@ class WalnutDice extends Dice {
}
}
static createInstance(diceType: string, scene: Scene) {
static createInstance(diceType: DiceType, scene: Scene) {
if (!this.material || !this.meshes) {
throw Error("Dice not loaded, call load before creating an instance");
}

View File

@ -6,12 +6,12 @@ import {
} from "./shared";
export type BoundingBox = {
min: Vector2,
max: Vector2,
width: number,
height: number,
center: Vector2
}
min: Vector2;
max: Vector2;
width: number;
height: number;
center: Vector2;
};
/**
* Vector class with x, y and static helper methods
@ -287,7 +287,12 @@ class Vector2 {
* @param {Vector2} C End of the curve
* @returns {Object} The distance to and the closest point on the curve
*/
static distanceToQuadraticBezier(pos: Vector2, A: Vector2, B: Vector2, C: Vector2): Object {
static distanceToQuadraticBezier(
pos: Vector2,
A: Vector2,
B: Vector2,
C: Vector2
): Object {
let distance = 0;
let point = { x: pos.x, y: pos.y };
@ -514,7 +519,10 @@ class Vector2 {
* @param {("counterClockwise"|"clockwise")=} direction Direction to rotate the vector
* @returns {Vector2}
*/
static rotate90(p: Vector2, direction: "counterClockwise" | "clockwise" = "clockwise"): Vector2 {
static rotate90(
p: Vector2,
direction: "counterClockwise" | "clockwise" = "clockwise"
): Vector2 {
if (direction === "clockwise") {
return { x: p.y, y: -p.x };
} else {
@ -527,7 +535,7 @@ class Vector2 {
* @param {Vector2[]} points
* @returns {Vector2}
*/
static centroid(points) {
static centroid(points: Vector2[]): Vector2 {
let center = { x: 0, y: 0 };
for (let point of points) {
center.x += point.x;
@ -544,7 +552,7 @@ class Vector2 {
* @param {Vector2[]} points
* @returns {boolean}
*/
static rectangular(points) {
static rectangular(points: Vector2[]): boolean {
if (points.length !== 4) {
return false;
}
@ -567,7 +575,7 @@ class Vector2 {
* @param {Vector2[]} points
* @returns {boolean}
*/
static circular(points, threshold = 0.1) {
static circular(points: Vector2[], threshold = 0.1): boolean {
const centroid = this.centroid(points);
let distances = [];
for (let point of points) {

View File

@ -1,4 +1,5 @@
import { Vector3 } from "@babylonjs/core/Maths/math";
import { DiceRoll } from "../types/Dice";
/**
* Find the number facing up on a mesh instance of a dice
@ -42,7 +43,7 @@ export function getDiceRoll(dice: any) {
return { type: dice.type, roll: number };
}
export function getDiceRollTotal(diceRolls: []) {
export function getDiceRollTotal(diceRolls: DiceRoll[]) {
return diceRolls.reduce((accumulator: number, dice: any) => {
if (dice.roll === "unknown") {
return accumulator;

View File

@ -2,136 +2,19 @@ import simplify from "simplify-js";
import polygonClipping, { Geom, Polygon, Ring } from "polygon-clipping";
import Vector2, { BoundingBox } from "./Vector2";
import Size from "./Size"
import Size from "./Size";
import { toDegrees } from "./shared";
import { Grid, getNearestCellCoordinates, getCellLocation } from "./grid";
import { getNearestCellCoordinates, getCellLocation } from "./grid";
/**
* @typedef PointsData
* @property {Vector2[]} points
*/
type PointsData = {
points: Vector2[]
}
/**
* @typedef RectData
* @property {number} x
* @property {number} y
* @property {number} width
* @property {number} height
*/
type RectData = {
x: number,
y: number,
width: number,
height: number
}
/**
* @typedef CircleData
* @property {number} x
* @property {number} y
* @property {number} radius
*/
type CircleData = {
x: number,
y: number,
radius: number
}
/**
* @typedef FogData
* @property {Vector2[]} points
* @property {Vector2[][]} holes
*/
type FogData = {
points: Vector2[]
holes: Vector2[][]
}
/**
* @typedef {(PointsData|RectData|CircleData)} ShapeData
*/
type ShapeData = PointsData | RectData | CircleData
/**
* @typedef {("line"|"rectangle"|"circle"|"triangle")} ShapeType
*/
type ShapeType = "line" | "rectangle" | "circle" | "triangle"
/**
* @typedef {("fill"|"stroke")} PathType
*/
type PathType = "fill" | "stroke"
/**
* @typedef Path
* @property {boolean} blend
* @property {string} color
* @property {PointsData} data
* @property {string} id
* @property {PathType} pathType
* @property {number} strokeWidth
* @property {"path"} type
*/
export type Path = {
blend: boolean,
color: string,
data: PointsData,
id: string,
pathType: PathType,
strokeWidth: number,
type: "path"
}
/**
* @typedef Shape
* @property {boolean} blend
* @property {string} color
* @property {ShapeData} data
* @property {string} id
* @property {ShapeType} shapeType
* @property {number} strokeWidth
* @property {"shape"} type
*/
export type Shape = {
blend: boolean,
color: string,
data: ShapeData,
id: string,
shapeType: ShapeType,
strokeWidth: number,
type: "shape"
}
/**
* @typedef Fog
* @property {string} color
* @property {FogData} data
* @property {string} id
* @property {number} strokeWidth
* @property {"fog"} type
* @property {boolean} visible
*/
export type Fog = {
color: string,
data: FogData,
id: string,
strokeWidth: number,
type: "fog",
visible: boolean
}
import {
ShapeType,
ShapeData,
PointsData,
RectData,
CircleData,
} from "../types/Drawing";
import { Fog } from "../types/Fog";
import { Grid } from "../types/Grid";
/**
*
@ -139,24 +22,26 @@ export type Fog = {
* @param {Vector2} brushPosition
* @returns {ShapeData}
*/
export function getDefaultShapeData(type: ShapeType, brushPosition: Vector2): ShapeData | undefined{
// TODO: handle undefined if no type found
export function getDefaultShapeData(
type: ShapeType,
brushPosition: Vector2
): ShapeData {
if (type === "line") {
return {
points: [
{ x: brushPosition.x, y: brushPosition.y },
{ x: brushPosition.x, y: brushPosition.y },
],
} as PointsData;
};
} else if (type === "circle") {
return { x: brushPosition.x, y: brushPosition.y, radius: 0 } as CircleData;
return { x: brushPosition.x, y: brushPosition.y, radius: 0 };
} else if (type === "rectangle") {
return {
x: brushPosition.x,
y: brushPosition.y,
width: 0,
height: 0,
} as RectData;
};
} else if (type === "triangle") {
return {
points: [
@ -164,7 +49,9 @@ export function getDefaultShapeData(type: ShapeType, brushPosition: Vector2): Sh
{ x: brushPosition.x, y: brushPosition.y },
{ x: brushPosition.x, y: brushPosition.y },
],
} as PointsData;
};
} else {
throw new Error("Shape type not implemented");
}
}
@ -197,15 +84,14 @@ export function getUpdatedShapeData(
gridCellNormalizedSize: Vector2,
mapWidth: number,
mapHeight: number
): ShapeData | undefined {
// TODO: handle undefined type
): ShapeData {
if (type === "line") {
data = data as PointsData;
return {
points: [data.points[0], { x: brushPosition.x, y: brushPosition.y }],
} as PointsData;
} else if (type === "circle") {
data = data as CircleData;
data = data as CircleData;
const gridRatio = getGridCellRatio(gridCellNormalizedSize);
const dif = Vector2.subtract(brushPosition, {
x: data.x,
@ -254,6 +140,8 @@ export function getUpdatedShapeData(
Vector2.add(Vector2.multiply(rightDirNorm, sideLength), points[0]),
],
};
} else {
throw new Error("Shape type not implemented");
}
}
@ -262,7 +150,10 @@ export function getUpdatedShapeData(
* @param {Vector2[]} points
* @param {number} tolerance
*/
export function simplifyPoints(points: Vector2[], tolerance: number): Vector2[] {
export function simplifyPoints(
points: Vector2[],
tolerance: number
): Vector2[] {
return simplify(points, tolerance);
}
@ -272,7 +163,10 @@ export function simplifyPoints(points: Vector2[], tolerance: number): Vector2[]
* @param {boolean} ignoreHidden
* @returns {Fog[]}
*/
export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog[] {
export function mergeFogShapes(
shapes: Fog[],
ignoreHidden: boolean = true
): Fog[] {
if (shapes.length === 0) {
return shapes;
}
@ -283,7 +177,7 @@ export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog
}
const shapePoints: Ring = shape.data.points.map(({ x, y }) => [x, y]);
const shapeHoles: Polygon = shape.data.holes.map((hole) =>
hole.map(({ x, y }: { x: number, y: number }) => [x, y])
hole.map(({ x, y }: { x: number; y: number }) => [x, y])
);
let shapeGeom: Geom = [[shapePoints, ...shapeHoles]];
geometries.push(shapeGeom);
@ -315,7 +209,7 @@ export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog
points: union[i][0].map(([x, y]) => ({ x, y })),
holes,
},
type: "fog"
type: "fog",
});
}
return merged;
@ -330,7 +224,10 @@ export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog
* @param {boolean} maxPoints Max amount of points per shape to get bounds for
* @returns {Vector2.BoundingBox[]}
*/
export function getFogShapesBoundingBoxes(shapes: Fog[], maxPoints = 0): BoundingBox[] {
export function getFogShapesBoundingBoxes(
shapes: Fog[],
maxPoints = 0
): BoundingBox[] {
let boxes = [];
for (let shape of shapes) {
if (maxPoints > 0 && shape.data.points.length > maxPoints) {
@ -361,11 +258,11 @@ export function getFogShapesBoundingBoxes(shapes: Fog[], maxPoints = 0): Boundin
*/
type Guide = {
start: Vector2,
end: Vector2,
orientation: "horizontal" | "vertical",
distance: number
}
start: Vector2;
end: Vector2;
orientation: "horizontal" | "vertical";
distance: number;
};
/**
* @param {Vector2} brushPosition Brush position in pixels
@ -382,7 +279,7 @@ export function getGuidesFromGridCell(
grid: Grid,
gridCellSize: Size,
gridOffset: Vector2,
gridCellOffset: Vector2,
gridCellOffset: Vector2,
snappingSensitivity: number,
mapSize: Vector2
): Guide[] {
@ -500,7 +397,10 @@ export function getGuidesFromBoundingBoxes(
* @param {Guide[]} guides
* @returns {Guide[]}
*/
export function findBestGuides(brushPosition: Vector2, guides: Guide[]): Guide[] {
export function findBestGuides(
brushPosition: Vector2,
guides: Guide[]
): Guide[] {
let bestGuides: Guide[] = [];
let verticalGuide = guides
.filter((guide) => guide.orientation === "vertical")

View File

@ -8,42 +8,6 @@ import { logError } from "./logging";
const SQRT3 = 1.73205;
const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented");
/**
* @typedef GridInset
* @property {Vector2} topLeft Top left position of the inset
* @property {Vector2} bottomRight Bottom right position of the inset
*/
type GridInset = {
topLeft: Vector2,
bottomRight: Vector2
}
/**
* @typedef GridMeasurement
* @property {("chebyshev"|"alternating"|"euclidean"|"manhattan")} type
* @property {string} scale
*/
type GridMeasurement ={
type: ("chebyshev"|"alternating"|"euclidean"|"manhattan")
scale: string
}
/**
* @typedef Grid
* @property {GridInset} inset The inset of the grid from the map
* @property {Vector2} size The number of columns and rows of the grid as `x` and `y`
* @property {("square"|"hexVertical"|"hexHorizontal")} type
* @property {GridMeasurement} measurement
*/
export type Grid = {
inset?: GridInset,
size: Vector2,
type: ("square"|"hexVertical"|"hexHorizontal"),
measurement?: GridMeasurement
}
/**
* Gets the size of a grid in pixels taking into account the inset
* @param {Grid} grid
@ -51,7 +15,11 @@ export type Grid = {
* @param {number} baseHeight Height of the grid in pixels before inset
* @returns {Size}
*/
export function getGridPixelSize(grid: Required<Grid>, baseWidth: number, baseHeight: number): Size {
export function getGridPixelSize(
grid: Required<Grid>,
baseWidth: number,
baseHeight: number
): Size {
const width = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * baseWidth;
const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight;
return new Size(width, height);
@ -64,7 +32,11 @@ export function getGridPixelSize(grid: Required<Grid>, baseWidth: number, baseHe
* @param {number} gridHeight Height of the grid in pixels after inset
* @returns {Size}
*/
export function getCellPixelSize(grid: Grid, gridWidth: number, gridHeight: number): Size {
export function getCellPixelSize(
grid: Grid,
gridWidth: number,
gridHeight: number
): Size {
if (grid.size.x === 0 || grid.size.y === 0) {
return new Size(0, 0);
}
@ -91,7 +63,12 @@ export function getCellPixelSize(grid: Grid, gridWidth: number, gridHeight: numb
* @param {Size} cellSize Cell size in pixels
* @returns {Vector2}
*/
export function getCellLocation(grid: Grid, col: number, row: number, cellSize: Size): Vector2 {
export function getCellLocation(
grid: Grid,
col: number,
row: number,
cellSize: Size
): Vector2 {
switch (grid.type) {
case "square":
return {
@ -121,7 +98,12 @@ export function getCellLocation(grid: Grid, col: number, row: number, cellSize:
* @param {Size} cellSize Cell size in pixels
* @returns {Vector2}
*/
export function getNearestCellCoordinates(grid: Grid, x: number, y: number, cellSize: Size): Vector2 {
export function getNearestCellCoordinates(
grid: Grid,
x: number,
y: number,
cellSize: Size
): Vector2 {
switch (grid.type) {
case "square":
return Vector2.divide(Vector2.floorTo({ x, y }, cellSize), cellSize);
@ -151,7 +133,12 @@ export function getNearestCellCoordinates(grid: Grid, x: number, y: number, cell
* @param {Size} cellSize Cell size in pixels
* @returns {Vector2[]}
*/
export function getCellCorners(grid: Grid, x: number, y: number, cellSize: Size): Vector2[] {
export function getCellCorners(
grid: Grid,
x: number,
y: number,
cellSize: Size
): Vector2[] {
const position = new Vector2(x, y);
switch (grid.type) {
case "square":
@ -193,7 +180,7 @@ export function getCellCorners(grid: Grid, x: number, y: number, cellSize: Size)
* @param {number} gridWidth Width of the grid in pixels after inset
* @returns {number}
*/
function getGridHeightFromWidth(grid: Grid, gridWidth: number): number{
function getGridHeightFromWidth(grid: Grid, gridWidth: number): number {
switch (grid.type) {
case "square":
return (grid.size.y * gridWidth) / grid.size.x;
@ -215,7 +202,11 @@ function getGridHeightFromWidth(grid: Grid, gridWidth: number): number{
* @param {number} mapHeight Height of the map in pixels before inset
* @returns {GridInset}
*/
export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset {
export function getGridDefaultInset(
grid: Grid,
mapWidth: number,
mapHeight: number
): GridInset {
// Max the width of the inset and figure out the resulting height value
const insetHeightNorm = getGridHeightFromWidth(grid, mapWidth) / mapHeight;
return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: insetHeightNorm } };
@ -228,7 +219,11 @@ export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: num
* @param {number} mapHeight Height of the map in pixels before inset
* @returns {GridInset}
*/
export function getGridUpdatedInset(grid: Required<Grid>, mapWidth: number, mapHeight: number): GridInset {
export function getGridUpdatedInset(
grid: Required<Grid>,
mapWidth: number,
mapHeight: number
): GridInset {
let inset = {
topLeft: { ...grid.inset.topLeft },
bottomRight: { ...grid.inset.bottomRight },
@ -263,7 +258,10 @@ export function getGridMaxZoom(grid: Grid): number {
* @param {("hexVertical"|"hexHorizontal")} type
* @returns {Vector2}
*/
export function hexCubeToOffset(cube: Vector3, type: ("hexVertical"|"hexHorizontal")) {
export function hexCubeToOffset(
cube: Vector3,
type: "hexVertical" | "hexHorizontal"
) {
if (type === "hexVertical") {
const x = cube.x + (cube.z + (cube.z & 1)) / 2;
const y = cube.z;
@ -280,7 +278,10 @@ export function hexCubeToOffset(cube: Vector3, type: ("hexVertical"|"hexHorizont
* @param {("hexVertical"|"hexHorizontal")} type
* @returns {Vector3}
*/
export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizontal")) {
export function hexOffsetToCube(
offset: Vector2,
type: "hexVertical" | "hexHorizontal"
) {
if (type === "hexVertical") {
const x = offset.x - (offset.y + (offset.y & 1)) / 2;
const z = offset.y;
@ -301,7 +302,12 @@ export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizo
* @param {Vector2} b
* @param {Size} cellSize
*/
export function gridDistance(grid: Required<Grid>, a: Vector2, b: Vector2, cellSize: Size) {
export function gridDistance(
grid: Required<Grid>,
a: Vector2,
b: Vector2,
cellSize: Size
) {
// Get grid coordinates
const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize);
const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize);
@ -315,7 +321,9 @@ export function gridDistance(grid: Required<Grid>, a: Vector2, b: Vector2, cellS
const min: any = Vector2.min(delta);
return max - min + Math.floor(1.5 * min);
} else if (grid.measurement.type === "euclidean") {
return Vector2.magnitude(Vector2.divide(Vector2.subtract(a, b), cellSize));
return Vector2.magnitude(
Vector2.divide(Vector2.subtract(a, b), cellSize)
);
} else if (grid.measurement.type === "manhattan") {
return Math.abs(aCoord.x - bCoord.x) + Math.abs(aCoord.y - bCoord.y);
}
@ -331,24 +339,13 @@ export function gridDistance(grid: Required<Grid>, a: Vector2, b: Vector2, cellS
2
);
} else if (grid.measurement.type === "euclidean") {
return Vector2.magnitude(Vector2.divide(Vector2.subtract(a, b), cellSize));
return Vector2.magnitude(
Vector2.divide(Vector2.subtract(a, b), cellSize)
);
}
}
}
/**
* @typedef GridScale
* @property {number} multiplier The number multiplier of the scale
* @property {string} unit The unit of the scale
* @property {number} digits The precision of the scale
*/
type GridScale = {
multiplier: number,
unit: string,
digits: number
}
/**
* Parse a string representation of scale e.g. 5ft into a `GridScale`
* @param {string} scale
@ -441,7 +438,10 @@ export function gridSizeVaild(x: number, y: number): boolean {
* @param {number[]} candidates
* @returns {Vector2 | null}
*/
function gridSizeHeuristic(image: CanvasImageSource, candidates: number[]): Vector2 | null {
function gridSizeHeuristic(
image: CanvasImageSource,
candidates: number[]
): Vector2 | null {
// TODO: check type for Image and CanvasSourceImage
const width: any = image.width;
const height: any = image.height;
@ -474,7 +474,10 @@ function gridSizeHeuristic(image: CanvasImageSource, candidates: number[]): Vect
* @param {number[]} candidates
* @returns {Vector2 | null}
*/
async function gridSizeML(image: CanvasImageSource, candidates: number[]): Promise<Vector2 | null> {
async function gridSizeML(
image: CanvasImageSource,
candidates: number[]
): Promise<Vector2 | null> {
// TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match
const width: any = image.width;
const height: any = image.height;

View File

@ -2,34 +2,14 @@ import { v4 as uuid } from "uuid";
import cloneDeep from "lodash.clonedeep";
import { keyBy } from "./shared";
/**
* @typedef GroupItem
* @property {string} id
* @property {"item"} type
*/
/**
* @typedef GroupContainer
* @property {string} id
* @property {"group"} type
* @property {GroupItem[]} items
* @property {string} name
*/
/**
* @typedef {GroupItem|GroupContainer} Group
*/
import { Group, GroupContainer, GroupItem } from "../types/Group";
/**
* Transform an array of group ids to their groups
* @param {string[]} groupIds
* @param {Group[]} groups
* @return {Group[[]}
*/
export function groupsFromIds(groupIds, groups) {
export function groupsFromIds(groupIds: string[], groups: Group[]): Group[] {
const groupsByIds = keyBy(groups, "id");
const filteredGroups = [];
const filteredGroups: Group[] = [];
for (let groupId of groupIds) {
if (groupId in groupsByIds) {
filteredGroups.push(groupsByIds[groupId]);
@ -40,10 +20,8 @@ export function groupsFromIds(groupIds, groups) {
/**
* Get all items from a group including all sub groups
* @param {Group} group
* @return {GroupItem[]}
*/
export function getGroupItems(group) {
export function getGroupItems(group: Group): GroupItem[] {
if (group.type === "group") {
let groups = [];
for (let item of group.items) {
@ -57,14 +35,14 @@ export function getGroupItems(group) {
/**
* Transform an array of groups into their assosiated items
* @param {Group[]} groups
* @param {any[]} allItems
* @param {string} itemKey
* @returns {any[]}
*/
export function itemsFromGroups(groups, allItems, itemKey = "id") {
export function itemsFromGroups<Item>(
groups: Group[],
allItems: Item[],
itemKey = "id"
): Item[] {
const allItemsById = keyBy(allItems, itemKey);
const groupedItems = [];
const groupedItems: Item[] = [];
for (let group of groups) {
const groupItems = getGroupItems(group);
@ -76,47 +54,52 @@ export function itemsFromGroups(groups, allItems, itemKey = "id") {
}
/**
* Combine two groups
* @param {Group} a
* @param {Group} b
* @returns {GroupContainer}
* Combine a group and a group item
*/
export function combineGroups(a, b) {
if (a.type === "item") {
return {
id: uuid(),
type: "group",
items: [a, b],
name: "",
};
}
if (a.type === "group") {
return {
id: a.id,
type: "group",
items: [...a.items, b],
name: a.name,
};
export function combineGroups(a: Group, b: Group): GroupContainer {
switch (a.type) {
case "item":
if (b.type !== "item") {
throw new Error("Unable to combine two GroupContainers");
}
return {
id: uuid(),
type: "group",
items: [a, b],
name: "",
};
case "group":
if (b.type !== "item") {
throw new Error("Unable to combine two GroupContainers");
}
return {
id: a.id,
type: "group",
items: [...a.items, b],
name: a.name,
};
default:
throw new Error("Group type not implemented");
}
}
/**
* Immutably move group at indices `indices` into group at index `into`
* @param {Group[]} groups
* @param {number} into
* @param {number[]} indices
* @returns {Group[]}
*/
export function moveGroupsInto(groups, into, indices) {
export function moveGroupsInto(
groups: Group[],
into: number,
indices: number[]
): Group[] {
const newGroups = cloneDeep(groups);
const intoGroup = newGroups[into];
let fromGroups = [];
let fromGroups: Group[] = [];
for (let i of indices) {
fromGroups.push(newGroups[i]);
}
let combined = intoGroup;
let combined: Group = intoGroup;
for (let fromGroup of fromGroups) {
combined = combineGroups(combined, fromGroup);
}
@ -133,12 +116,12 @@ export function moveGroupsInto(groups, into, indices) {
/**
* Immutably move group at indices `indices` to index `to`
* @param {Group[]} groups
* @param {number} into
* @param {number[]} indices
* @returns {Group[]}
*/
export function moveGroups(groups, to, indices) {
export function moveGroups(
groups: Group[],
to: number,
indices: number[]
): Group[] {
const newGroups = cloneDeep(groups);
let fromGroups = [];
@ -160,28 +143,31 @@ export function moveGroups(groups, to, indices) {
/**
* Move items from a sub group to the start of the base group
* @param {Group[]} groups
* @param {string} fromId The id of the group to move from
* @param {number[]} indices The indices of the items in the group
* @param fromId The id of the group to move from
* @param indices The indices of the items in the group
*/
export function ungroup(groups, fromId, indices) {
export function ungroup(groups: Group[], fromId: string, indices: number[]) {
const newGroups = cloneDeep(groups);
let fromIndex = newGroups.findIndex((group) => group.id === fromId);
const fromIndex = newGroups.findIndex((group) => group.id === fromId);
const from = newGroups[fromIndex];
if (from.type !== "group") {
throw new Error(`Unable to ungroup ${fromId}, not a group`);
}
let items = [];
let items: GroupItem[] = [];
for (let i of indices) {
items.push(newGroups[fromIndex].items[i]);
items.push(from.items[i]);
}
// Remove items from previous group
for (let item of items) {
const i = newGroups[fromIndex].items.findIndex((el) => el.id === item.id);
newGroups[fromIndex].items.splice(i, 1);
const i = from.items.findIndex((el) => el.id === item.id);
from.items.splice(i, 1);
}
// If we have no more items in the group delete it
if (newGroups[fromIndex].items.length === 0) {
if (from.items.length === 0) {
newGroups.splice(fromIndex, 1);
}
@ -193,11 +179,8 @@ export function ungroup(groups, fromId, indices) {
/**
* Recursively find a group within a group array
* @param {Group[]} groups
* @param {string} groupId
* @returns {Group}
*/
export function findGroup(groups, groupId) {
export function findGroup(groups: Group[], groupId: string): Group | undefined {
for (let group of groups) {
if (group.id === groupId) {
return group;
@ -213,11 +196,9 @@ export function findGroup(groups, groupId) {
/**
* Transform and item array to a record of item ids to item names
* @param {any[]} items
* @param {string=} itemKey
*/
export function getItemNames(items, itemKey = "id") {
let names = {};
export function getItemNames(items: any[], itemKey: string = "id") {
let names: Record<string, string> = {};
for (let item of items) {
names[item[itemKey]] = item.name;
}
@ -226,15 +207,20 @@ export function getItemNames(items, itemKey = "id") {
/**
* Immutably rename a group
* @param {Group[]} groups
* @param {string} groupId
* @param {string} newName
*/
export function renameGroup(groups, groupId, newName) {
export function renameGroup(
groups: Group[],
groupId: string,
newName: string
): Group[] {
let newGroups = cloneDeep(groups);
const groupIndex = newGroups.findIndex((group) => group.id === groupId);
const group = groups[groupIndex];
if (group.type !== "group") {
throw new Error(`Unable to rename group ${groupId}, not of type group`);
}
if (groupIndex >= 0) {
newGroups[groupIndex].name = newName;
group.name = newName;
}
return newGroups;
}
@ -244,7 +230,7 @@ export function renameGroup(groups, groupId, newName) {
* @param {Group[]} groups
* @param {string[]} itemIds
*/
export function removeGroupsItems(groups, itemIds) {
export function removeGroupsItems(groups: Group[], itemIds: string[]): Group[] {
let newGroups = cloneDeep(groups);
for (let i = newGroups.length - 1; i >= 0; i--) {
@ -258,11 +244,11 @@ export function removeGroupsItems(groups, itemIds) {
for (let j = items.length - 1; j >= 0; j--) {
const item = items[j];
if (itemIds.includes(item.id)) {
newGroups[i].items.splice(j, 1);
group.items.splice(j, 1);
}
}
// Remove group if no items are left
if (newGroups[i].items.length === 0) {
if (group.items.length === 0) {
newGroups.splice(i, 1);
}
}

View File

@ -3,13 +3,15 @@ import imageOutline from "image-outline";
import blobToBuffer from "./blobToBuffer";
import Vector2 from "./Vector2";
import { Outline } from "../types/Outline";
const lightnessDetectionOffset = 0.1;
/**
* @param {HTMLImageElement} image
* @returns {boolean} True is the image is light
*/
export function getImageLightness(image: HTMLImageElement) {
export function getImageLightness(image: HTMLImageElement): boolean {
const width = image.width;
const height = image.height;
let canvas = document.createElement("canvas");
@ -17,8 +19,7 @@ export function getImageLightness(image: HTMLImageElement) {
canvas.height = height;
let context = canvas.getContext("2d");
if (!context) {
// TODO: handle if context is null
return;
return false;
}
context.drawImage(image, 0, 0);
@ -45,18 +46,12 @@ export function getImageLightness(image: HTMLImageElement) {
return norm + lightnessDetectionOffset >= 0;
}
/**
* @typedef CanvasImage
* @property {Blob|null} blob The blob of the resized image, `null` if the image was unable to be resized to that dimension
* @property {number} width
* @property {number} height
*/
type CanvasImage = {
blob: Blob | null,
width: number,
height: number
}
file: Uint8Array;
width: number;
height: number;
mime: string;
};
/**
* @param {HTMLCanvasElement} canvas
@ -64,11 +59,25 @@ type CanvasImage = {
* @param {number} quality
* @returns {Promise<CanvasImage>}
*/
export async function canvasToImage(canvas: HTMLCanvasElement, type: string, quality: number): Promise<CanvasImage> {
export async function canvasToImage(
canvas: HTMLCanvasElement,
type: string,
quality: number
): Promise<CanvasImage | undefined> {
return new Promise((resolve) => {
canvas.toBlob(
(blob) => {
resolve({ blob, width: canvas.width, height: canvas.height });
async (blob) => {
if (blob) {
const file = await blobToBuffer(blob);
resolve({
file,
width: canvas.width,
height: canvas.height,
mime: type,
});
} else {
resolve(undefined);
}
},
type,
quality
@ -81,9 +90,14 @@ export async function canvasToImage(canvas: HTMLCanvasElement, type: string, qua
* @param {number} size the size of the longest edge of the new image
* @param {string} type the mime type of the image
* @param {number} quality if image is a jpeg or webp this is the quality setting
* @returns {Promise<CanvasImage>}
* @returns {Promise<CanvasImage | undefined>}
*/
export async function resizeImage(image: HTMLImageElement, size: number, type: string, quality: number): Promise<CanvasImage> {
export async function resizeImage(
image: HTMLImageElement,
size: number,
type: string,
quality: number
): Promise<CanvasImage | undefined> {
const width = image.width;
const height = image.height;
const ratio = width / height;
@ -96,37 +110,27 @@ export async function resizeImage(image: HTMLImageElement, size: number, type: s
canvas.height = size;
}
let context = canvas.getContext("2d");
// TODO: Add error if context is empty
if (context) {
context.drawImage(image, 0, 0, canvas.width, canvas.height);
} else {
return undefined;
}
return await canvasToImage(canvas, type, quality);
}
/**
* @typedef ImageAsset
* @property {number} width
* @property {number} height
* @property {Uint8Array} file
* @property {string} mime
*/
export type ImageFile = {
file: Uint8Array | null,
width: number,
height: number,
type: "file",
id: string
}
/**
* Create a image file with resolution `size`x`size` with cover cropping
* @param {HTMLImageElement} image the image to resize
* @param {string} type the mime type of the image
* @param {number} size the width and height of the thumbnail
* @param {number} quality if image is a jpeg or webp this is the quality setting
* @returns {Promise<ImageAsset>}
*/
export async function createThumbnail(image: HTMLImageElement, type: string, size = 300, quality = 0.5): Promise<ImageFile> {
export async function createThumbnail(
image: HTMLImageElement,
type: string,
size = 300,
quality = 0.5
): Promise<CanvasImage | undefined> {
let canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
@ -166,55 +170,20 @@ export async function createThumbnail(image: HTMLImageElement, type: string, siz
}
}
const thumbnailImage = await canvasToImage(canvas, type, quality);
let thumbnailBuffer = null;
if (thumbnailImage.blob) {
thumbnailBuffer = await blobToBuffer(thumbnailImage.blob);
}
return {
file: thumbnailBuffer,
width: thumbnailImage.width,
height: thumbnailImage.height,
mime: type,
};
return await canvasToImage(canvas, type, quality);
}
/**
* @typedef CircleOutline
* @property {"circle"} type
* @property {number} x - Center X of the circle
* @property {number} y - Center Y of the circle
* @property {number} radius
*/
/**
* @typedef RectOutline
* @property {"rect"} type
* @property {number} width
* @property {number} height
* @property {number} x - Leftmost X position of the rect
* @property {number} y - Topmost Y position of the rect
*/
/**
* @typedef PathOutline
* @property {"path"} type
* @property {number[]} points - Alternating x, y coordinates zipped together
*/
/**
* @typedef {CircleOutline|RectOutline|PathOutline} Outline
*/
/**
* Get the outline of an image
* @param {HTMLImageElement} image
* @returns {Outline}
*/
export function getImageOutline(image, maxPoints = 100) {
export function getImageOutline(
image: HTMLImageElement,
maxPoints: number = 100
): Outline {
// Basic rect outline for fail conditions
const defaultOutline = {
const defaultOutline: Outline = {
type: "rect",
x: 0,
y: 0,

View File

@ -10,14 +10,16 @@ import {
} from "./grid";
import Vector2 from "./Vector2";
const defaultMapProps = {
showGrid: false,
snapToGrid: true,
quality: "original",
group: "",
import { Map, FileMapResolutions, FileMap } from "../types/Map";
import { Asset } from "../types/Asset";
type Resolution = {
size: number;
quality: number;
id: "low" | "medium" | "high" | "ultra";
};
const mapResolutions = [
const mapResolutions: Resolution[] = [
{
size: 30, // Pixels per grid
quality: 0.5, // JPEG compression quality
@ -33,30 +35,35 @@ const mapResolutions = [
* @param {any} map
* @returns {undefined|string}
*/
export function getMapPreviewAsset(map) {
const res = map.resolutions;
switch (map.quality) {
case "low":
return;
case "medium":
return res.low;
case "high":
return res.medium;
case "ultra":
return res.medium;
case "original":
if (res.medium) {
return res.medium;
} else if (res.low) {
export function getMapPreviewAsset(map: Map): string | undefined {
if (map.type === "file") {
const res = map.resolutions;
switch (map.quality) {
case "low":
return;
case "medium":
return res.low;
}
return;
default:
return;
case "high":
return res.medium;
case "ultra":
return res.medium;
case "original":
if (res.medium) {
return res.medium;
} else if (res.low) {
return res.low;
}
return;
default:
return;
}
}
}
export async function createMapFromFile(file, userId) {
export async function createMapFromFile(
file: File,
userId: string
): Promise<{ map: Map; assets: Asset[] }> {
let image = new Image();
const buffer = await blobToBuffer(file);
@ -107,10 +114,10 @@ export async function createMapFromFile(file, userId) {
gridSize = { x: 22, y: 22 };
}
let assets = [];
let assets: Asset[] = [];
// Create resolutions
const resolutions = {};
const resolutions: FileMapResolutions = {};
for (let resolution of mapResolutions) {
const resolutionPixelSize = Vector2.multiply(gridSize, resolution.size);
if (
@ -119,20 +126,16 @@ export async function createMapFromFile(file, userId) {
) {
const resized = await resizeImage(
image,
Vector2.max(resolutionPixelSize),
Vector2.max(resolutionPixelSize) as number,
file.type,
resolution.quality
);
if (resized.blob) {
if (resized) {
const assetId = uuid();
resolutions[resolution.id] = assetId;
const resizedBuffer = await blobToBuffer(resized.blob);
const asset = {
file: resizedBuffer,
width: resized.width,
height: resized.height,
...resized,
id: assetId,
mime: file.type,
owner: userId,
};
assets.push(asset);
@ -141,12 +144,11 @@ export async function createMapFromFile(file, userId) {
}
// Create thumbnail
const thumbnailImage = await createThumbnail(image, file.type);
const thumbnail = {
...thumbnailImage,
id: uuid(),
owner: userId,
};
assets.push(thumbnail);
const thumbnailId = uuid();
if (thumbnailImage) {
const thumbnail = { ...thumbnailImage, id: thumbnailId, owner: userId };
assets.push(thumbnail);
}
const fileAsset = {
id: uuid(),
@ -158,11 +160,11 @@ export async function createMapFromFile(file, userId) {
};
assets.push(fileAsset);
const map = {
const map: FileMap = {
name,
resolutions,
file: fileAsset.id,
thumbnail: thumbnail.id,
thumbnail: thumbnailId,
type: "file",
grid: {
size: gridSize,
@ -183,7 +185,9 @@ export async function createMapFromFile(file, userId) {
created: Date.now(),
lastModified: Date.now(),
owner: userId,
...defaultMapProps,
showGrid: false,
snapToGrid: true,
quality: "original",
};
URL.revokeObjectURL(url);

View File

@ -1,5 +1,5 @@
export function omit(obj:object, keys: string[]) {
let tmp: { [key: string]: any } = {};
export function omit(obj: Record<PropertyKey, any>, keys: string[]) {
let tmp: Record<PropertyKey, any> = {};
for (let [key, value] of Object.entries(obj)) {
if (keys.includes(key)) {
continue;
@ -9,18 +9,21 @@ export function omit(obj:object, keys: string[]) {
return tmp;
}
export function fromEntries(iterable: any) {
export function fromEntries(iterable: Iterable<[string | number, any]>) {
if (Object.fromEntries) {
return Object.fromEntries(iterable);
}
return [...iterable].reduce((obj, [key, val]) => {
obj[key] = val;
return obj;
}, {});
return [...iterable].reduce(
(obj: Record<string | number, any>, [key, val]) => {
obj[key] = val;
return obj;
},
{}
);
}
// Check to see if all tracks are muted
export function isStreamStopped(stream: any) {
export function isStreamStopped(stream: MediaStream) {
return stream.getTracks().reduce((a: any, b: any) => a && b, { mute: true });
}
@ -55,19 +58,22 @@ export function logImage(url: string, width: number, height: number): void {
console.log("%c ", style);
}
export function isEmpty(obj: any): boolean {
export function isEmpty(obj: Object): boolean {
return Object.keys(obj).length === 0 && obj.constructor === Object;
}
export function keyBy(array: any, key: any) {
export function keyBy<Type>(array: Type[], key: string): Record<string, Type> {
return array.reduce(
(prev: any, current: any) => ({ ...prev, [key ? current[key] : current]: current }),
(prev: any, current: any) => ({
...prev,
[key ? current[key] : current]: current,
}),
{}
);
}
export function groupBy(array: any, key: string) {
return array.reduce((prev: any, current: any) => {
export function groupBy(array: Record<PropertyKey, any>[], key: string) {
return array.reduce((prev: Record<string, any[]>, current) => {
const k = current[key];
(prev[k] || (prev[k] = [])).push(current);
return prev;
@ -76,7 +82,7 @@ export function groupBy(array: any, key: string) {
export const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
export function shuffle(array) {
export function shuffle<Type>(array: Type[]) {
let temp = [...array];
var currentIndex = temp.length,
randomIndex;

View File

@ -1,12 +1,22 @@
import { v4 as uuid } from "uuid";
import Case from "case";
import { Stage } from "konva/types/Stage";
import blobToBuffer from "./blobToBuffer";
import { createThumbnail, getImageOutline } from "./image";
import Vector2 from "./Vector2";
export function createTokenState(token, position, userId) {
let tokenState = {
import { Token, FileToken } from "../types/Token";
import { TokenState, BaseTokenState } from "../types/TokenState";
import { Asset } from "../types/Asset";
import { Outline } from "../types/Outline";
export function createTokenState(
token: Token,
position: Vector2,
userId: string
): TokenState {
let tokenState: BaseTokenState = {
id: uuid(),
tokenId: token.id,
owner: userId,
@ -21,20 +31,29 @@ export function createTokenState(token, position, userId) {
rotation: 0,
locked: false,
visible: true,
type: token.type,
outline: token.outline,
width: token.width,
height: token.height,
};
if (token.type === "file") {
tokenState.file = token.file;
} else if (token.type === "default") {
tokenState.key = token.key;
return {
...tokenState,
type: "file",
file: token.file,
};
} else {
return {
...tokenState,
type: "default",
key: token.key,
};
}
return tokenState;
}
export async function createTokenFromFile(file, userId) {
export async function createTokenFromFile(
file: File,
userId: string
): Promise<{ token: Token; assets: Asset[] }> {
if (!file) {
return Promise.reject();
}
@ -77,10 +96,13 @@ export async function createTokenFromFile(file, userId) {
return new Promise((resolve, reject) => {
image.onload = async function () {
let assets = [];
let assets: Asset[] = [];
const thumbnailImage = await createThumbnail(image, file.type);
const thumbnail = { ...thumbnailImage, id: uuid(), owner: userId };
assets.push(thumbnail);
const thumbnailId = uuid();
if (thumbnailImage) {
const thumbnail = { ...thumbnailImage, id: thumbnailId, owner: userId };
assets.push(thumbnail);
}
const fileAsset = {
id: uuid(),
@ -94,10 +116,10 @@ export async function createTokenFromFile(file, userId) {
const outline = getImageOutline(image);
const token = {
const token: FileToken = {
name,
defaultSize,
thumbnail: thumbnail.id,
thumbnail: thumbnailId,
file: fileAsset.id,
id: uuid(),
type: "file",
@ -107,7 +129,6 @@ export async function createTokenFromFile(file, userId) {
defaultCategory: "character",
defaultLabel: "",
hideInSidebar: false,
group: "",
width: image.width,
height: image.height,
outline,
@ -122,12 +143,15 @@ export async function createTokenFromFile(file, userId) {
}
export function clientPositionToMapPosition(
mapStage,
clientPosition,
mapStage: Stage,
clientPosition: Vector2,
checkMapBounds = true
) {
): Vector2 | undefined {
const mapImage = mapStage.findOne("#mapImage");
const map = document.querySelector(".map");
if (!map) {
return;
}
const mapRect = map.getBoundingClientRect();
// Check map bounds
@ -158,7 +182,11 @@ export function clientPositionToMapPosition(
return normalizedPosition;
}
export function getScaledOutline(tokenState, tokenWidth, tokenHeight) {
export function getScaledOutline(
tokenState: TokenState,
tokenWidth: number,
tokenHeight: number
): Outline {
let outline = tokenState.outline;
if (outline.type === "rect") {
return {
@ -187,14 +215,23 @@ export function getScaledOutline(tokenState, tokenWidth, tokenHeight) {
}
export class Intersection {
outline;
position;
center;
rotation;
points: Vector2[] | undefined;
/**
*
* @param {Outline} outline
* @param {Vector2} position - Top left position of the token
* @param {Vector2} center - Center position of the token
* @param {number} rotation - Rotation of the token in degrees
*/
constructor(outline, position, center, rotation) {
constructor(
outline: Outline,
position: Vector2,
center: Vector2,
rotation: number
) {
this.outline = outline;
this.position = position;
this.center = center;
@ -253,8 +290,11 @@ export class Intersection {
* @param {Vector2} point
* @returns {boolean}
*/
intersects(point) {
if (this.outline.type === "rect" || this.outline.type === "path") {
intersects(point: Vector2) {
if (
this.points &&
(this.outline.type === "rect" || this.outline.type === "path")
) {
return Vector2.pointInPolygon(point, this.points);
} else if (this.outline.type === "circle") {
return Vector2.distance(this.center, point) < this.outline.radius;

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
function useDebounce(value: any, delay: number): any {
const [debouncedValue, setDebouncedValue] = useState();
function useDebounce<Type>(value: Type, delay: number): Type {
const [debouncedValue, setDebouncedValue] = useState<Type>(value);
useEffect(() => {
const timeout = setTimeout(() => {

View File

@ -5,7 +5,12 @@ import { useAuth } from "../contexts/AuthContext";
import Modal from "../components/Modal";
function AuthModal({ isOpen, onSubmit }: { isOpen: boolean, onSubmit: (newPassword: string) => void}) {
type AuthModalProps = {
isOpen: boolean;
onSubmit: (newPassword: string) => void;
};
function AuthModal({ isOpen, onSubmit }: AuthModalProps) {
const { password, setPassword } = useAuth();
const [tmpPassword, setTempPassword] = useState<string>(password);
@ -19,7 +24,7 @@ function AuthModal({ isOpen, onSubmit }: { isOpen: boolean, onSubmit: (newPassw
onSubmit(tmpPassword);
}
const inputRef = useRef<any>();
const inputRef = useRef<HTMLInputElement>(null);
function focusInput(): void {
inputRef.current && inputRef.current?.focus();
}

View File

@ -3,22 +3,24 @@ import { Box, Input, Button, Label, Flex } from "theme-ui";
import Modal from "../components/Modal";
type ChangeNicknameModalProps = {
isOpen: boolean;
onRequestClose: () => void;
onChangeSubmit: any;
nickname: string;
onChange: any;
};
function ChangeNicknameModal({
isOpen,
onRequestClose,
onChangeSubmit,
nickname,
onChange,
}: {
isOpen: boolean,
onRequestClose: () => void,
onChangeSubmit: any,
nickname: string,
onChange: any,
}) {
const inputRef = useRef<HTMLInputElement | null>(null);
}: ChangeNicknameModalProps) {
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current && inputRef.current?.focus();
inputRef.current?.focus();
}
return (

View File

@ -3,13 +3,13 @@ import { Box, Label, Flex, Button, Text } from "theme-ui";
import Modal from "../components/Modal";
type ConfirmModalProps = {
isOpen: boolean,
onRequestClose: () => void,
onConfirm: () => void,
confirmText: string,
label: string,
description: string,
}
isOpen: boolean;
onRequestClose: () => void;
onConfirm: () => void;
confirmText: string;
label: string;
description: string;
};
function ConfirmModal({
isOpen,
@ -18,7 +18,7 @@ function ConfirmModal({
confirmText,
label,
description,
}: ConfirmModalProps ) {
}: ConfirmModalProps) {
return (
<Modal
isOpen={isOpen}

View File

@ -1,73 +0,0 @@
import { useEffect, useState } from "react";
import { Box, Button, Label, Flex } from "theme-ui";
import Modal from "../components/Modal";
import Select from "../components/Select";
type EditGroupProps = {
isOpen: boolean,
onRequestClose: () => void,
onChange: any,
groups: string[],
defaultGroup: string | undefined | false,
}
function EditGroupModal({
isOpen,
onRequestClose,
onChange,
groups,
defaultGroup,
}: EditGroupProps) {
const [value, setValue] = useState<{ value: string; label: string; } | undefined>();
const [options, setOptions] = useState<{ value: string; label: string; }[]>([]);
useEffect(() => {
if (defaultGroup) {
setValue({ value: defaultGroup, label: defaultGroup });
} else {
setValue(undefined);
}
}, [defaultGroup]);
useEffect(() => {
setOptions(groups.map((group) => ({ value: group, label: group })));
}, [groups]);
function handleCreate(group: string) {
const newOption = { value: group, label: group };
setValue(newOption);
setOptions((prev) => [...prev, newOption]);
}
function handleChange() {
onChange(value ? value.value : "");
}
return (
<Modal
isOpen={isOpen}
onRequestClose={onRequestClose}
style={{ content: { overflow: "visible" } }}
>
<Box onSubmit={handleChange} sx={{ width: "300px" }}>
<Label py={2}>Select or add a group</Label>
<Select
creatable
options={options}
value={value}
onChange={setValue}
onCreateOption={handleCreate}
placeholder=""
/>
<Flex py={2}>
<Button sx={{ flexGrow: 1 }} onClick={handleChange}>
Save
</Button>
</Flex>
</Box>
</Modal>
);
}
export default EditGroupModal;

View File

@ -10,7 +10,8 @@ import { isEmpty } from "../helpers/shared";
import { getGridDefaultInset } from "../helpers/grid";
import useResponsiveLayout from "../hooks/useResponsiveLayout";
import { Map, MapState } from "../components/map/Map";
import { Map } from "../types/Map";
import { MapState } from "../types/MapState";
type EditMapProps = {
isOpen: boolean;
@ -145,7 +146,7 @@ function EditMapModal({
style={{
minHeight: 0,
padding: "16px",
backgroundColor: theme.colors.muted,
backgroundColor: theme.colors?.muted as string,
margin: "0 8px",
height: "100%",
}}

View File

@ -9,7 +9,7 @@ import TokenPreview from "../components/token/TokenPreview";
import { isEmpty } from "../helpers/shared";
import useResponsiveLayout from "../hooks/useResponsiveLayout";
import { Token } from "../tokens";
import { Token } from "../types/Token";
type EditModalProps = {
isOpen: boolean;
@ -98,7 +98,7 @@ function EditTokenModal({
style={{
minHeight: 0,
padding: "16px",
backgroundColor: theme.colors.muted,
backgroundColor: theme.colors?.muted as string,
margin: "0 8px",
height: "100%",
}}

View File

@ -2,7 +2,12 @@ import { Box, Label, Flex, Button, Text } from "theme-ui";
import Modal from "../components/Modal";
function GameExpiredModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void }) {
type GameExpiredModalProps = {
isOpen: boolean;
onRequestClose: () => void;
};
function GameExpiredModal({ isOpen, onRequestClose }: GameExpiredModalProps) {
return (
<Modal
isOpen={isOpen}

View File

@ -7,7 +7,15 @@ import Link from "../components/Link";
const gettingStarted = raw("../docs/howTo/gettingStarted.md");
function GettingStartedModal({ isOpen, onRequestClose }: { isOpen: boolean, onRequestClose: () => void } ) {
type GettingStartedModalProps = {
isOpen: boolean;
onRequestClose: () => void;
};
function GettingStartedModal({
isOpen,
onRequestClose,
}: GettingStartedModalProps) {
return (
<Modal
isOpen={isOpen}

View File

@ -3,25 +3,37 @@ import { Box, Input, Button, Label, Flex } from "theme-ui";
import Modal from "../components/Modal";
function GroupNameModal({ isOpen, name, onSubmit, onRequestClose }) {
type GroupNameModalProps = {
isOpen: boolean;
onRequestClose: () => void;
name: string;
onSubmit: (name: string) => void;
};
function GroupNameModal({
isOpen,
name,
onSubmit,
onRequestClose,
}: GroupNameModalProps) {
const [tmpName, setTempName] = useState(name);
useEffect(() => {
setTempName(name);
}, [name]);
function handleChange(event) {
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setTempName(event.target.value);
}
function handleSubmit(event) {
function handleSubmit(event: React.FormEvent<HTMLDivElement>) {
event.preventDefault();
onSubmit(tmpName);
}
const inputRef = useRef();
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current && inputRef.current.focus();
inputRef.current?.focus();
}
return (

View File

@ -14,11 +14,13 @@ import ErrorBanner from "../components/banner/ErrorBanner";
import { useUserId } from "../contexts/UserIdContext";
import { useDatabase } from "../contexts/DatabaseContext";
import SelectDataModal from "./SelectDataModal";
import SelectDataModal, { SelectData } from "./SelectDataModal";
import { getDatabase } from "../database";
import { Map, MapState, TokenState } from "../components/map/Map";
import { Token } from "../tokens";
import { Map } from "../types/Map";
import { MapState } from "../types/MapState";
import { Token } from "../types/Token";
import { Group } from "../types/Group";
const importDBName = "OwlbearRodeoImportDB";
@ -49,7 +51,11 @@ function ImportExportModal({
const [showExportSelector, setShowExportSelector] = useState(false);
const { addToast } = useToasts();
function addSuccessToast(message: string, maps: Map[], tokens: Token[]) {
function addSuccessToast(
message: string,
maps: SelectData[],
tokens: SelectData[]
) {
const mapText = `${maps.length} map${maps.length > 1 ? "s" : ""}`;
const tokenText = `${tokens.length} token${tokens.length > 1 ? "s" : ""}`;
if (maps.length > 0 && tokens.length > 0) {
@ -145,10 +151,10 @@ function ImportExportModal({
}
async function handleImportSelectorConfirm(
checkedMaps: Map[],
checkedTokens: Token[],
checkedMapGroups: any[],
checkedTokenGroups: any[]
checkedMaps: SelectData[],
checkedTokens: SelectData[],
checkedMapGroups: Group[],
checkedTokenGroups: Group[]
) {
setIsLoading(true);
backgroundTaskRunningRef.current = true;
@ -204,16 +210,15 @@ function ImportExportModal({
// Apply new token ids to imported state
for (let tokenState of Object.values(state.tokens)) {
if (tokenState.tokenId in newTokenIds) {
state.tokens[tokenState.id].tokenId =
newTokenIds[tokenState.tokenId];
tokenState.tokenId = newTokenIds[tokenState.tokenId];
}
// Change token state file asset id
if (tokenState.type === "file" && tokenState.file in newAssetIds) {
state.tokens[tokenState.id].file = newAssetIds[tokenState.file];
tokenState.file = newAssetIds[tokenState.file];
}
// Change token state owner if owned by the user of the map
if (tokenState.owner === map.owner) {
state.tokens[tokenState.id].owner = userId;
if (tokenState.owner === map.owner && userId) {
tokenState.owner = userId;
}
}
// Generate new ids
@ -368,8 +373,8 @@ function ImportExportModal({
}
async function handleExportSelectorConfirm(
checkedMaps: Map[],
checkedTokens: TokenState[]
checkedMaps: SelectData[],
checkedTokens: SelectData[]
) {
setShowExportSelector(false);
setIsLoading(true);

View File

@ -4,7 +4,12 @@ import { useHistory } from "react-router-dom";
import Modal from "../components/Modal";
function JoinModal({ isOpen, onRequestClose }: any) {
type JoinModalProps = {
isOpen: boolean;
onRequestClose: () => void;
};
function JoinModal({ isOpen, onRequestClose }: JoinModalProps) {
let history = useHistory();
const [gameId, setGameId] = useState("");
@ -17,9 +22,9 @@ function JoinModal({ isOpen, onRequestClose }: any) {
history.push(`/game/${gameId}`);
}
const inputRef = useRef<any>();
const inputRef = useRef<HTMLInputElement>(null);
function focusInput() {
inputRef.current && inputRef.current.focus();
inputRef.current?.focus();
}
return (
@ -27,7 +32,6 @@ function JoinModal({ isOpen, onRequestClose }: any) {
isOpen={isOpen}
onRequestClose={onRequestClose}
onAfterOpen={focusInput}
>
<Flex
sx={{

View File

@ -8,23 +8,35 @@ import Divider from "../components/Divider";
import { getDatabase } from "../database";
import { Map } from "../types/Map";
import { Group, GroupContainer } from "../types/Group";
import { MapState } from "../types/MapState";
import { Token } from "../types/Token";
type SelectDataProps = {
isOpen: boolean;
onRequestClose: () => void;
onConfirm: any;
onConfirm: (
checkedMaps: SelectData[],
checkedTokens: SelectData[],
checkedMapGroups: Group[],
checkedTokenGroups: Group[]
) => void;
confirmText: string;
label: string;
databaseName: string;
filter: any;
filter: (table: string, data: Map | MapState | Token, id: string) => boolean;
};
type SelectData = {
export type SelectData = {
name: string;
id: string;
type: "default" | "file";
checked: boolean;
};
type DataRecord = Record<string, SelectData>;
function SelectDataModal({
isOpen,
onRequestClose,
@ -34,13 +46,13 @@ function SelectDataModal({
databaseName,
filter,
}: SelectDataProps) {
const [maps, setMaps] = useState<Record<string, SelectData>>({});
const [mapGroups, setMapGroups] = useState<any[]>([]);
const [maps, setMaps] = useState<DataRecord>({});
const [mapGroups, setMapGroups] = useState<Group[]>([]);
const [tokensByMap, setTokensByMap] = useState<Record<string, Set<string>>>(
{}
);
const [tokens, setTokens] = useState<Record<string, SelectData>>({});
const [tokenGroups, setTokenGroups] = useState<any[]>([]);
const [tokens, setTokens] = useState<DataRecord>({});
const [tokenGroups, setTokenGroups] = useState<Group[]>([]);
const [isLoading, setIsLoading] = useState(false);
const hasMaps = Object.values(maps).length > 0;
@ -51,13 +63,13 @@ function SelectDataModal({
if (isOpen && databaseName) {
setIsLoading(true);
const db = getDatabase({ addons: [] }, databaseName);
let loadedMaps: Record<string, SelectData> = {};
let loadedMaps: DataRecord = {};
let loadedTokensByMap: Record<string, Set<string>> = {};
let loadedTokens: Record<string, SelectData> = {};
let loadedTokens: DataRecord = {};
await db
.table("maps")
.filter((map) => filter("maps", map, map.id))
.each((map) => {
.filter((map: Map) => filter("maps", map, map.id))
.each((map: Map) => {
loadedMaps[map.id] = {
name: map.name,
id: map.id,
@ -67,19 +79,19 @@ function SelectDataModal({
});
await db
.table("states")
.filter((state) => filter("states", state, state.mapId))
.each((state) => {
.filter((state: MapState) => filter("states", state, state.mapId))
.each((state: MapState) => {
loadedTokensByMap[state.mapId] = new Set(
Object.values(state.tokens).map(
(tokenState: any) => tokenState.tokenId
(tokenState) => tokenState.tokenId
)
);
});
await db
.table("tokens")
.filter((token) => filter("tokens", token, token.id))
.each((token) => {
.filter((token: Token) => filter("tokens", token, token.id))
.each((token: Token) => {
loadedTokens[token.id] = {
name: token.name,
id: token.id,
@ -110,9 +122,11 @@ function SelectDataModal({
}, [isOpen, databaseName, filter]);
// An object mapping a tokenId to how many checked maps it is currently used in
const [tokenUsedCount, setTokenUsedCount] = useState<any>({});
const [tokenUsedCount, setTokenUsedCount] = useState<Record<string, number>>(
{}
);
useEffect(() => {
let tokensUsed: any = {};
let tokensUsed: Record<string, number> = {};
for (let mapId in maps) {
if (maps[mapId].checked && mapId in tokensByMap) {
for (let tokenId of tokensByMap[mapId]) {
@ -126,7 +140,7 @@ function SelectDataModal({
}
setTokenUsedCount(tokensUsed);
// Update tokens to ensure used tokens are checked
setTokens((prevTokens: any) => {
setTokens((prevTokens) => {
let newTokens = { ...prevTokens };
for (let id in newTokens) {
if (id in tokensUsed && newTokens[id].type !== "default") {
@ -137,7 +151,7 @@ function SelectDataModal({
});
}, [maps, tokensByMap]);
function getCheckedGroups(groups: any[], data: Record<string, SelectData>) {
function getCheckedGroups(groups: Group[], data: DataRecord) {
let checkedGroups = [];
for (let group of groups) {
if (group.type === "item") {
@ -181,7 +195,7 @@ function SelectDataModal({
});
// If all token select is unchecked then ensure all tokens are unchecked
if (!event.target.checked && !tokensSelectChecked) {
setTokens((prevTokens: any) => {
setTokens((prevTokens) => {
let newTokens = { ...prevTokens };
let tempUsedCount = { ...tokenUsedCount };
for (let id in newTokens) {
@ -219,17 +233,16 @@ function SelectDataModal({
// Some tokens are checked not by maps or all tokens are checked by maps
const tokensSelectChecked =
Object.values(tokens).some(
(token: any) => !(token.id in tokenUsedCount) && token.checked
) ||
Object.values(tokens).every((token: any) => token.id in tokenUsedCount);
(token) => !(token.id in tokenUsedCount) && token.checked
) || Object.values(tokens).every((token) => token.id in tokenUsedCount);
function renderGroupContainer(
group: any,
group: GroupContainer,
checked: boolean,
renderItem: (group: any) => React.ReactNode,
renderItem: (group: Group) => React.ReactNode,
onGroupChange: (
event: React.ChangeEvent<HTMLInputElement>,
group: any
group: GroupContainer
) => void
) {
return (
@ -270,7 +283,7 @@ function SelectDataModal({
);
}
function renderMapGroup(group: any) {
function renderMapGroup(group: Group) {
if (group.type === "item") {
const map = maps[group.id];
if (map) {
@ -287,24 +300,24 @@ function SelectDataModal({
);
}
} else {
if (group.items.some((item: any) => item.id in maps)) {
if (group.items.some((item) => item.id in maps)) {
return renderGroupContainer(
group,
group.items.some((item: any) => maps[item.id]?.checked),
group.items.some((item) => maps[item.id]?.checked),
renderMapGroup,
(e, group) =>
handleMapsChanged(
e,
group.items
.filter((group: any) => group.id in maps)
.map((group: any) => maps[group.id])
.filter((group) => group.id in maps)
.map((group) => maps[group.id])
)
);
}
}
}
function renderTokenGroup(group: any) {
function renderTokenGroup(group: Group) {
if (group.type === "item") {
const token = tokens[group.id];
if (token) {
@ -332,12 +345,11 @@ function SelectDataModal({
);
}
} else {
if (group.items.some((item: any) => item.id in tokens)) {
if (group.items.some((item) => item.id in tokens)) {
const checked =
group.items.some(
(item: any) =>
!(item.id in tokenUsedCount) && tokens[item.id]?.checked
) || group.items.every((item: any) => item.id in tokenUsedCount);
(item) => !(item.id in tokenUsedCount) && tokens[item.id]?.checked
) || group.items.every((item) => item.id in tokenUsedCount);
return renderGroupContainer(
group,
checked,
@ -346,8 +358,8 @@ function SelectDataModal({
handleTokensChanged(
e,
group.items
.filter((group: any) => group.id in tokens)
.map((group: any) => tokens[group.id])
.filter((group) => group.id in tokens)
.map((group) => tokens[group.id])
)
);
}
@ -423,8 +435,8 @@ function SelectDataModal({
</Button>
<Button
disabled={
!Object.values(maps).some((map: any) => map.checked) &&
!Object.values(tokens).some((token: any) => token.checked)
!Object.values(maps).some((map) => map.checked) &&
!Object.values(tokens).some((token) => token.checked)
}
sx={{ flexGrow: 1 }}
m={1}

View File

@ -7,16 +7,22 @@ import DiceTiles from "../components/dice/DiceTiles";
import { dice } from "../dice";
import useResponsiveLayout from "../hooks/useResponsiveLayout";
import Dice from "../dice/Dice";
import { DefaultDice } from "../types/Dice";
type SelectDiceProps = {
isOpen: boolean,
onRequestClose: () => void,
onDone: any,
defaultDice: Dice
}
isOpen: boolean;
onRequestClose: () => void;
onDone: (dice: DefaultDice) => void;
defaultDice: DefaultDice;
};
function SelectDiceModal({ isOpen, onRequestClose, onDone, defaultDice }: SelectDiceProps) {
function SelectDiceModal({
isOpen,
onRequestClose,
onDone,
defaultDice,
}: SelectDiceProps) {
const [selectedDice, setSelectedDice] = useState(defaultDice);
const layout = useResponsiveLayout();
@ -24,7 +30,9 @@ function SelectDiceModal({ isOpen, onRequestClose, onDone, defaultDice }: Select
<Modal
isOpen={isOpen}
onRequestClose={onRequestClose}
style={{ content: { maxWidth: layout.modalSize, width: "calc(100% - 16px)" } }}
style={{
content: { maxWidth: layout.modalSize, width: "calc(100% - 16px)" },
}}
>
<Flex
sx={{

View File

@ -140,11 +140,13 @@ function SelectMapModal({
}
async function handleImageUpload(file: File) {
setIsLoading(true);
const { map, assets } = await createMapFromFile(file, userId);
await addMap(map);
await addAssets(assets);
setIsLoading(false);
if (userId) {
setIsLoading(true);
const { map, assets } = await createMapFromFile(file, userId);
await addMap(map);
await addAssets(assets);
setIsLoading(false);
}
}
/**
@ -183,7 +185,7 @@ function SelectMapModal({
if (groupIds.length === 1) {
// Only allow adding a map from dragging if there is a single group item selected
const group = findGroup(mapGroups, groupIds[0]);
setCanAddDraggedMap(group && group.type === "item");
setCanAddDraggedMap(group !== undefined && group.type === "item");
} else {
setCanAddDraggedMap(false);
}
@ -198,8 +200,10 @@ function SelectMapModal({
const layout = useResponsiveLayout();
const [modalSize, setModalSize] = useState({ width: 0, height: 0 });
function handleModalResize(width: number, height: number) {
setModalSize({ width, height });
function handleModalResize(width?: number, height?: number) {
if (width && height) {
setModalSize({ width, height });
}
}
const editingMap =
@ -249,7 +253,7 @@ function SelectMapModal({
<TileActionBar onAdd={openImageDialog} addTitle="Import Map(s)" />
<Box sx={{ position: "relative" }}>
<TileDragProvider
onDragAdd={canAddDraggedMap && handleDragAdd}
onDragAdd={(canAddDraggedMap && handleDragAdd) || undefined}
onDragStart={() => setIsDraggingMap(true)}
onDragEnd={() => setIsDraggingMap(false)}
onDragCancel={() => setIsDraggingMap(false)}
@ -263,7 +267,7 @@ function SelectMapModal({
</TilesContainer>
</TileDragProvider>
<TileDragProvider
onDragAdd={canAddDraggedMap && handleDragAdd}
onDragAdd={(canAddDraggedMap && handleDragAdd) || undefined}
onDragStart={() => setIsDraggingMap(true)}
onDragEnd={() => setIsDraggingMap(false)}
onDragCancel={() => setIsDraggingMap(false)}

View File

@ -35,7 +35,7 @@ import { GroupProvider } from "../contexts/GroupContext";
import { TileDragProvider } from "../contexts/TileDragContext";
import { useMapStage } from "../contexts/MapStageContext";
import { TokenState } from "../components/map/Map";
import { TokenState } from "../types/TokenState";
type SelectTokensModalProps = {
isOpen: boolean;
@ -146,6 +146,9 @@ function SelectTokensModal({
}
async function handleImageUpload(file: File) {
if (!userId) {
return;
}
setIsLoading(true);
const { token, assets } = await createTokenFromFile(file, userId);
await addToken(token);
@ -161,7 +164,7 @@ function SelectTokensModal({
const [isDraggingToken, setIsDraggingToken] = useState(false);
const mapStageRef = useMapStage();
function handleTokensAddToMap(groupIds: string[], rect: any) {
function handleTokensAddToMap(groupIds: string[], rect: DOMRect) {
let clientPosition = new Vector2(
rect.width / 2 + rect.left,
rect.height / 2 + rect.top
@ -172,11 +175,11 @@ function SelectTokensModal({
}
let position = clientPositionToMapPosition(mapStage, clientPosition, false);
if (!position) {
if (!position || !userId) {
return;
}
let newTokenStates = [];
let newTokenStates: TokenState[] = [];
for (let id of groupIds) {
if (id in tokensById) {
@ -210,8 +213,10 @@ function SelectTokensModal({
const layout = useResponsiveLayout();
const [modalSize, setModalSize] = useState({ width: 0, height: 0 });
function handleModalResize(width: number, height: number) {
setModalSize({ width, height });
function handleModalResize(width?: number, height?: number) {
if (width && height) {
setModalSize({ width, height });
}
}
const editingToken =

View File

@ -32,7 +32,6 @@ import undead from "./Undead.png";
import warlock from "./Warlock.png";
import wizard from "./Wizard.png";
import unknown from "./Unknown.png";
import { ImageFile } from "../helpers/image";
export const tokenSources = {
barbarian,
@ -81,36 +80,6 @@ function getDefaultTokenSize(key: string) {
}
}
type TokenCategory = "character" | "vehicle" | "prop"
export type BaseToken = {
id: string,
name: string,
defaultSize: number,
defaultCategory: TokenCategory,
defaultLabel: string,
hideInSidebar: boolean,
width: number,
height: number,
owner: string,
created: number,
lastModified: number,
lastUsed: number,
}
export interface DefaultToken extends BaseToken {
key: string,
type: "default",
}
export interface FileToken extends BaseToken {
file: Uint8Array,
thumbnail: ImageFile,
type: "file",
}
export type Token = DefaultToken | FileToken;
export function getDefaultTokens(userId: string) {
const tokenKeys = Object.keys(tokenSources);
let tokens = [];

View File

@ -1,16 +1,18 @@
import { InstancedMesh } from "@babylonjs/core";
import Dice from "../dice/Dice";
export type DiceType = "d4" | "d6" | "d8" | "d10" | "d12" | "d20";
export type DiceType = "d4" | "d6" | "d8" | "d10" | "d12" | "d20" | "d100";
export type DiceRoll = {
type: DiceType;
roll: number | "unknown";
};
export type Dice = {
export type DiceMesh = {
type: DiceType;
instance: InstancedMesh;
asleep: boolean;
sleepTimeout?: NodeJS.Timeout;
d10Instance?: InstancedMesh;
};
@ -18,3 +20,10 @@ export type DiceState = {
share: boolean;
rolls: DiceRoll[];
};
export type DefaultDice = {
key: string;
name: string;
class: typeof Dice;
preview: string;
};

View File

@ -32,6 +32,8 @@ export type CircleData = {
radius: number;
};
export type ShapeData = PointsData | RectData | CircleData;
export type BaseDrawing = {
blend: boolean;
color: string;
@ -43,6 +45,8 @@ export type BaseShape = BaseDrawing & {
type: "shape";
};
export type ShapeType = "line" | "rectangle" | "circle" | "triangle";
export type Line = BaseShape & {
shapeType: "line";
data: PointsData;

View File

@ -1,7 +1,9 @@
import Vector2 from "../helpers/Vector2";
export type GridInset = {
/** Top left position of the inset */
topLeft: Vector2;
/** Bottom right position of the inset */
bottomRight: Vector2;
};
@ -19,8 +21,19 @@ export type GridMeasurement = {
export type GridType = "square" | "hexVertical" | "hexHorizontal";
export type Grid = {
/** The inset of the grid from the map */
inset: GridInset;
/** The number of columns and rows of the grid as `x` and `y` */
size: Vector2;
type: GridType;
measurement: GridMeasurement;
};
export type GridScale = {
/** The number multiplier of the scale */
multiplier: number;
/** The unit of the scale */
unit: string;
/** The precision of the scale */
digits: number;
};

View File

@ -31,6 +31,7 @@ export type FileMap = BaseMap & {
file: string;
resolutions: FileMapResolutions;
thumbnail: string;
quality: "low" | "medium" | "high" | "ultra" | "original";
};
export type Map = DefaultMap | FileMap;

14
src/types/external/image.outline.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
declare module "image-outline" {
type ImageOutlineOptions = {
opacityThreshold?: number;
simplifyThreshold?: number;
pixelFn?: "opaque" | "not-white" | "not-black";
};
declare function imageOutline(
imageElement: HTMLImageElement,
options: ImageOutlineOptions
): { x: number; y: number }[];
export default imageOutline;
}

View File

@ -24,7 +24,11 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"incremental": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"typeRoots": [
"src/types/external",
"node_modules/@types"
]
},
"include": [
"src/**/*"

171
yarn.lock
View File

@ -1803,24 +1803,6 @@
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
"@emotion/babel-plugin@^11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz#3a16850ba04d8d9651f07f3fb674b3436a4fb9d7"
integrity sha512-UZKwBV2rADuhRp+ZOGgNWg2eYgbzKzQXfQPtJbu/PLy8onurxlNCLvxMQEvlr1/GudguPI5IU9qIY1+2z1M5bA==
dependencies:
"@babel/helper-module-imports" "^7.12.13"
"@babel/plugin-syntax-jsx" "^7.12.13"
"@babel/runtime" "^7.13.10"
"@emotion/hash" "^0.8.0"
"@emotion/memoize" "^0.7.5"
"@emotion/serialize" "^1.0.2"
babel-plugin-macros "^2.6.1"
convert-source-map "^1.5.0"
escape-string-regexp "^4.0.0"
find-root "^1.1.0"
source-map "^0.5.7"
stylis "^4.0.3"
"@dnd-kit/accessibility@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz#b56e3750414fd907b7d6972b3116aa8f96d07fde"
@ -1852,6 +1834,24 @@
dependencies:
tslib "^2.0.0"
"@emotion/babel-plugin@^11.3.0":
version "11.3.0"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.3.0.tgz#3a16850ba04d8d9651f07f3fb674b3436a4fb9d7"
integrity sha512-UZKwBV2rADuhRp+ZOGgNWg2eYgbzKzQXfQPtJbu/PLy8onurxlNCLvxMQEvlr1/GudguPI5IU9qIY1+2z1M5bA==
dependencies:
"@babel/helper-module-imports" "^7.12.13"
"@babel/plugin-syntax-jsx" "^7.12.13"
"@babel/runtime" "^7.13.10"
"@emotion/hash" "^0.8.0"
"@emotion/memoize" "^0.7.5"
"@emotion/serialize" "^1.0.2"
babel-plugin-macros "^2.6.1"
convert-source-map "^1.5.0"
escape-string-regexp "^4.0.0"
find-root "^1.1.0"
source-map "^0.5.7"
stylis "^4.0.3"
"@emotion/cache@^10.0.27":
version "10.0.29"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
@ -2748,76 +2748,76 @@
dependencies:
"@babel/runtime" "^7.12.5"
"@theme-ui/color-modes@0.8.4":
version "0.8.4"
resolved "https://registry.yarnpkg.com/@theme-ui/color-modes/-/color-modes-0.8.4.tgz#d09bff439e990bc8751677b1d06adf73534a5571"
integrity sha512-3Ueb6yKBFkyHsLEFlLH3Igl6ZHbVamJHn6YoAwIut0QQrAOAfTSG1vZr/4LJUCILc/U0y9kPvTw7MBpUKi1hWg==
"@theme-ui/color-modes@0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@theme-ui/color-modes/-/color-modes-0.10.0.tgz#85071f16d7d4458f3dab5a7af8b9ea459da4dcd0"
integrity sha512-6sZaagCFK48p2YjecLljFwPkiB3/R9dMNKUQC3+fnaH3N9FcsflNWpjKAYhtQ5QLKvYacFdqczT4YaMtGwKb/Q==
dependencies:
"@emotion/react" "^11.1.1"
"@theme-ui/core" "0.8.4"
"@theme-ui/css" "0.8.4"
"@theme-ui/core" "0.10.0"
"@theme-ui/css" "0.10.0"
deepmerge "^4.2.2"
"@theme-ui/components@0.8.4":
version "0.8.4"
resolved "https://registry.yarnpkg.com/@theme-ui/components/-/components-0.8.4.tgz#a362d0625f6a6efacc35a2db29fb6c2368257fd2"
integrity sha512-JOP/rABNS2Bu/hWA68Tdt6pUyCtCU+nMMWZAyvj2qDIn2mBrLwBKvvxyrwaGT5tHniIX4oVG57GH1Sb94Rw+mg==
"@theme-ui/components@0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@theme-ui/components/-/components-0.10.0.tgz#c1aa9cade71e5a6cf7c19f9e0ade900122ef23f9"
integrity sha512-zPA+16fP+R140kns+3FBhybsPzNjcCWHgXcwIPjww1dfDnlXRa7al9Nz4Y8zyWvk1wNiGqUa09Y1sabK6EYspQ==
dependencies:
"@emotion/react" "^11.1.1"
"@emotion/styled" "^11.0.0"
"@styled-system/color" "^5.1.2"
"@styled-system/should-forward-prop" "^5.1.2"
"@styled-system/space" "^5.1.2"
"@theme-ui/css" "0.8.4"
"@theme-ui/css" "0.10.0"
"@types/styled-system" "^5.1.10"
"@theme-ui/core@0.8.4":
version "0.8.4"
resolved "https://registry.yarnpkg.com/@theme-ui/core/-/core-0.8.4.tgz#64ce2db0b2d50768cb8726e61f9d391cabae0448"
integrity sha512-zHOLJ/Zw024SlQEl7+mpIk2wuNaWRFCr9brYtV+2kyLwQeETIIBXEdmQ6yJ6wc1nSJNs0VOHk/sLRPvreb+5uQ==
"@theme-ui/core@0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@theme-ui/core/-/core-0.10.0.tgz#2373d53368da5fa561915414ade070a9de0e9678"
integrity sha512-3DeTHGqyqIi05JCsJ+G+fqW6YsX/oGJiaAvMgLfd86tGdJOnDseEBQG41oDFHSWtQSJDpBcoFgAWMGITmYdH+g==
dependencies:
"@emotion/react" "^11.1.1"
"@theme-ui/css" "0.8.4"
"@theme-ui/parse-props" "0.8.4"
"@theme-ui/css" "0.10.0"
"@theme-ui/parse-props" "0.10.0"
deepmerge "^4.2.2"
"@theme-ui/css@0.8.4":
version "0.8.4"
resolved "https://registry.yarnpkg.com/@theme-ui/css/-/css-0.8.4.tgz#2539c8ccb52054d54593786e5f1e89f118e908c0"
integrity sha512-ZubYp4glaDpsJSd2z38FlwkItvXk8+t0i0323ZWNO7Liqg4t/hJT+7RmtWj1NQ2IPVqTUEmsH/hVdz5SIPu2LA==
"@theme-ui/css@0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@theme-ui/css/-/css-0.10.0.tgz#871f63334fb138491406c32f8347d53b19846a9b"
integrity sha512-Up3HqXoy2ERn/9gVxApCSl2n9vwtHBwPrYlMyEjX0YPs/rxmo+Aqe3kAxO+SG9idMw08mtdaDfMIFaPsBE5ovA==
dependencies:
"@emotion/react" "^11.1.1"
csstype "^3.0.5"
"@theme-ui/mdx@0.8.4":
version "0.8.4"
resolved "https://registry.yarnpkg.com/@theme-ui/mdx/-/mdx-0.8.4.tgz#86bee495402216f65bd3244303f2248d91ca90d0"
integrity sha512-pI2XalkIV+Ky2q/y+RBuHi9fBxzCECXZDSMYH98FExO3C6X5sdPDJ1u9kDKgbqJxUxTBQlZKSvU+fG9hjN3oQQ==
"@theme-ui/mdx@0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@theme-ui/mdx/-/mdx-0.10.0.tgz#14124cb194df8023f7253391900d8d17a6011a92"
integrity sha512-IcDrQONVrOFQFCFdyrlNoTTKmhw7ELtrLktRYmmWtCEz+KHpBiEVdxNo2yvz/05zF2BPGKOqu4wkMpUR13wNSQ==
dependencies:
"@emotion/react" "^11.1.1"
"@emotion/styled" "^11.0.0"
"@mdx-js/react" "^1.6.22"
"@theme-ui/core" "0.8.4"
"@theme-ui/css" "0.8.4"
"@theme-ui/core" "0.10.0"
"@theme-ui/css" "0.10.0"
"@theme-ui/parse-props@0.8.4":
version "0.8.4"
resolved "https://registry.yarnpkg.com/@theme-ui/parse-props/-/parse-props-0.8.4.tgz#398e54e11768248938c2c505c04bc44042b66409"
integrity sha512-2CgPKsApLVRu9wYzaaC/+ZhmKwohQ5uZylbf0HzVA3X4rGIfs7aktsM16FvCeiTyAn+7Z8MTShgSOSsD0S8l3Q==
"@theme-ui/parse-props@0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@theme-ui/parse-props/-/parse-props-0.10.0.tgz#8d2f4f3b3edafd925c3872ddd559e2b62006f43f"
integrity sha512-UfcLyThXYsB9azc8qbsZVgbF7xf+GLF2Hhy+suyjwQ3XSVOx97B5ZsuzCNUGbggtBw4dXayJgRmMz0FHyp0L8Q==
dependencies:
"@emotion/react" "^11.1.1"
"@theme-ui/css" "0.8.4"
"@theme-ui/css" "0.10.0"
"@theme-ui/theme-provider@0.8.4":
version "0.8.4"
resolved "https://registry.yarnpkg.com/@theme-ui/theme-provider/-/theme-provider-0.8.4.tgz#87ba378f2bd4d07af5c04a215db193eb78843e89"
integrity sha512-xYXqs2y9hkZmqSTA76X2dfrAfUw8LDHnBa1Xp6J41Zb4iXcxbLp3Aq1yRT1xGBAAVRGyhfMkYqxSXfNxN8R1ig==
"@theme-ui/theme-provider@0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@theme-ui/theme-provider/-/theme-provider-0.10.0.tgz#16586af579bef804f1e4997a131134d2465bfd5d"
integrity sha512-1AVsegjEAw7uidr0/qJMoKktKbdXuXRjfukI9712GZleft3dzoHhkQUO7IefXjbafyu/plzo/WTXkbz0A4uhmA==
dependencies:
"@emotion/react" "^11.1.1"
"@theme-ui/color-modes" "0.8.4"
"@theme-ui/core" "0.8.4"
"@theme-ui/css" "0.8.4"
"@theme-ui/mdx" "0.8.4"
"@theme-ui/color-modes" "0.10.0"
"@theme-ui/core" "0.10.0"
"@theme-ui/css" "0.10.0"
"@theme-ui/mdx" "0.10.0"
"@types/anymatch@*":
version "1.3.1"
@ -3096,6 +3096,11 @@
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/resize-observer-browser@^0.1.5":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.6.tgz#d8e6c2f830e2650dc06fe74464472ff64b54a302"
integrity sha512-61IfTac0s9jvNtBCpyo86QeaN8qqpMGHdK0uGKCCIy2dt5/Yk84VduHIdWAcmkC5QvdkPL0p5eWYgUZtHKKUVg==
"@types/resolve@0.0.8":
version "0.0.8"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
@ -3161,6 +3166,11 @@
dependencies:
source-map "^0.6.1"
"@types/uuid@^8.3.1":
version "8.3.1"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
"@types/webgl-ext@0.0.30":
version "0.0.30"
resolved "https://registry.yarnpkg.com/@types/webgl-ext/-/webgl-ext-0.0.30.tgz#0ce498c16a41a23d15289e0b844d945b25f0fb9d"
@ -8774,11 +8784,6 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@ -10969,11 +10974,6 @@ queue-microtask@^1.1.0:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3"
integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==
raf-schd@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
@ -11162,15 +11162,14 @@ react-refresh@^0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
react-resize-detector@4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-4.2.3.tgz#7df258668a30bdfd88e655bbdb27db7fd7b23127"
integrity sha512-4AeS6lxdz2KOgDZaOVt1duoDHrbYwSrUX32KeM9j6t9ISyRphoJbTRCMS1aPFxZHFqcCGLT1gMl3lEcSWZNW0A==
react-resize-detector@^6.7.4:
version "6.7.4"
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-6.7.4.tgz#594cc026115af05484e8011157b5dc2137492680"
integrity sha512-wzvGmUdEDMhiUHVZGnl4kuyj/TEQhvbB5LyAGkbYXetwJ2O+u/zftmPvU+kxiO1h+d9aUqQBKcNLS7TvB3ytqA==
dependencies:
lodash "^4.17.15"
lodash-es "^4.17.15"
prop-types "^15.7.2"
raf-schd "^4.0.2"
"@types/resize-observer-browser" "^0.1.5"
lodash.debounce "^4.0.8"
lodash.throttle "^4.1.1"
resize-observer-polyfill "^1.5.1"
react-router-dom@^5.1.2:
@ -12899,17 +12898,17 @@ text-table@0.2.0, text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
theme-ui@^0.8.4:
version "0.8.4"
resolved "https://registry.yarnpkg.com/theme-ui/-/theme-ui-0.8.4.tgz#c2e2aebd6266d71a9b6be70cfff018fa8337866e"
integrity sha512-vBIixheMRmFf3YuqqonYeWg0dhbshWqUzTIx4ROYLSlkbsGwcCAxSwZBtwxwnTvieaInwNUBvQ+KOZ3HpWbT6A==
theme-ui@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/theme-ui/-/theme-ui-0.10.0.tgz#4fce8fbe7ad008ec07b383eaf5f468b0317fcfa1"
integrity sha512-6uj9/4n6gZrlhrfQKt+QoLdtVc46ETJZv42iqedCatXaaTA5tHN1j7TJDmvYD9ooD/CT0+hsvOrx2d2etb/kYg==
dependencies:
"@theme-ui/color-modes" "0.8.4"
"@theme-ui/components" "0.8.4"
"@theme-ui/core" "0.8.4"
"@theme-ui/css" "0.8.4"
"@theme-ui/mdx" "0.8.4"
"@theme-ui/theme-provider" "0.8.4"
"@theme-ui/color-modes" "0.10.0"
"@theme-ui/components" "0.10.0"
"@theme-ui/core" "0.10.0"
"@theme-ui/css" "0.10.0"
"@theme-ui/mdx" "0.10.0"
"@theme-ui/theme-provider" "0.10.0"
throat@^5.0.0:
version "5.0.0"