More typescript
This commit is contained in:
parent
45a4443dd1
commit
ecfab87aa0
@ -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"
|
||||
}
|
||||
|
@ -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
|
@ -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}
|
@ -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={{
|
@ -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);
|
@ -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>
|
||||
);
|
||||
}
|
@ -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")
|
@ -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
|
@ -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>
|
@ -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);
|
@ -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;
|
@ -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,
|
@ -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 (
|
@ -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,
|
||||
|
@ -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" };
|
||||
|
@ -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",
|
||||
};
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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={{
|
@ -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
|
@ -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}
|
@ -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)}
|
||||
/>
|
@ -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
|
@ -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;
|
||||
|
@ -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;
|
68
src/contexts/DragContext.tsx
Normal file
68
src/contexts/DragContext.tsx
Normal 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;
|
@ -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;
|
||||
}
|
@ -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";
|
||||
|
||||
|
@ -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,
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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],
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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")
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
@ -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;
|
||||
|
@ -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;
|
@ -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(() => {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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}
|
||||
|
@ -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;
|
@ -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%",
|
||||
}}
|
||||
|
@ -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%",
|
||||
}}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 (
|
@ -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);
|
||||
|
@ -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={{
|
||||
|
@ -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}
|
||||
|
@ -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={{
|
||||
|
@ -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)}
|
||||
|
@ -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 =
|
||||
|
@ -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 = [];
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
14
src/types/external/image.outline.d.ts
vendored
Normal 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;
|
||||
}
|
@ -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
171
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user