More typescript
This commit is contained in:
parent
45a4443dd1
commit
ecfab87aa0
@ -47,7 +47,7 @@
|
|||||||
"react-markdown": "4",
|
"react-markdown": "4",
|
||||||
"react-media": "^2.0.0-rc.1",
|
"react-media": "^2.0.0-rc.1",
|
||||||
"react-modal": "^3.12.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-dom": "^5.1.2",
|
||||||
"react-router-hash-link": "^2.2.2",
|
"react-router-hash-link": "^2.2.2",
|
||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "^4.0.3",
|
||||||
@ -63,7 +63,7 @@
|
|||||||
"socket.io-client": "^4.1.2",
|
"socket.io-client": "^4.1.2",
|
||||||
"socket.io-msgpack-parser": "^3.0.1",
|
"socket.io-msgpack-parser": "^3.0.1",
|
||||||
"source-map-explorer": "^2.5.2",
|
"source-map-explorer": "^2.5.2",
|
||||||
"theme-ui": "^0.8.4",
|
"theme-ui": "^0.10.0",
|
||||||
"use-image": "^1.0.7",
|
"use-image": "^1.0.7",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webrtc-adapter": "^7.7.1"
|
"webrtc-adapter": "^7.7.1"
|
||||||
@ -107,6 +107,7 @@
|
|||||||
"@types/react-router-dom": "^5.1.7",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/shortid": "^0.0.29",
|
"@types/shortid": "^0.0.29",
|
||||||
"@types/simple-peer": "^9.6.3",
|
"@types/simple-peer": "^9.6.3",
|
||||||
|
"@types/uuid": "^8.3.1",
|
||||||
"typescript": "^4.2.4",
|
"typescript": "^4.2.4",
|
||||||
"worker-loader": "^3.0.8"
|
"worker-loader": "^3.0.8"
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import React from "react";
|
import { Box, Input, InputProps } from "theme-ui";
|
||||||
import { Box, Input } from "theme-ui";
|
|
||||||
|
|
||||||
import SearchIcon from "../icons/SearchIcon";
|
import SearchIcon from "../icons/SearchIcon";
|
||||||
|
|
||||||
function Search(props) {
|
function Search(props: InputProps) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: "relative", flexGrow: 1 }}>
|
<Box sx={{ position: "relative", flexGrow: 1 }}>
|
||||||
<Input
|
<Input
|
@ -3,7 +3,21 @@ import { IconButton } from "theme-ui";
|
|||||||
|
|
||||||
import Count from "./DiceButtonCount";
|
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 (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
title={title}
|
title={title}
|
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box, Text } from "theme-ui";
|
import { Box, Text } from "theme-ui";
|
||||||
|
|
||||||
function DiceButtonCount({ children }) {
|
function DiceButtonCount({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Flex, IconButton, Box } from "theme-ui";
|
import { Flex, IconButton, Box } from "theme-ui";
|
||||||
import SimpleBar from "simplebar-react";
|
import SimpleBar from "simplebar-react";
|
||||||
|
|
||||||
@ -21,6 +21,20 @@ import Divider from "../Divider";
|
|||||||
import { dice } from "../../dice";
|
import { dice } from "../../dice";
|
||||||
import useSetting from "../../hooks/useSetting";
|
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({
|
function DiceButtons({
|
||||||
diceRolls,
|
diceRolls,
|
||||||
onDiceAdd,
|
onDiceAdd,
|
||||||
@ -30,29 +44,32 @@ function DiceButtons({
|
|||||||
shareDice,
|
shareDice,
|
||||||
onShareDiceChange,
|
onShareDiceChange,
|
||||||
loading,
|
loading,
|
||||||
}) {
|
}: DiceButtonsProps) {
|
||||||
const [currentDiceStyle, setCurrentDiceStyle] = useSetting("dice.style");
|
const [currentDiceStyle, setCurrentDiceStyle] = useSetting("dice.style");
|
||||||
const [currentDice, setCurrentDice] = useState(
|
const [currentDice, setCurrentDice] = useState<DefaultDice>(
|
||||||
dice.find((d) => d.key === currentDiceStyle)
|
dice.find((d) => d.key === currentDiceStyle) || dice[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialDice = dice.find((d) => d.key === currentDiceStyle);
|
const initialDice = dice.find((d) => d.key === currentDiceStyle);
|
||||||
onDiceLoad(initialDice);
|
if (initialDice) {
|
||||||
setCurrentDice(initialDice);
|
onDiceLoad(initialDice);
|
||||||
|
setCurrentDice(initialDice);
|
||||||
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const diceCounts = {};
|
const diceCounts: Partial<Record<DiceType, number>> = {};
|
||||||
for (let dice of diceRolls) {
|
for (let dice of diceRolls) {
|
||||||
if (dice.type in diceCounts) {
|
if (dice.type in diceCounts) {
|
||||||
diceCounts[dice.type] += 1;
|
// TODO: Check type
|
||||||
|
diceCounts[dice.type]! += 1;
|
||||||
} else {
|
} else {
|
||||||
diceCounts[dice.type] = 1;
|
diceCounts[dice.type] = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDiceChange(dice) {
|
async function handleDiceChange(dice: DefaultDice) {
|
||||||
await onDiceLoad(dice);
|
await onDiceLoad(dice);
|
||||||
setCurrentDice(dice);
|
setCurrentDice(dice);
|
||||||
setCurrentDiceStyle(dice.key);
|
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 { Engine } from "@babylonjs/core/Engines/engine";
|
||||||
import { Scene } from "@babylonjs/core/scene";
|
import { Scene } from "@babylonjs/core/scene";
|
||||||
import { Vector3, Color4, Matrix } from "@babylonjs/core/Maths/math";
|
import { Vector3, Color4, Matrix } from "@babylonjs/core/Maths/math";
|
||||||
import { AmmoJSPlugin } from "@babylonjs/core/Physics/Plugins/ammoJSPlugin";
|
import { AmmoJSPlugin } from "@babylonjs/core/Physics/Plugins/ammoJSPlugin";
|
||||||
import { TargetCamera } from "@babylonjs/core/Cameras/targetCamera";
|
import { TargetCamera } from "@babylonjs/core/Cameras/targetCamera";
|
||||||
|
//@ts-ignore
|
||||||
import * as AMMO from "ammo.js";
|
import * as AMMO from "ammo.js";
|
||||||
|
|
||||||
import "@babylonjs/core/Physics/physicsEngineComponent";
|
import "@babylonjs/core/Physics/physicsEngineComponent";
|
||||||
@ -19,20 +20,44 @@ import ReactResizeDetector from "react-resize-detector";
|
|||||||
import usePreventTouch from "../../hooks/usePreventTouch";
|
import usePreventTouch from "../../hooks/usePreventTouch";
|
||||||
|
|
||||||
import ErrorBanner from "../banner/ErrorBanner";
|
import ErrorBanner from "../banner/ErrorBanner";
|
||||||
|
import { AbstractMesh } from "@babylonjs/core/Meshes/abstractMesh";
|
||||||
|
|
||||||
const diceThrowSpeed = 2;
|
const diceThrowSpeed = 2;
|
||||||
|
|
||||||
function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
|
type DiceInteractionProps = {
|
||||||
const [error, setError] = useState();
|
onSceneMount?: ({
|
||||||
|
scene,
|
||||||
|
engine,
|
||||||
|
canvas,
|
||||||
|
}: {
|
||||||
|
scene: Scene;
|
||||||
|
engine: Engine;
|
||||||
|
canvas: HTMLCanvasElement | WebGLRenderingContext;
|
||||||
|
}) => void;
|
||||||
|
onPointerDown: () => void;
|
||||||
|
onPointerUp: () => any;
|
||||||
|
};
|
||||||
|
|
||||||
const sceneRef = useRef();
|
function DiceInteraction({
|
||||||
const engineRef = useRef();
|
onSceneMount,
|
||||||
const canvasRef = useRef();
|
onPointerDown,
|
||||||
const containerRef = useRef();
|
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(() => {
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const canvas = canvasRef.current;
|
|
||||||
const engine = new Engine(canvas, true, {
|
const engine = new Engine(canvas, true, {
|
||||||
preserveDrawingBuffer: true,
|
preserveDrawingBuffer: true,
|
||||||
stencil: true,
|
stencil: true,
|
||||||
@ -67,13 +92,14 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
|
|||||||
const delta = newPosition.subtract(currentPosition);
|
const delta = newPosition.subtract(currentPosition);
|
||||||
selectedMesh.setAbsolutePosition(newPosition);
|
selectedMesh.setAbsolutePosition(newPosition);
|
||||||
const velocity = delta.scale(1000 / scene.deltaTime);
|
const velocity = delta.scale(1000 / scene.deltaTime);
|
||||||
selectedMeshVelocityWindowRef.current = selectedMeshVelocityWindowRef.current.slice(
|
selectedMeshVelocityWindowRef.current =
|
||||||
Math.max(
|
selectedMeshVelocityWindowRef.current.slice(
|
||||||
selectedMeshVelocityWindowRef.current.length -
|
Math.max(
|
||||||
selectedMeshVelocityWindowSize,
|
selectedMeshVelocityWindowRef.current.length -
|
||||||
0
|
selectedMeshVelocityWindowSize,
|
||||||
)
|
0
|
||||||
);
|
)
|
||||||
|
);
|
||||||
selectedMeshVelocityWindowRef.current.push(velocity);
|
selectedMeshVelocityWindowRef.current.push(velocity);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -82,21 +108,27 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
|
|||||||
}
|
}
|
||||||
}, [onSceneMount]);
|
}, [onSceneMount]);
|
||||||
|
|
||||||
const selectedMeshRef = useRef();
|
const selectedMeshRef = useRef<AbstractMesh | null>(null);
|
||||||
const selectedMeshVelocityWindowRef = useRef([]);
|
const selectedMeshVelocityWindowRef = useRef<Vector3[]>([]);
|
||||||
const selectedMeshVelocityWindowSize = 4;
|
const selectedMeshVelocityWindowSize = 4;
|
||||||
const selectedMeshMassRef = useRef();
|
const selectedMeshMassRef = useRef<number>(0);
|
||||||
function handlePointerDown() {
|
function handlePointerDown() {
|
||||||
const scene = sceneRef.current;
|
const scene = sceneRef.current;
|
||||||
if (scene) {
|
if (scene) {
|
||||||
const pickInfo = scene.pick(scene.pointerX, scene.pointerY);
|
const pickInfo = scene.pick(scene.pointerX, scene.pointerY);
|
||||||
if (pickInfo.hit && pickInfo.pickedMesh.name !== "dice_tray") {
|
if (
|
||||||
pickInfo.pickedMesh.physicsImpostor.setLinearVelocity(Vector3.Zero());
|
pickInfo &&
|
||||||
pickInfo.pickedMesh.physicsImpostor.setAngularVelocity(Vector3.Zero());
|
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
|
// Save the meshes mass and set it to 0 so we can pick it up
|
||||||
selectedMeshMassRef.current = pickInfo.pickedMesh.physicsImpostor.mass;
|
selectedMeshMassRef.current =
|
||||||
pickInfo.pickedMesh.physicsImpostor.setMass(0);
|
pickInfo.pickedMesh.physicsImpostor?.mass || 0;
|
||||||
|
pickInfo.pickedMesh.physicsImpostor?.setMass(0);
|
||||||
|
|
||||||
selectedMeshRef.current = pickInfo.pickedMesh;
|
selectedMeshRef.current = pickInfo.pickedMesh;
|
||||||
}
|
}
|
||||||
@ -119,27 +151,29 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-apply the meshes mass
|
// Re-apply the meshes mass
|
||||||
selectedMesh.physicsImpostor.setMass(selectedMeshMassRef.current);
|
selectedMesh.physicsImpostor?.setMass(selectedMeshMassRef.current);
|
||||||
selectedMesh.physicsImpostor.forceUpdate();
|
selectedMesh.physicsImpostor?.forceUpdate();
|
||||||
|
|
||||||
selectedMesh.physicsImpostor.applyImpulse(
|
selectedMesh.physicsImpostor?.applyImpulse(
|
||||||
velocity.scale(diceThrowSpeed * selectedMesh.physicsImpostor.mass),
|
velocity.scale(diceThrowSpeed * selectedMesh.physicsImpostor.mass),
|
||||||
selectedMesh.physicsImpostor.getObjectCenter()
|
selectedMesh.physicsImpostor.getObjectCenter()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
selectedMeshRef.current = null;
|
selectedMeshRef.current = null;
|
||||||
selectedMeshVelocityWindowRef.current = [];
|
selectedMeshVelocityWindowRef.current = [];
|
||||||
selectedMeshMassRef.current = null;
|
selectedMeshMassRef.current = 0;
|
||||||
|
|
||||||
onPointerUp();
|
onPointerUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleResize(width, height) {
|
function handleResize(width?: number, height?: number) {
|
||||||
const engine = engineRef.current;
|
if (width && height) {
|
||||||
if (engine) {
|
const engine = engineRef.current;
|
||||||
engine.resize();
|
if (engine && canvasRef.current) {
|
||||||
canvasRef.current.width = width;
|
engine.resize();
|
||||||
canvasRef.current.height = height;
|
canvasRef.current.width = width;
|
||||||
|
canvasRef.current.height = height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,7 +199,7 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) {
|
|||||||
style={{ outline: "none" }}
|
style={{ outline: "none" }}
|
||||||
/>
|
/>
|
||||||
</ReactResizeDetector>
|
</ReactResizeDetector>
|
||||||
<ErrorBanner error={error} onRequestClose={() => setError()} />
|
<ErrorBanner error={error} onRequestClose={() => setError(undefined)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -5,17 +5,28 @@ import ClearDiceIcon from "../../icons/ClearDiceIcon";
|
|||||||
import RerollDiceIcon from "../../icons/RerollDiceIcon";
|
import RerollDiceIcon from "../../icons/RerollDiceIcon";
|
||||||
|
|
||||||
import { getDiceRollTotal } from "../../helpers/dice";
|
import { getDiceRollTotal } from "../../helpers/dice";
|
||||||
|
import { DiceRoll } from "../../types/Dice";
|
||||||
|
|
||||||
const maxDiceRollsShown = 6;
|
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);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
if (diceRolls.length === 0) {
|
if (diceRolls.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let rolls = [];
|
let rolls: React.ReactChild[] = [];
|
||||||
if (diceRolls.length > 1) {
|
if (diceRolls.length > 1) {
|
||||||
rolls = diceRolls
|
rolls = diceRolls
|
||||||
.filter((dice) => dice.roll !== "unknown")
|
.filter((dice) => dice.roll !== "unknown")
|
@ -1,9 +1,17 @@
|
|||||||
import React from "react";
|
|
||||||
import { Image } from "theme-ui";
|
import { Image } from "theme-ui";
|
||||||
|
|
||||||
import Tile from "../tile/Tile";
|
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 (
|
return (
|
||||||
<div style={{ cursor: "pointer" }}>
|
<div style={{ cursor: "pointer" }}>
|
||||||
<Tile
|
<Tile
|
@ -1,12 +1,24 @@
|
|||||||
import React from "react";
|
|
||||||
import { Grid } from "theme-ui";
|
import { Grid } from "theme-ui";
|
||||||
import SimpleBar from "simplebar-react";
|
import SimpleBar from "simplebar-react";
|
||||||
|
|
||||||
import DiceTile from "./DiceTile";
|
import DiceTile from "./DiceTile";
|
||||||
|
|
||||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
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();
|
const layout = useResponsiveLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -29,7 +41,6 @@ function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
|
|||||||
isSelected={selectedDice && dice.key === selectedDice.key}
|
isSelected={selectedDice && dice.key === selectedDice.key}
|
||||||
onDiceSelect={onDiceSelect}
|
onDiceSelect={onDiceSelect}
|
||||||
onDone={onDone}
|
onDone={onDone}
|
||||||
size={layout.tileSize}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</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 { Vector3 } from "@babylonjs/core/Maths/math";
|
||||||
import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight";
|
import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight";
|
||||||
import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator";
|
import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator";
|
||||||
import { CubeTexture } from "@babylonjs/core/Materials/Textures/cubeTexture";
|
import { CubeTexture } from "@babylonjs/core/Materials/Textures/cubeTexture";
|
||||||
import { Box } from "theme-ui";
|
import { Box } from "theme-ui";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
import environment from "../../dice/environment.dds";
|
import environment from "../../dice/environment.dds";
|
||||||
|
|
||||||
import DiceInteraction from "./DiceInteraction";
|
import DiceInteraction from "./DiceInteraction";
|
||||||
@ -19,6 +20,16 @@ import { useDiceLoading } from "../../contexts/DiceLoadingContext";
|
|||||||
|
|
||||||
import { getDiceRoll } from "../../helpers/dice";
|
import { getDiceRoll } from "../../helpers/dice";
|
||||||
import useSetting from "../../hooks/useSetting";
|
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({
|
function DiceTrayOverlay({
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -26,17 +37,18 @@ function DiceTrayOverlay({
|
|||||||
onShareDiceChange,
|
onShareDiceChange,
|
||||||
diceRolls,
|
diceRolls,
|
||||||
onDiceRollsChange,
|
onDiceRollsChange,
|
||||||
}) {
|
}: DiceTrayOverlayProps) {
|
||||||
const sceneRef = useRef();
|
const sceneRef = useRef<Scene>();
|
||||||
const shadowGeneratorRef = useRef();
|
const shadowGeneratorRef = useRef<ShadowGenerator>();
|
||||||
const diceRefs = useRef([]);
|
const diceRefs = useRef<DiceMesh[]>([]);
|
||||||
const sceneVisibleRef = useRef(false);
|
const sceneVisibleRef = useRef(false);
|
||||||
const sceneInteractionRef = useRef(false);
|
const sceneInteractionRef = useRef(false);
|
||||||
// Add to the counter to ingore sleep values
|
// Add to the counter to ingore sleep values
|
||||||
const sceneKeepAwakeRef = useRef(0);
|
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 { assetLoadStart, assetLoadFinish, isLoading } = useDiceLoading();
|
||||||
const [fullScreen] = useSetting("map.fullScreen");
|
const [fullScreen] = useSetting("map.fullScreen");
|
||||||
|
|
||||||
@ -50,7 +62,7 @@ function DiceTrayOverlay({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Forces rendering for 1 second
|
// Forces rendering for 1 second
|
||||||
function forceRender() {
|
function forceRender(): () => void {
|
||||||
// Force rerender
|
// Force rerender
|
||||||
sceneKeepAwakeRef.current++;
|
sceneKeepAwakeRef.current++;
|
||||||
let triggered = false;
|
let triggered = false;
|
||||||
@ -97,7 +109,7 @@ function DiceTrayOverlay({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function initializeScene(scene) {
|
async function initializeScene(scene: Scene) {
|
||||||
handleAssetLoadStart();
|
handleAssetLoadStart();
|
||||||
let light = new DirectionalLight(
|
let light = new DirectionalLight(
|
||||||
"DirectionalLight",
|
"DirectionalLight",
|
||||||
@ -124,16 +136,14 @@ function DiceTrayOverlay({
|
|||||||
handleAssetLoadFinish();
|
handleAssetLoadFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(scene) {
|
function update(scene: Scene) {
|
||||||
function getDiceSpeed(dice) {
|
function getDiceSpeed(dice: DiceMesh) {
|
||||||
const diceSpeed = dice.instance.physicsImpostor
|
const diceSpeed =
|
||||||
.getLinearVelocity()
|
dice.instance.physicsImpostor?.getLinearVelocity()?.length() || 0;
|
||||||
.length();
|
|
||||||
// If the dice is a d100 check the d10 as well
|
// If the dice is a d100 check the d10 as well
|
||||||
if (dice.type === "d100") {
|
if (dice.d10Instance) {
|
||||||
const d10Speed = dice.d10Instance.physicsImpostor
|
const d10Speed =
|
||||||
.getLinearVelocity()
|
dice.d10Instance.physicsImpostor?.getLinearVelocity()?.length() || 0;
|
||||||
.length();
|
|
||||||
return Math.max(diceSpeed, d10Speed);
|
return Math.max(diceSpeed, d10Speed);
|
||||||
} else {
|
} else {
|
||||||
return diceSpeed;
|
return diceSpeed;
|
||||||
@ -157,14 +167,14 @@ function DiceTrayOverlay({
|
|||||||
const dice = die[i];
|
const dice = die[i];
|
||||||
const speed = getDiceSpeed(dice);
|
const speed = getDiceSpeed(dice);
|
||||||
// If the speed has been below 0.01 for 1s set dice to sleep
|
// If the speed has been below 0.01 for 1s set dice to sleep
|
||||||
if (speed < 0.01 && !dice.sleepTimout) {
|
if (speed < 0.01 && !dice.sleepTimeout) {
|
||||||
dice.sleepTimout = setTimeout(() => {
|
dice.sleepTimeout = setTimeout(() => {
|
||||||
dice.asleep = true;
|
dice.asleep = true;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else if (speed > 0.5 && (dice.asleep || dice.sleepTimout)) {
|
} else if (speed > 0.5 && (dice.asleep || dice.sleepTimeout)) {
|
||||||
dice.asleep = false;
|
dice.asleep = false;
|
||||||
clearTimeout(dice.sleepTimout);
|
dice.sleepTimeout && clearTimeout(dice.sleepTimeout);
|
||||||
dice.sleepTimout = null;
|
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 scene = sceneRef.current;
|
||||||
const shadowGenerator = shadowGeneratorRef.current;
|
const shadowGenerator = shadowGeneratorRef.current;
|
||||||
if (scene && shadowGenerator) {
|
if (scene && shadowGenerator) {
|
||||||
const instance = style.createInstance(type, scene);
|
const instance = style.createInstance(type, scene);
|
||||||
shadowGenerator.addShadowCaster(instance);
|
shadowGenerator.addShadowCaster(instance);
|
||||||
Dice.roll(instance);
|
style.roll(instance);
|
||||||
let dice = { type, instance, asleep: false };
|
let dice: DiceMesh = { type, instance, asleep: false };
|
||||||
// If we have a d100 add a d10 as well
|
// If we have a d100 add a d10 as well
|
||||||
if (type === "d100") {
|
if (type === "d100") {
|
||||||
const d10Instance = style.createInstance("d10", scene);
|
const d10Instance = style.createInstance("d10", scene);
|
||||||
@ -196,7 +206,7 @@ function DiceTrayOverlay({
|
|||||||
const die = diceRefs.current;
|
const die = diceRefs.current;
|
||||||
for (let dice of die) {
|
for (let dice of die) {
|
||||||
dice.instance.dispose();
|
dice.instance.dispose();
|
||||||
if (dice.type === "d100") {
|
if (dice.d10Instance) {
|
||||||
dice.d10Instance.dispose();
|
dice.d10Instance.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,14 +218,14 @@ function DiceTrayOverlay({
|
|||||||
const die = diceRefs.current;
|
const die = diceRefs.current;
|
||||||
for (let dice of die) {
|
for (let dice of die) {
|
||||||
Dice.roll(dice.instance);
|
Dice.roll(dice.instance);
|
||||||
if (dice.type === "d100") {
|
if (dice.d10Instance) {
|
||||||
Dice.roll(dice.d10Instance);
|
Dice.roll(dice.d10Instance);
|
||||||
}
|
}
|
||||||
dice.asleep = false;
|
dice.asleep = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDiceLoad(dice) {
|
async function handleDiceLoad(dice: DefaultDice) {
|
||||||
handleAssetLoadStart();
|
handleAssetLoadStart();
|
||||||
const scene = sceneRef.current;
|
const scene = sceneRef.current;
|
||||||
if (scene) {
|
if (scene) {
|
||||||
@ -230,10 +240,13 @@ function DiceTrayOverlay({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let renderTimeout;
|
let renderTimeout: NodeJS.Timeout;
|
||||||
let renderCleanup;
|
let renderCleanup: () => void;
|
||||||
function handleResize() {
|
function handleResize() {
|
||||||
const map = document.querySelector(".map");
|
const map = document.querySelector(".map");
|
||||||
|
if (!map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const mapRect = map.getBoundingClientRect();
|
const mapRect = map.getBoundingClientRect();
|
||||||
|
|
||||||
const availableWidth = mapRect.width - 108; // Subtract padding
|
const availableWidth = mapRect.width - 108; // Subtract padding
|
||||||
@ -283,7 +296,7 @@ function DiceTrayOverlay({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newRolls = [];
|
let newRolls: DiceRoll[] = [];
|
||||||
for (let i = 0; i < die.length; i++) {
|
for (let i = 0; i < die.length; i++) {
|
||||||
const dice = die[i];
|
const dice = die[i];
|
||||||
let roll = getDiceRoll(dice);
|
let roll = getDiceRoll(dice);
|
@ -1,10 +1,22 @@
|
|||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { IconButton } from "theme-ui";
|
import { IconButton } from "theme-ui";
|
||||||
|
|
||||||
import SelectDiceIcon from "../../icons/SelectDiceIcon";
|
import SelectDiceIcon from "../../icons/SelectDiceIcon";
|
||||||
import SelectDiceModal from "../../modals/SelectDiceModal";
|
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);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
function openModal() {
|
function openModal() {
|
||||||
@ -14,7 +26,7 @@ function SelectDiceButton({ onDiceChange, currentDice, disabled }) {
|
|||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDone(dice) {
|
function handleDone(dice: DefaultDice) {
|
||||||
onDiceChange(dice);
|
onDiceChange(dice);
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
@ -39,4 +51,8 @@ function SelectDiceButton({ onDiceChange, currentDice, disabled }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SelectDiceButton.defaultProps = {
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
export default SelectDiceButton;
|
export default SelectDiceButton;
|
@ -1,7 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDraggable } from "@dnd-kit/core";
|
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({
|
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
||||||
id,
|
id,
|
||||||
data,
|
data,
|
@ -1,7 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDroppable } from "@dnd-kit/core";
|
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 });
|
const { setNodeRef } = useDroppable({ id, disabled });
|
||||||
|
|
||||||
return (
|
return (
|
@ -26,75 +26,7 @@ import {
|
|||||||
EditShapeAction,
|
EditShapeAction,
|
||||||
RemoveShapeAction,
|
RemoveShapeAction,
|
||||||
} from "../../actions";
|
} from "../../actions";
|
||||||
import { Fog, Path, Shape } from "../../helpers/drawing";
|
|
||||||
import Session from "../../network/Session";
|
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({
|
function Map({
|
||||||
map,
|
map,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Box } from "theme-ui";
|
import { Box, ThemeUIStyleObject } from "theme-ui";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
function LazyTile({ children }) {
|
function LazyTile({ children }: { children: React.ReactNode }) {
|
||||||
const [ref, inView] = useInView({ triggerOnce: false });
|
const [ref, inView] = useInView({ triggerOnce: false });
|
||||||
|
|
||||||
const sx = inView
|
const sx: ThemeUIStyleObject = inView
|
||||||
? {}
|
? {}
|
||||||
: { width: "100%", height: "0", paddingTop: "100%", position: "relative" };
|
: { 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";
|
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({
|
function SortableTile({
|
||||||
id,
|
id,
|
||||||
disableGrouping,
|
disableGrouping,
|
||||||
@ -14,7 +24,7 @@ function SortableTile({
|
|||||||
children,
|
children,
|
||||||
isDragging,
|
isDragging,
|
||||||
cursor,
|
cursor,
|
||||||
}) {
|
}: SortableTileProps) {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
@ -35,7 +45,7 @@ function SortableTile({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Sort div left aligned
|
// Sort div left aligned
|
||||||
const sortDropStyle = {
|
const sortDropStyle: React.CSSProperties = {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
left: "-5px",
|
left: "-5px",
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -46,7 +56,7 @@ function SortableTile({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Group div center aligned
|
// Group div center aligned
|
||||||
const groupDropStyle = {
|
const groupDropStyle: React.CSSProperties = {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -55,7 +65,7 @@ function SortableTile({
|
|||||||
borderWidth: "4px",
|
borderWidth: "4px",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
borderStyle:
|
borderStyle:
|
||||||
over?.id === `${GROUP_ID_PREFIX}${id}` && active.id !== id
|
over?.id === `${GROUP_ID_PREFIX}${id}` && active?.id !== id
|
||||||
? "solid"
|
? "solid"
|
||||||
: "none",
|
: "none",
|
||||||
};
|
};
|
@ -15,8 +15,14 @@ import {
|
|||||||
GROUP_SORTABLE_ID,
|
GROUP_SORTABLE_ID,
|
||||||
} from "../../contexts/TileDragContext";
|
} from "../../contexts/TileDragContext";
|
||||||
import { useGroup } from "../../contexts/GroupContext";
|
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 dragId = useTileDragId();
|
||||||
const dragCursor = useTileDragCursor();
|
const dragCursor = useTileDragCursor();
|
||||||
const overGroupId = useTileOverGroupId();
|
const overGroupId = useTileOverGroupId();
|
||||||
@ -38,14 +44,14 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
const sortableId = subgroup ? GROUP_SORTABLE_ID : BASE_SORTABLE_ID;
|
const sortableId = subgroup ? GROUP_SORTABLE_ID : BASE_SORTABLE_ID;
|
||||||
|
|
||||||
// Only populate selected groups if needed
|
// Only populate selected groups if needed
|
||||||
let selectedGroupIds = [];
|
let selectedGroupIds: string[] = [];
|
||||||
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
|
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
|
||||||
selectedGroupIds = allSelectedIds;
|
selectedGroupIds = allSelectedIds;
|
||||||
}
|
}
|
||||||
const disableSorting = (openGroupId && !subgroup) || filter;
|
const disableSorting = !!((openGroupId && !subgroup) || filter);
|
||||||
const disableGrouping = subgroup || disableSorting || 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 (overGroupId === group.id && dragId && group.id !== dragId) {
|
||||||
// If dragging over a group render a preview of that group
|
// If dragging over a group render a preview of that group
|
||||||
const previewGroup = moveGroupsInto(
|
const previewGroup = moveGroupsInto(
|
||||||
@ -61,7 +67,7 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
function renderTiles() {
|
function renderTiles() {
|
||||||
const groupsByIds = keyBy(activeGroups, "id");
|
const groupsByIds = keyBy(activeGroups, "id");
|
||||||
const selectedGroupIdsSet = new Set(selectedGroupIds);
|
const selectedGroupIdsSet = new Set(selectedGroupIds);
|
||||||
let selectedGroups = [];
|
let selectedGroups: Group[] = [];
|
||||||
let hasSelectedContainerGroup = false;
|
let hasSelectedContainerGroup = false;
|
||||||
for (let groupId of selectedGroupIds) {
|
for (let groupId of selectedGroupIds) {
|
||||||
const group = groupsByIds[groupId];
|
const group = groupsByIds[groupId];
|
||||||
@ -72,8 +78,8 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return activeGroups.map((group) => {
|
return activeGroups.map((group: Group) => {
|
||||||
const isDragging = dragId && selectedGroupIdsSet.has(group.id);
|
const isDragging = dragId !== null && selectedGroupIdsSet.has(group.id);
|
||||||
const disableTileGrouping =
|
const disableTileGrouping =
|
||||||
disableGrouping || isDragging || hasSelectedContainerGroup;
|
disableGrouping || isDragging || hasSelectedContainerGroup;
|
||||||
return (
|
return (
|
||||||
@ -84,7 +90,7 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
disableSorting={disableSorting}
|
disableSorting={disableSorting}
|
||||||
hidden={group.id === openGroupId}
|
hidden={group.id === openGroupId}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
cursor={dragCursor}
|
cursor={dragCursor || ""}
|
||||||
>
|
>
|
||||||
{renderSortableGroup(group, selectedGroups)}
|
{renderSortableGroup(group, selectedGroups)}
|
||||||
</SortableTile>
|
</SortableTile>
|
@ -8,8 +8,17 @@ import Vector2 from "../../helpers/Vector2";
|
|||||||
|
|
||||||
import { useTileDragId } from "../../contexts/TileDragContext";
|
import { useTileDragId } from "../../contexts/TileDragContext";
|
||||||
import { useGroup } from "../../contexts/GroupContext";
|
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 dragId = useTileDragId();
|
||||||
const {
|
const {
|
||||||
groups,
|
groups,
|
||||||
@ -27,7 +36,7 @@ function SortableTilesDragOverlay({ renderTile, subgroup }) {
|
|||||||
: groups;
|
: groups;
|
||||||
|
|
||||||
// Only populate selected groups if needed
|
// Only populate selected groups if needed
|
||||||
let selectedGroupIds = [];
|
let selectedGroupIds: string[] = [];
|
||||||
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
|
if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
|
||||||
selectedGroupIds = allSelectedIds;
|
selectedGroupIds = allSelectedIds;
|
||||||
}
|
}
|
@ -3,6 +3,18 @@ import { Flex, IconButton, Box, Text, Badge } from "theme-ui";
|
|||||||
|
|
||||||
import EditTileIcon from "../../icons/EditTileIcon";
|
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({
|
function Tile({
|
||||||
title,
|
title,
|
||||||
isSelected,
|
isSelected,
|
||||||
@ -13,7 +25,7 @@ function Tile({
|
|||||||
badges,
|
badges,
|
||||||
editTitle,
|
editTitle,
|
||||||
children,
|
children,
|
||||||
}) {
|
}: TileProps) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { Flex, IconButton } from "theme-ui";
|
import { Flex, IconButton } from "theme-ui";
|
||||||
|
|
||||||
import AddIcon from "../../icons/AddIcon";
|
import AddIcon from "../../icons/AddIcon";
|
||||||
@ -10,7 +9,12 @@ import RadioIconButton from "../RadioIconButton";
|
|||||||
|
|
||||||
import { useGroup } from "../../contexts/GroupContext";
|
import { useGroup } from "../../contexts/GroupContext";
|
||||||
|
|
||||||
function TileActionBar({ onAdd, addTitle }) {
|
type TileActionBarProps = {
|
||||||
|
onAdd: () => void;
|
||||||
|
addTitle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function TileActionBar({ onAdd, addTitle }: TileActionBarProps) {
|
||||||
const {
|
const {
|
||||||
selectMode,
|
selectMode,
|
||||||
onSelectModeChange,
|
onSelectModeChange,
|
||||||
@ -33,7 +37,7 @@ function TileActionBar({ onAdd, addTitle }) {
|
|||||||
outlineOffset: "0px",
|
outlineOffset: "0px",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onFocus={() => onGroupSelect()}
|
onFocus={() => onGroupSelect(undefined)}
|
||||||
>
|
>
|
||||||
<Search value={filter} onChange={(e) => onFilterChange(e.target.value)} />
|
<Search value={filter} onChange={(e) => onFilterChange(e.target.value)} />
|
||||||
<Flex
|
<Flex
|
@ -9,7 +9,7 @@ import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
|||||||
|
|
||||||
import Droppable from "../drag/Droppable";
|
import Droppable from "../drag/Droppable";
|
||||||
|
|
||||||
function TilesContainer({ children }) {
|
function TilesContainer({ children }: { children: React.ReactNode }) {
|
||||||
const { onGroupSelect } = useGroup();
|
const { onGroupSelect } = useGroup();
|
||||||
|
|
||||||
const { theme } = useThemeUI();
|
const { theme } = useThemeUI();
|
||||||
@ -21,9 +21,9 @@ function TilesContainer({ children }) {
|
|||||||
<SimpleBar
|
<SimpleBar
|
||||||
style={{
|
style={{
|
||||||
height: layout.tileContainerHeight,
|
height: layout.tileContainerHeight,
|
||||||
backgroundColor: theme.colors.muted,
|
backgroundColor: theme.colors?.muted as string,
|
||||||
}}
|
}}
|
||||||
onClick={() => onGroupSelect()}
|
onClick={() => onGroupSelect(undefined)}
|
||||||
>
|
>
|
||||||
<Grid
|
<Grid
|
||||||
p={3}
|
p={3}
|
@ -16,15 +16,16 @@ import GroupNameModal from "../../modals/GroupNameModal";
|
|||||||
import { renameGroup } from "../../helpers/group";
|
import { renameGroup } from "../../helpers/group";
|
||||||
|
|
||||||
import Droppable from "../drag/Droppable";
|
import Droppable from "../drag/Droppable";
|
||||||
|
import { Group } from "../../types/Group";
|
||||||
|
|
||||||
function TilesOverlay({ modalSize, children }) {
|
type TilesOverlayProps = {
|
||||||
const {
|
modalSize: { width: number; height: number };
|
||||||
groups,
|
children: React.ReactNode;
|
||||||
openGroupId,
|
};
|
||||||
onGroupClose,
|
|
||||||
onGroupSelect,
|
function TilesOverlay({ modalSize, children }: TilesOverlayProps) {
|
||||||
onGroupsChange,
|
const { groups, openGroupId, onGroupClose, onGroupSelect, onGroupsChange } =
|
||||||
} = useGroup();
|
useGroup();
|
||||||
|
|
||||||
const { theme } = useThemeUI();
|
const { theme } = useThemeUI();
|
||||||
|
|
||||||
@ -37,23 +38,29 @@ function TilesOverlay({ modalSize, children }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [containerSize, setContinerSize] = useState({ width: 0, height: 0 });
|
const [containerSize, setContinerSize] = useState({ width: 0, height: 0 });
|
||||||
function handleContainerResize(width, height) {
|
function handleContainerResize(width?: number, height?: number) {
|
||||||
const size = Math.min(width, height) - 16;
|
if (width && height) {
|
||||||
setContinerSize({ width: size, height: size });
|
const size = Math.min(width, height) - 16;
|
||||||
|
setContinerSize({ width: size, height: size });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isGroupNameModalOpen, setIsGroupNameModalOpen] = useState(false);
|
const [isGroupNameModalOpen, setIsGroupNameModalOpen] = useState(false);
|
||||||
function handleGroupNameChange(name) {
|
function handleGroupNameChange(name: string) {
|
||||||
onGroupsChange(renameGroup(groups, openGroupId, name));
|
if (openGroupId) {
|
||||||
setIsGroupNameModalOpen(false);
|
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) {
|
if (!openGroupId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const groupName = group && group.type === "group" && group.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
@ -104,14 +111,14 @@ function TilesOverlay({ modalSize, children }) {
|
|||||||
>
|
>
|
||||||
<Flex my={1} sx={{ position: "relative" }}>
|
<Flex my={1} sx={{ position: "relative" }}>
|
||||||
<Text as="p" my="2px">
|
<Text as="p" my="2px">
|
||||||
{group?.name}
|
{groupName}
|
||||||
</Text>
|
</Text>
|
||||||
<IconButton
|
<IconButton
|
||||||
sx={{
|
sx={{
|
||||||
width: "24px",
|
width: "24px",
|
||||||
height: "24px",
|
height: "24px",
|
||||||
position: group?.name ? "absolute" : "relative",
|
position: groupName ? "absolute" : "relative",
|
||||||
left: group?.name ? "100%" : 0,
|
left: groupName ? "100%" : 0,
|
||||||
}}
|
}}
|
||||||
title="Edit Group"
|
title="Edit Group"
|
||||||
aria-label="Edit Group"
|
aria-label="Edit Group"
|
||||||
@ -125,9 +132,9 @@ function TilesOverlay({ modalSize, children }) {
|
|||||||
width: containerSize.width - 16,
|
width: containerSize.width - 16,
|
||||||
height: containerSize.height - 48,
|
height: containerSize.height - 48,
|
||||||
marginBottom: "8px",
|
marginBottom: "8px",
|
||||||
backgroundColor: theme.colors.muted,
|
backgroundColor: theme.colors?.muted as string,
|
||||||
}}
|
}}
|
||||||
onClick={() => onGroupSelect()}
|
onClick={() => onGroupSelect(undefined)}
|
||||||
>
|
>
|
||||||
<Grid
|
<Grid
|
||||||
sx={{
|
sx={{
|
||||||
@ -179,7 +186,7 @@ function TilesOverlay({ modalSize, children }) {
|
|||||||
</ReactResizeDetector>
|
</ReactResizeDetector>
|
||||||
<GroupNameModal
|
<GroupNameModal
|
||||||
isOpen={isGroupNameModalOpen}
|
isOpen={isGroupNameModalOpen}
|
||||||
name={group?.name}
|
name={groupName || ""}
|
||||||
onSubmit={handleGroupNameChange}
|
onSubmit={handleGroupNameChange}
|
||||||
onRequestClose={() => setIsGroupNameModalOpen(false)}
|
onRequestClose={() => setIsGroupNameModalOpen(false)}
|
||||||
/>
|
/>
|
@ -8,49 +8,20 @@ import { useDatabase } from "./DatabaseContext";
|
|||||||
import useDebounce from "../hooks/useDebounce";
|
import useDebounce from "../hooks/useDebounce";
|
||||||
|
|
||||||
import { omit } from "../helpers/shared";
|
import { omit } from "../helpers/shared";
|
||||||
|
import { Asset } from "../types/Asset";
|
||||||
|
|
||||||
/**
|
type AssetsContext = {
|
||||||
* @typedef Asset
|
getAsset: (assetId: string) => Promise<Asset | undefined>;
|
||||||
* @property {string} id
|
addAssets: (assets: Asset[]) => void;
|
||||||
* @property {number} width
|
putAsset: (asset: Asset) => void;
|
||||||
* @property {number} height
|
};
|
||||||
* @property {Uint8Array} file
|
|
||||||
* @property {string} mime
|
|
||||||
* @property {string} owner
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
const AssetsContext = React.createContext<AssetsContext | undefined>(undefined);
|
||||||
* @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();
|
|
||||||
|
|
||||||
// 100 MB max cache size
|
// 100 MB max cache size
|
||||||
const maxCacheSize = 1e8;
|
const maxCacheSize = 1e8;
|
||||||
|
|
||||||
export function AssetsProvider({ children }) {
|
export function AssetsProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { worker, database, databaseStatus } = useDatabase();
|
const { worker, database, databaseStatus } = useDatabase();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -61,33 +32,39 @@ export function AssetsProvider({ children }) {
|
|||||||
|
|
||||||
const getAsset = useCallback(
|
const getAsset = useCallback(
|
||||||
async (assetId) => {
|
async (assetId) => {
|
||||||
return await database.table("assets").get(assetId);
|
if (database) {
|
||||||
|
return await database.table("assets").get(assetId);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[database]
|
[database]
|
||||||
);
|
);
|
||||||
|
|
||||||
const addAssets = useCallback(
|
const addAssets = useCallback(
|
||||||
async (assets) => {
|
async (assets) => {
|
||||||
await database.table("assets").bulkAdd(assets);
|
if (database) {
|
||||||
|
await database.table("assets").bulkAdd(assets);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[database]
|
[database]
|
||||||
);
|
);
|
||||||
|
|
||||||
const putAsset = useCallback(
|
const putAsset = useCallback(
|
||||||
async (asset) => {
|
async (asset) => {
|
||||||
// Check for broadcast channel and attempt to use worker to put map to avoid UI lockup
|
if (database) {
|
||||||
// Safari doesn't support BC so fallback to single thread
|
// Check for broadcast channel and attempt to use worker to put map to avoid UI lockup
|
||||||
if (window.BroadcastChannel) {
|
// Safari doesn't support BC so fallback to single thread
|
||||||
const packedAsset = encode(asset);
|
if (window.BroadcastChannel) {
|
||||||
const success = await worker.putData(
|
const packedAsset = encode(asset);
|
||||||
Comlink.transfer(packedAsset, [packedAsset.buffer]),
|
const success = await worker.putData(
|
||||||
"assets"
|
Comlink.transfer(packedAsset, [packedAsset.buffer]),
|
||||||
);
|
"assets"
|
||||||
if (!success) {
|
);
|
||||||
|
if (!success) {
|
||||||
|
await database.table("assets").put(asset);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
await database.table("assets").put(asset);
|
await database.table("assets").put(asset);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
await database.table("assets").put(asset);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[database, worker]
|
[database, worker]
|
||||||
@ -119,35 +96,38 @@ export function useAssets() {
|
|||||||
* @property {number} references
|
* @property {number} references
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
type AssetURL = {
|
||||||
* @type React.Context<undefined|Object.<string, AssetURL>>
|
url: string | null;
|
||||||
*/
|
id: string;
|
||||||
export const AssetURLsStateContext = React.createContext();
|
references: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
type AssetURLs = Record<string, AssetURL>;
|
||||||
* @type React.Context<undefined|React.Dispatch<React.SetStateAction<{}>>>
|
|
||||||
*/
|
export const AssetURLsStateContext =
|
||||||
export const AssetURLsUpdaterContext = React.createContext();
|
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
|
* Helper to manage sharing of custom image sources between uses of useAssetURL
|
||||||
*/
|
*/
|
||||||
export function AssetURLsProvider({ children }) {
|
export function AssetURLsProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [assetURLs, setAssetURLs] = useState({});
|
const [assetURLs, setAssetURLs] = useState<AssetURLs>({});
|
||||||
const { database } = useDatabase();
|
const { database } = useDatabase();
|
||||||
|
|
||||||
// Keep track of the assets that need to be loaded
|
// Keep track of the assets that need to be loaded
|
||||||
const [assetKeys, setAssetKeys] = useState([]);
|
const [assetKeys, setAssetKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
// Load assets after 100ms
|
// Load assets after 100ms
|
||||||
const loadingDebouncedAssetURLs = useDebounce(assetURLs, 100);
|
const loadingDebouncedAssetURLs = useDebounce(assetURLs, 100);
|
||||||
|
|
||||||
// Update the asset keys to load when a url is added without an asset attached
|
// Update the asset keys to load when a url is added without an asset attached
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadingDebouncedAssetURLs) {
|
let keysToLoad: string[] = [];
|
||||||
return;
|
|
||||||
}
|
|
||||||
let keysToLoad = [];
|
|
||||||
for (let url of Object.values(loadingDebouncedAssetURLs)) {
|
for (let url of Object.values(loadingDebouncedAssetURLs)) {
|
||||||
if (url.url === null) {
|
if (url.url === null) {
|
||||||
keysToLoad.push(url.id);
|
keysToLoad.push(url.id);
|
||||||
@ -159,8 +139,9 @@ export function AssetURLsProvider({ children }) {
|
|||||||
}, [loadingDebouncedAssetURLs]);
|
}, [loadingDebouncedAssetURLs]);
|
||||||
|
|
||||||
// Get the new assets whenever the keys change
|
// Get the new assets whenever the keys change
|
||||||
const assets = useLiveQuery(
|
const assets = useLiveQuery<Asset[]>(
|
||||||
() => database?.table("assets").where("id").anyOf(assetKeys).toArray(),
|
() =>
|
||||||
|
database?.table("assets").where("id").anyOf(assetKeys).toArray() || [],
|
||||||
[database, assetKeys]
|
[database, assetKeys]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -197,7 +178,7 @@ export function AssetURLsProvider({ children }) {
|
|||||||
let urlsToCleanup = [];
|
let urlsToCleanup = [];
|
||||||
for (let url of Object.values(prevURLs)) {
|
for (let url of Object.values(prevURLs)) {
|
||||||
if (url.references <= 0) {
|
if (url.references <= 0) {
|
||||||
URL.revokeObjectURL(url.url);
|
url.url && URL.revokeObjectURL(url.url);
|
||||||
urlsToCleanup.push(url.id);
|
urlsToCleanup.push(url.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -220,13 +201,13 @@ export function AssetURLsProvider({ children }) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to load either file or default asset into a URL
|
* 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);
|
const assetURLs = useContext(AssetURLsStateContext);
|
||||||
if (assetURLs === undefined) {
|
if (assetURLs === undefined) {
|
||||||
throw new Error("useAssetURL must be used within a AssetURLsProvider");
|
throw new Error("useAssetURL must be used within a AssetURLsProvider");
|
||||||
@ -242,7 +223,7 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateAssetURL() {
|
function updateAssetURL() {
|
||||||
function increaseReferences(prevURLs) {
|
function increaseReferences(prevURLs: AssetURLs): AssetURLs {
|
||||||
return {
|
return {
|
||||||
...prevURLs,
|
...prevURLs,
|
||||||
[assetId]: {
|
[assetId]: {
|
||||||
@ -252,13 +233,13 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReference(prevURLs) {
|
function createReference(prevURLs: AssetURLs): AssetURLs {
|
||||||
return {
|
return {
|
||||||
...prevURLs,
|
...prevURLs,
|
||||||
[assetId]: { url: null, id: assetId, references: 1 },
|
[assetId]: { url: null, id: assetId, references: 1 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
setAssetURLs((prevURLs) => {
|
setAssetURLs?.((prevURLs) => {
|
||||||
if (assetId in prevURLs) {
|
if (assetId in prevURLs) {
|
||||||
// Check if the asset url is already added and increase references
|
// Check if the asset url is already added and increase references
|
||||||
return increaseReferences(prevURLs);
|
return increaseReferences(prevURLs);
|
||||||
@ -303,36 +284,29 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource) {
|
|||||||
return unknownSource;
|
return unknownSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
type FileData = {
|
||||||
* @typedef FileData
|
file: string;
|
||||||
* @property {string} file
|
type: "file";
|
||||||
* @property {"file"} type
|
thumbnail: string;
|
||||||
* @property {string} thumbnail
|
quality?: string;
|
||||||
* @property {string=} quality
|
resolutions?: Record<string, string>;
|
||||||
* @property {Object.<string, string>=} resolutions
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
type DefaultData = {
|
||||||
* @typedef DefaultData
|
key: string;
|
||||||
* @property {string} key
|
type: "default";
|
||||||
* @property {"default"} type
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a map or token into a URL taking into account a thumbnail and multiple resolutions
|
* 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(
|
export function useDataURL(
|
||||||
data,
|
data: FileData | DefaultData,
|
||||||
defaultSources,
|
defaultSources: Record<string, string>,
|
||||||
unknownSource,
|
unknownSource: string | undefined,
|
||||||
thumbnail = false
|
thumbnail = false
|
||||||
) {
|
) {
|
||||||
const [assetId, setAssetId] = useState();
|
const [assetId, setAssetId] = useState<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -344,7 +318,11 @@ export function useDataURL(
|
|||||||
} else {
|
} else {
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
setAssetId(data.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]);
|
setAssetId(data.resolutions[data.quality]);
|
||||||
} else {
|
} else {
|
||||||
setAssetId(data.file);
|
setAssetId(data.file);
|
||||||
@ -356,7 +334,7 @@ export function useDataURL(
|
|||||||
}, [data, thumbnail]);
|
}, [data, thumbnail]);
|
||||||
|
|
||||||
const assetURL = useAssetURL(
|
const assetURL = useAssetURL(
|
||||||
assetId,
|
assetId || "",
|
||||||
data?.type,
|
data?.type,
|
||||||
defaultSources,
|
defaultSources,
|
||||||
unknownSource
|
unknownSource
|
@ -2,9 +2,11 @@ import React, { useState, useEffect, useContext } from "react";
|
|||||||
|
|
||||||
import FakeStorage from "../helpers/FakeStorage";
|
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);
|
const AuthContext = React.createContext<AuthContext | undefined>(undefined);
|
||||||
|
|
||||||
let storage: Storage | FakeStorage;
|
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 { getGroupItems, groupsFromIds } from "../helpers/group";
|
||||||
|
|
||||||
import shortcuts from "../shortcuts";
|
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({
|
export function GroupProvider({
|
||||||
groups,
|
groups,
|
||||||
@ -17,16 +49,17 @@ export function GroupProvider({
|
|||||||
onGroupsSelect,
|
onGroupsSelect,
|
||||||
disabled,
|
disabled,
|
||||||
children,
|
children,
|
||||||
}) {
|
}: GroupProviderProps) {
|
||||||
const [selectedGroupIds, setSelectedGroupIds] = useState([]);
|
const [selectedGroupIds, setSelectedGroupIds] = useState<string[]>([]);
|
||||||
// Either single, multiple or range
|
// Either single, multiple or range
|
||||||
const [selectMode, setSelectMode] = useState("single");
|
const [selectMode, setSelectMode] =
|
||||||
|
useState<"single" | "multiple" | "range">("single");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Group Open
|
* Group Open
|
||||||
*/
|
*/
|
||||||
const [openGroupId, setOpenGroupId] = useState();
|
const [openGroupId, setOpenGroupId] = useState<string>();
|
||||||
const [openGroupItems, setOpenGroupItems] = useState([]);
|
const [openGroupItems, setOpenGroupItems] = useState<Group[]>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (openGroupId) {
|
if (openGroupId) {
|
||||||
const openGroups = groupsFromIds([openGroupId], groups);
|
const openGroups = groupsFromIds([openGroupId], groups);
|
||||||
@ -37,29 +70,29 @@ export function GroupProvider({
|
|||||||
// Close group if we can't find it
|
// Close group if we can't find it
|
||||||
// This can happen if it was deleted or all it's items were deleted
|
// This can happen if it was deleted or all it's items were deleted
|
||||||
setOpenGroupItems([]);
|
setOpenGroupItems([]);
|
||||||
setOpenGroupId();
|
setOpenGroupId(undefined);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setOpenGroupItems([]);
|
setOpenGroupItems([]);
|
||||||
}
|
}
|
||||||
}, [openGroupId, groups]);
|
}, [openGroupId, groups]);
|
||||||
|
|
||||||
function handleGroupOpen(groupId) {
|
function handleGroupOpen(groupId: string) {
|
||||||
setSelectedGroupIds([]);
|
setSelectedGroupIds([]);
|
||||||
setOpenGroupId(groupId);
|
setOpenGroupId(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGroupClose() {
|
function handleGroupClose() {
|
||||||
setSelectedGroupIds([]);
|
setSelectedGroupIds([]);
|
||||||
setOpenGroupId();
|
setOpenGroupId(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search
|
* Search
|
||||||
*/
|
*/
|
||||||
const [filter, setFilter] = useState();
|
const [filter, setFilter] = useState<string>();
|
||||||
const [filteredGroupItems, setFilteredGroupItems] = useState([]);
|
const [filteredGroupItems, setFilteredGroupItems] = useState<GroupItem[]>([]);
|
||||||
const [fuse, setFuse] = useState();
|
const [fuse, setFuse] = useState<Fuse<GroupItem & { name: string }>>();
|
||||||
// Update search index when items change
|
// Update search index when items change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let items = [];
|
let items = [];
|
||||||
@ -76,10 +109,10 @@ export function GroupProvider({
|
|||||||
|
|
||||||
// Perform search when search changes
|
// Perform search when search changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filter) {
|
if (filter && fuse) {
|
||||||
const query = fuse.search(filter);
|
const query = fuse.search(filter);
|
||||||
setFilteredGroupItems(query.map((result) => result.item));
|
setFilteredGroupItems(query.map((result) => result.item));
|
||||||
setOpenGroupId();
|
setOpenGroupId(undefined);
|
||||||
} else {
|
} else {
|
||||||
setFilteredGroupItems([]);
|
setFilteredGroupItems([]);
|
||||||
}
|
}
|
||||||
@ -96,23 +129,30 @@ export function GroupProvider({
|
|||||||
: groups;
|
: groups;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param {Group[] | GroupItem[]} newGroups
|
||||||
* @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object
|
* @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 (groupId) {
|
||||||
// If a group is specidifed then update that group with the new items
|
// If a group is specidifed then update that group with the new items
|
||||||
const groupIndex = groups.findIndex((group) => group.id === groupId);
|
const groupIndex = groups.findIndex((group) => group.id === groupId);
|
||||||
let updatedGroups = cloneDeep(groups);
|
let updatedGroups = cloneDeep(groups);
|
||||||
const group = updatedGroups[groupIndex];
|
const group = updatedGroups[groupIndex];
|
||||||
updatedGroups[groupIndex] = { ...group, items: newGroups };
|
updatedGroups[groupIndex] = {
|
||||||
|
...group,
|
||||||
|
items: newGroups,
|
||||||
|
} as GroupContainer;
|
||||||
onGroupsChange(updatedGroups);
|
onGroupsChange(updatedGroups);
|
||||||
} else {
|
} else {
|
||||||
onGroupsChange(newGroups);
|
onGroupsChange(newGroups);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGroupSelect(groupId) {
|
function handleGroupSelect(groupId: string | undefined) {
|
||||||
let groupIds = [];
|
let groupIds: string[] = [];
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
switch (selectMode) {
|
switch (selectMode) {
|
||||||
case "single":
|
case "single":
|
||||||
@ -133,8 +173,8 @@ export function GroupProvider({
|
|||||||
const lastIndex = activeGroups.findIndex(
|
const lastIndex = activeGroups.findIndex(
|
||||||
(g) => g.id === selectedGroupIds[selectedGroupIds.length - 1]
|
(g) => g.id === selectedGroupIds[selectedGroupIds.length - 1]
|
||||||
);
|
);
|
||||||
let idsToAdd = [];
|
let idsToAdd: string[] = [];
|
||||||
let idsToRemove = [];
|
let idsToRemove: string[] = [];
|
||||||
const direction = currentIndex > lastIndex ? 1 : -1;
|
const direction = currentIndex > lastIndex ? 1 : -1;
|
||||||
for (
|
for (
|
||||||
let i = lastIndex + direction;
|
let i = lastIndex + direction;
|
||||||
@ -166,7 +206,7 @@ export function GroupProvider({
|
|||||||
/**
|
/**
|
||||||
* Shortcuts
|
* Shortcuts
|
||||||
*/
|
*/
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event: React.KeyboardEvent) {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -178,7 +218,7 @@ export function GroupProvider({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyUp(event) {
|
function handleKeyUp(event: React.KeyboardEvent) {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
@ -9,7 +9,9 @@ import { useLiveQuery } from "dexie-react-hooks";
|
|||||||
|
|
||||||
import { useDatabase } from "./DatabaseContext";
|
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";
|
import { removeGroupsItems } from "../helpers/group";
|
||||||
|
|
||||||
|
@ -6,19 +6,26 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
|
RectEntry,
|
||||||
} from "@dnd-kit/core";
|
} 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 { useGroup } from "./GroupContext";
|
||||||
|
|
||||||
import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
|
import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
|
||||||
|
import Vector2 from "../helpers/Vector2";
|
||||||
|
|
||||||
import usePreventSelect from "../hooks/usePreventSelect";
|
import usePreventSelect from "../hooks/usePreventSelect";
|
||||||
|
|
||||||
const TileDragIdContext = React.createContext();
|
const TileDragIdContext =
|
||||||
const TileOverGroupIdContext = React.createContext();
|
React.createContext<string | undefined | null>(undefined);
|
||||||
const TileDragCursorContext = React.createContext();
|
const TileOverGroupIdContext =
|
||||||
|
React.createContext<string | undefined | null>(undefined);
|
||||||
|
const TileDragCursorContext =
|
||||||
|
React.createContext<string | undefined | null>(undefined);
|
||||||
|
|
||||||
export const BASE_SORTABLE_ID = "__base__";
|
export const BASE_SORTABLE_ID = "__base__";
|
||||||
export const GROUP_SORTABLE_ID = "__group__";
|
export const GROUP_SORTABLE_ID = "__group__";
|
||||||
@ -27,7 +34,7 @@ export const UNGROUP_ID = "__ungroup__";
|
|||||||
export const ADD_TO_MAP_ID = "__add__";
|
export const ADD_TO_MAP_ID = "__add__";
|
||||||
|
|
||||||
// Custom rectIntersect that takes a point
|
// Custom rectIntersect that takes a point
|
||||||
function rectIntersection(rects, point) {
|
function rectIntersection(rects: RectEntry[], point: Vector2) {
|
||||||
for (let rect of rects) {
|
for (let rect of rects) {
|
||||||
const [id, bounds] = rect;
|
const [id, bounds] = rect;
|
||||||
if (
|
if (
|
||||||
@ -44,13 +51,21 @@ function rectIntersection(rects, point) {
|
|||||||
return null;
|
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({
|
export function TileDragProvider({
|
||||||
onDragAdd,
|
onDragAdd,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
onDragCancel,
|
onDragCancel,
|
||||||
children,
|
children,
|
||||||
}) {
|
}: TileDragProviderProps) {
|
||||||
const {
|
const {
|
||||||
groups,
|
groups,
|
||||||
activeGroups,
|
activeGroups,
|
||||||
@ -71,23 +86,23 @@ export function TileDragProvider({
|
|||||||
|
|
||||||
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
||||||
|
|
||||||
const [dragId, setDragId] = useState(null);
|
const [dragId, setDragId] = useState<string | null>(null);
|
||||||
const [overId, setOverId] = useState(null);
|
const [overId, setOverId] = useState<string | null>(null);
|
||||||
const [dragCursor, setDragCursor] = useState("pointer");
|
const [dragCursor, setDragCursor] = useState("pointer");
|
||||||
|
|
||||||
const [preventSelect, resumeSelect] = usePreventSelect();
|
const [preventSelect, resumeSelect] = usePreventSelect();
|
||||||
|
|
||||||
const [overGroupId, setOverGroupId] = useState(null);
|
const [overGroupId, setOverGroupId] = useState<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOverGroupId(
|
setOverGroupId(
|
||||||
(overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9)) || null
|
(overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9)) || null
|
||||||
);
|
);
|
||||||
}, [overId]);
|
}, [overId]);
|
||||||
|
|
||||||
function handleDragStart(event) {
|
function handleDragStart(event: DragStartEvent) {
|
||||||
const { active, over } = event;
|
const { active } = event;
|
||||||
setDragId(active.id);
|
setDragId(active.id);
|
||||||
setOverId(over?.id || null);
|
setOverId(null);
|
||||||
if (!selectedGroupIds.includes(active.id)) {
|
if (!selectedGroupIds.includes(active.id)) {
|
||||||
onGroupSelect(active.id);
|
onGroupSelect(active.id);
|
||||||
}
|
}
|
||||||
@ -98,7 +113,7 @@ export function TileDragProvider({
|
|||||||
preventSelect();
|
preventSelect();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragOver(event) {
|
function handleDragOver(event: DragOverEvent) {
|
||||||
const { over } = event;
|
const { over } = event;
|
||||||
|
|
||||||
setOverId(over?.id || null);
|
setOverId(over?.id || null);
|
||||||
@ -116,7 +131,7 @@ export function TileDragProvider({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragEnd(event) {
|
function handleDragEnd(event: CustomDragEndEvent) {
|
||||||
const { active, over, overlayNodeClientRect } = event;
|
const { active, over, overlayNodeClientRect } = event;
|
||||||
|
|
||||||
setDragId(null);
|
setDragId(null);
|
||||||
@ -130,7 +145,7 @@ export function TileDragProvider({
|
|||||||
selectedIndices = selectedIndices.sort((a, b) => a - b);
|
selectedIndices = selectedIndices.sort((a, b) => a - b);
|
||||||
|
|
||||||
if (over.id.startsWith(GROUP_ID_PREFIX)) {
|
if (over.id.startsWith(GROUP_ID_PREFIX)) {
|
||||||
onGroupSelect();
|
onGroupSelect(undefined);
|
||||||
// Handle tile group
|
// Handle tile group
|
||||||
const overId = over.id.slice(9);
|
const overId = over.id.slice(9);
|
||||||
if (overId !== active.id) {
|
if (overId !== active.id) {
|
||||||
@ -143,10 +158,12 @@ export function TileDragProvider({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (over.id === UNGROUP_ID) {
|
} else if (over.id === UNGROUP_ID) {
|
||||||
onGroupSelect();
|
if (openGroupId) {
|
||||||
// Handle tile ungroup
|
onGroupSelect(undefined);
|
||||||
const newGroups = ungroup(groups, openGroupId, selectedIndices);
|
// Handle tile ungroup
|
||||||
onGroupsChange(newGroups);
|
const newGroups = ungroup(groups, openGroupId, selectedIndices);
|
||||||
|
onGroupsChange(newGroups, undefined);
|
||||||
|
}
|
||||||
} else if (over.id === ADD_TO_MAP_ID) {
|
} else if (over.id === ADD_TO_MAP_ID) {
|
||||||
onDragAdd &&
|
onDragAdd &&
|
||||||
overlayNodeClientRect &&
|
overlayNodeClientRect &&
|
||||||
@ -168,7 +185,7 @@ export function TileDragProvider({
|
|||||||
onDragEnd && onDragEnd(event);
|
onDragEnd && onDragEnd(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragCancel(event) {
|
function handleDragCancel(event: DragCancelEvent) {
|
||||||
setDragId(null);
|
setDragId(null);
|
||||||
setOverId(null);
|
setOverId(null);
|
||||||
setDragCursor("pointer");
|
setDragCursor("pointer");
|
||||||
@ -178,7 +195,7 @@ export function TileDragProvider({
|
|||||||
onDragCancel && onDragCancel(event);
|
onDragCancel && onDragCancel(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
function customCollisionDetection(rects, rect) {
|
function customCollisionDetection(rects: RectEntry[], rect: ViewRect) {
|
||||||
const rectCenter = {
|
const rectCenter = {
|
||||||
x: rect.left + rect.width / 2,
|
x: rect.left + rect.width / 2,
|
||||||
y: rect.top + rect.height / 2,
|
y: rect.top + rect.height / 2,
|
@ -1,12 +1,10 @@
|
|||||||
import React, { useEffect, useState, useContext } from "react";
|
import React, { useEffect, useState, useContext } from "react";
|
||||||
|
|
||||||
import { useDatabase } from "./DatabaseContext";
|
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 { database, databaseStatus } = useDatabase();
|
||||||
|
|
||||||
const [userId, setUserId] = useState();
|
const [userId, setUserId] = useState();
|
||||||
@ -15,9 +13,11 @@ export function UserIdProvider({ children }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
async function loadUserId() {
|
async function loadUserId() {
|
||||||
const storedUserId = await database.table("user").get("userId");
|
if (database) {
|
||||||
if (storedUserId) {
|
const storedUserId = await database.table("user").get("userId");
|
||||||
setUserId(storedUserId.value);
|
if (storedUserId) {
|
||||||
|
setUserId(storedUserId.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
|||||||
import { Vector3 } from "@babylonjs/core/Maths/math";
|
import { Vector3 } from "@babylonjs/core/Maths/math";
|
||||||
import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader";
|
import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader";
|
||||||
import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial";
|
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 d4Source from "./shared/d4.glb";
|
||||||
import d6Source from "./shared/d6.glb";
|
import d6Source from "./shared/d6.glb";
|
||||||
@ -13,7 +16,15 @@ import d100Source from "./shared/d100.glb";
|
|||||||
|
|
||||||
import { lerp } from "../helpers/shared";
|
import { lerp } from "../helpers/shared";
|
||||||
import { importTextureAsync } from "../helpers/babylon";
|
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 minDiceRollSpeed = 600;
|
||||||
const maxDiceRollSpeed = 800;
|
const maxDiceRollSpeed = 800;
|
||||||
@ -21,10 +32,16 @@ const maxDiceRollSpeed = 800;
|
|||||||
class Dice {
|
class Dice {
|
||||||
static instanceCount = 0;
|
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 = {};
|
let meshes: any = {};
|
||||||
const addToMeshes = async (type: string | number, defaultSource: 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);
|
const mesh = await this.loadMesh(source, material, scene);
|
||||||
meshes[type] = mesh;
|
meshes[type] = mesh;
|
||||||
};
|
};
|
||||||
@ -54,7 +71,11 @@ class Dice {
|
|||||||
|
|
||||||
static async loadMaterial(materialName: string, textures: any, scene: Scene) {
|
static async loadMaterial(materialName: string, textures: any, scene: Scene) {
|
||||||
let pbr = new PBRMaterial(materialName, 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.albedo),
|
||||||
importTextureAsync(textures.normal),
|
importTextureAsync(textures.normal),
|
||||||
importTextureAsync(textures.metalRoughness),
|
importTextureAsync(textures.metalRoughness),
|
||||||
@ -69,7 +90,12 @@ class Dice {
|
|||||||
return pbr;
|
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);
|
let instance = mesh.createInstance(name);
|
||||||
instance.position = mesh.position;
|
instance.position = mesh.position;
|
||||||
for (let child of mesh.getChildTransformNodes()) {
|
for (let child of mesh.getChildTransformNodes()) {
|
||||||
@ -77,7 +103,7 @@ class Dice {
|
|||||||
const locator: any = child.clone(child.name, instance);
|
const locator: any = child.clone(child.name, instance);
|
||||||
// TODO: handle possible null value
|
// TODO: handle possible null value
|
||||||
if (!locator) {
|
if (!locator) {
|
||||||
throw Error
|
throw Error;
|
||||||
}
|
}
|
||||||
locator.setAbsolutePosition(child.getAbsolutePosition());
|
locator.setAbsolutePosition(child.getAbsolutePosition());
|
||||||
locator.name = child.name;
|
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?.setLinearVelocity(Vector3.Zero());
|
||||||
instance.physicsImpostor?.setAngularVelocity(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++;
|
this.instanceCount++;
|
||||||
|
|
||||||
return this.createInstanceFromMesh(
|
return this.createInstanceFromMesh(
|
||||||
@ -166,6 +196,14 @@ class Dice {
|
|||||||
scene
|
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;
|
export default Dice;
|
||||||
|
@ -19,7 +19,9 @@ import GlassPreview from "./glass/preview.png";
|
|||||||
import GemstonePreview from "./gemstone/preview.png";
|
import GemstonePreview from "./gemstone/preview.png";
|
||||||
import Dice from "./Dice";
|
import Dice from "./Dice";
|
||||||
|
|
||||||
type DiceClasses = Record<string, Dice>;
|
import { DefaultDice } from "../types/Dice";
|
||||||
|
|
||||||
|
type DiceClasses = Record<string, typeof Dice>;
|
||||||
|
|
||||||
export const diceClasses: DiceClasses = {
|
export const diceClasses: DiceClasses = {
|
||||||
galaxy: GalaxyDice,
|
galaxy: GalaxyDice,
|
||||||
@ -45,7 +47,7 @@ export const dicePreviews: DicePreview = {
|
|||||||
gemstone: GemstonePreview,
|
gemstone: GemstonePreview,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const dice = Object.keys(diceClasses).map((key) => ({
|
export const dice: DefaultDice[] = Object.keys(diceClasses).map((key) => ({
|
||||||
key,
|
key,
|
||||||
name: Case.capital(key),
|
name: Case.capital(key),
|
||||||
class: diceClasses[key],
|
class: diceClasses[key],
|
||||||
|
@ -12,6 +12,7 @@ import d12Source from "./d12.glb";
|
|||||||
import d20Source from "./d20.glb";
|
import d20Source from "./d20.glb";
|
||||||
import d100Source from "./d100.glb";
|
import d100Source from "./d100.glb";
|
||||||
import { Material, Mesh, Scene } from "@babylonjs/core";
|
import { Material, Mesh, Scene } from "@babylonjs/core";
|
||||||
|
import { DiceType } from "../../types/Dice";
|
||||||
|
|
||||||
const sourceOverrides = {
|
const sourceOverrides = {
|
||||||
d4: d4Source,
|
d4: d4Source,
|
||||||
@ -24,7 +25,7 @@ const sourceOverrides = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class WalnutDice extends Dice {
|
class WalnutDice extends Dice {
|
||||||
static meshes: Record<string, Mesh>;
|
static meshes: Record<DiceType, Mesh>;
|
||||||
static material: Material;
|
static material: Material;
|
||||||
|
|
||||||
static getDicePhysicalProperties(diceType: string) {
|
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) {
|
if (!this.material || !this.meshes) {
|
||||||
throw Error("Dice not loaded, call load before creating an instance");
|
throw Error("Dice not loaded, call load before creating an instance");
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,12 @@ import {
|
|||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
|
||||||
export type BoundingBox = {
|
export type BoundingBox = {
|
||||||
min: Vector2,
|
min: Vector2;
|
||||||
max: Vector2,
|
max: Vector2;
|
||||||
width: number,
|
width: number;
|
||||||
height: number,
|
height: number;
|
||||||
center: Vector2
|
center: Vector2;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vector class with x, y and static helper methods
|
* Vector class with x, y and static helper methods
|
||||||
@ -287,7 +287,12 @@ class Vector2 {
|
|||||||
* @param {Vector2} C End of the curve
|
* @param {Vector2} C End of the curve
|
||||||
* @returns {Object} The distance to and the closest point on 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 distance = 0;
|
||||||
let point = { x: pos.x, y: pos.y };
|
let point = { x: pos.x, y: pos.y };
|
||||||
|
|
||||||
@ -514,7 +519,10 @@ class Vector2 {
|
|||||||
* @param {("counterClockwise"|"clockwise")=} direction Direction to rotate the vector
|
* @param {("counterClockwise"|"clockwise")=} direction Direction to rotate the vector
|
||||||
* @returns {Vector2}
|
* @returns {Vector2}
|
||||||
*/
|
*/
|
||||||
static rotate90(p: Vector2, direction: "counterClockwise" | "clockwise" = "clockwise"): Vector2 {
|
static rotate90(
|
||||||
|
p: Vector2,
|
||||||
|
direction: "counterClockwise" | "clockwise" = "clockwise"
|
||||||
|
): Vector2 {
|
||||||
if (direction === "clockwise") {
|
if (direction === "clockwise") {
|
||||||
return { x: p.y, y: -p.x };
|
return { x: p.y, y: -p.x };
|
||||||
} else {
|
} else {
|
||||||
@ -527,7 +535,7 @@ class Vector2 {
|
|||||||
* @param {Vector2[]} points
|
* @param {Vector2[]} points
|
||||||
* @returns {Vector2}
|
* @returns {Vector2}
|
||||||
*/
|
*/
|
||||||
static centroid(points) {
|
static centroid(points: Vector2[]): Vector2 {
|
||||||
let center = { x: 0, y: 0 };
|
let center = { x: 0, y: 0 };
|
||||||
for (let point of points) {
|
for (let point of points) {
|
||||||
center.x += point.x;
|
center.x += point.x;
|
||||||
@ -544,7 +552,7 @@ class Vector2 {
|
|||||||
* @param {Vector2[]} points
|
* @param {Vector2[]} points
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
static rectangular(points) {
|
static rectangular(points: Vector2[]): boolean {
|
||||||
if (points.length !== 4) {
|
if (points.length !== 4) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -567,7 +575,7 @@ class Vector2 {
|
|||||||
* @param {Vector2[]} points
|
* @param {Vector2[]} points
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
static circular(points, threshold = 0.1) {
|
static circular(points: Vector2[], threshold = 0.1): boolean {
|
||||||
const centroid = this.centroid(points);
|
const centroid = this.centroid(points);
|
||||||
let distances = [];
|
let distances = [];
|
||||||
for (let point of points) {
|
for (let point of points) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Vector3 } from "@babylonjs/core/Maths/math";
|
import { Vector3 } from "@babylonjs/core/Maths/math";
|
||||||
|
import { DiceRoll } from "../types/Dice";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the number facing up on a mesh instance of a 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 };
|
return { type: dice.type, roll: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDiceRollTotal(diceRolls: []) {
|
export function getDiceRollTotal(diceRolls: DiceRoll[]) {
|
||||||
return diceRolls.reduce((accumulator: number, dice: any) => {
|
return diceRolls.reduce((accumulator: number, dice: any) => {
|
||||||
if (dice.roll === "unknown") {
|
if (dice.roll === "unknown") {
|
||||||
return accumulator;
|
return accumulator;
|
||||||
|
@ -2,136 +2,19 @@ import simplify from "simplify-js";
|
|||||||
import polygonClipping, { Geom, Polygon, Ring } from "polygon-clipping";
|
import polygonClipping, { Geom, Polygon, Ring } from "polygon-clipping";
|
||||||
|
|
||||||
import Vector2, { BoundingBox } from "./Vector2";
|
import Vector2, { BoundingBox } from "./Vector2";
|
||||||
import Size from "./Size"
|
import Size from "./Size";
|
||||||
import { toDegrees } from "./shared";
|
import { toDegrees } from "./shared";
|
||||||
import { Grid, getNearestCellCoordinates, getCellLocation } from "./grid";
|
import { getNearestCellCoordinates, getCellLocation } from "./grid";
|
||||||
|
|
||||||
/**
|
import {
|
||||||
* @typedef PointsData
|
ShapeType,
|
||||||
* @property {Vector2[]} points
|
ShapeData,
|
||||||
*/
|
PointsData,
|
||||||
|
RectData,
|
||||||
type PointsData = {
|
CircleData,
|
||||||
points: Vector2[]
|
} from "../types/Drawing";
|
||||||
}
|
import { Fog } from "../types/Fog";
|
||||||
|
import { Grid } from "../types/Grid";
|
||||||
/**
|
|
||||||
* @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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -139,24 +22,26 @@ export type Fog = {
|
|||||||
* @param {Vector2} brushPosition
|
* @param {Vector2} brushPosition
|
||||||
* @returns {ShapeData}
|
* @returns {ShapeData}
|
||||||
*/
|
*/
|
||||||
export function getDefaultShapeData(type: ShapeType, brushPosition: Vector2): ShapeData | undefined{
|
export function getDefaultShapeData(
|
||||||
// TODO: handle undefined if no type found
|
type: ShapeType,
|
||||||
|
brushPosition: Vector2
|
||||||
|
): ShapeData {
|
||||||
if (type === "line") {
|
if (type === "line") {
|
||||||
return {
|
return {
|
||||||
points: [
|
points: [
|
||||||
{ x: brushPosition.x, y: brushPosition.y },
|
{ x: brushPosition.x, y: brushPosition.y },
|
||||||
{ x: brushPosition.x, y: brushPosition.y },
|
{ x: brushPosition.x, y: brushPosition.y },
|
||||||
],
|
],
|
||||||
} as PointsData;
|
};
|
||||||
} else if (type === "circle") {
|
} 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") {
|
} else if (type === "rectangle") {
|
||||||
return {
|
return {
|
||||||
x: brushPosition.x,
|
x: brushPosition.x,
|
||||||
y: brushPosition.y,
|
y: brushPosition.y,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
} as RectData;
|
};
|
||||||
} else if (type === "triangle") {
|
} else if (type === "triangle") {
|
||||||
return {
|
return {
|
||||||
points: [
|
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 },
|
||||||
{ 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,
|
gridCellNormalizedSize: Vector2,
|
||||||
mapWidth: number,
|
mapWidth: number,
|
||||||
mapHeight: number
|
mapHeight: number
|
||||||
): ShapeData | undefined {
|
): ShapeData {
|
||||||
// TODO: handle undefined type
|
|
||||||
if (type === "line") {
|
if (type === "line") {
|
||||||
data = data as PointsData;
|
data = data as PointsData;
|
||||||
return {
|
return {
|
||||||
points: [data.points[0], { x: brushPosition.x, y: brushPosition.y }],
|
points: [data.points[0], { x: brushPosition.x, y: brushPosition.y }],
|
||||||
} as PointsData;
|
} as PointsData;
|
||||||
} else if (type === "circle") {
|
} else if (type === "circle") {
|
||||||
data = data as CircleData;
|
data = data as CircleData;
|
||||||
const gridRatio = getGridCellRatio(gridCellNormalizedSize);
|
const gridRatio = getGridCellRatio(gridCellNormalizedSize);
|
||||||
const dif = Vector2.subtract(brushPosition, {
|
const dif = Vector2.subtract(brushPosition, {
|
||||||
x: data.x,
|
x: data.x,
|
||||||
@ -254,6 +140,8 @@ export function getUpdatedShapeData(
|
|||||||
Vector2.add(Vector2.multiply(rightDirNorm, sideLength), points[0]),
|
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 {Vector2[]} points
|
||||||
* @param {number} tolerance
|
* @param {number} tolerance
|
||||||
*/
|
*/
|
||||||
export function simplifyPoints(points: Vector2[], tolerance: number): Vector2[] {
|
export function simplifyPoints(
|
||||||
|
points: Vector2[],
|
||||||
|
tolerance: number
|
||||||
|
): Vector2[] {
|
||||||
return simplify(points, tolerance);
|
return simplify(points, tolerance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,7 +163,10 @@ export function simplifyPoints(points: Vector2[], tolerance: number): Vector2[]
|
|||||||
* @param {boolean} ignoreHidden
|
* @param {boolean} ignoreHidden
|
||||||
* @returns {Fog[]}
|
* @returns {Fog[]}
|
||||||
*/
|
*/
|
||||||
export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog[] {
|
export function mergeFogShapes(
|
||||||
|
shapes: Fog[],
|
||||||
|
ignoreHidden: boolean = true
|
||||||
|
): Fog[] {
|
||||||
if (shapes.length === 0) {
|
if (shapes.length === 0) {
|
||||||
return shapes;
|
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 shapePoints: Ring = shape.data.points.map(({ x, y }) => [x, y]);
|
||||||
const shapeHoles: Polygon = shape.data.holes.map((hole) =>
|
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]];
|
let shapeGeom: Geom = [[shapePoints, ...shapeHoles]];
|
||||||
geometries.push(shapeGeom);
|
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 })),
|
points: union[i][0].map(([x, y]) => ({ x, y })),
|
||||||
holes,
|
holes,
|
||||||
},
|
},
|
||||||
type: "fog"
|
type: "fog",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return merged;
|
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
|
* @param {boolean} maxPoints Max amount of points per shape to get bounds for
|
||||||
* @returns {Vector2.BoundingBox[]}
|
* @returns {Vector2.BoundingBox[]}
|
||||||
*/
|
*/
|
||||||
export function getFogShapesBoundingBoxes(shapes: Fog[], maxPoints = 0): BoundingBox[] {
|
export function getFogShapesBoundingBoxes(
|
||||||
|
shapes: Fog[],
|
||||||
|
maxPoints = 0
|
||||||
|
): BoundingBox[] {
|
||||||
let boxes = [];
|
let boxes = [];
|
||||||
for (let shape of shapes) {
|
for (let shape of shapes) {
|
||||||
if (maxPoints > 0 && shape.data.points.length > maxPoints) {
|
if (maxPoints > 0 && shape.data.points.length > maxPoints) {
|
||||||
@ -361,11 +258,11 @@ export function getFogShapesBoundingBoxes(shapes: Fog[], maxPoints = 0): Boundin
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
type Guide = {
|
type Guide = {
|
||||||
start: Vector2,
|
start: Vector2;
|
||||||
end: Vector2,
|
end: Vector2;
|
||||||
orientation: "horizontal" | "vertical",
|
orientation: "horizontal" | "vertical";
|
||||||
distance: number
|
distance: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Vector2} brushPosition Brush position in pixels
|
* @param {Vector2} brushPosition Brush position in pixels
|
||||||
@ -382,7 +279,7 @@ export function getGuidesFromGridCell(
|
|||||||
grid: Grid,
|
grid: Grid,
|
||||||
gridCellSize: Size,
|
gridCellSize: Size,
|
||||||
gridOffset: Vector2,
|
gridOffset: Vector2,
|
||||||
gridCellOffset: Vector2,
|
gridCellOffset: Vector2,
|
||||||
snappingSensitivity: number,
|
snappingSensitivity: number,
|
||||||
mapSize: Vector2
|
mapSize: Vector2
|
||||||
): Guide[] {
|
): Guide[] {
|
||||||
@ -500,7 +397,10 @@ export function getGuidesFromBoundingBoxes(
|
|||||||
* @param {Guide[]} guides
|
* @param {Guide[]} guides
|
||||||
* @returns {Guide[]}
|
* @returns {Guide[]}
|
||||||
*/
|
*/
|
||||||
export function findBestGuides(brushPosition: Vector2, guides: Guide[]): Guide[] {
|
export function findBestGuides(
|
||||||
|
brushPosition: Vector2,
|
||||||
|
guides: Guide[]
|
||||||
|
): Guide[] {
|
||||||
let bestGuides: Guide[] = [];
|
let bestGuides: Guide[] = [];
|
||||||
let verticalGuide = guides
|
let verticalGuide = guides
|
||||||
.filter((guide) => guide.orientation === "vertical")
|
.filter((guide) => guide.orientation === "vertical")
|
||||||
|
@ -8,42 +8,6 @@ import { logError } from "./logging";
|
|||||||
const SQRT3 = 1.73205;
|
const SQRT3 = 1.73205;
|
||||||
const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented");
|
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
|
* Gets the size of a grid in pixels taking into account the inset
|
||||||
* @param {Grid} grid
|
* @param {Grid} grid
|
||||||
@ -51,7 +15,11 @@ export type Grid = {
|
|||||||
* @param {number} baseHeight Height of the grid in pixels before inset
|
* @param {number} baseHeight Height of the grid in pixels before inset
|
||||||
* @returns {Size}
|
* @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 width = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * baseWidth;
|
||||||
const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight;
|
const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight;
|
||||||
return new Size(width, height);
|
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
|
* @param {number} gridHeight Height of the grid in pixels after inset
|
||||||
* @returns {Size}
|
* @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) {
|
if (grid.size.x === 0 || grid.size.y === 0) {
|
||||||
return new Size(0, 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
|
* @param {Size} cellSize Cell size in pixels
|
||||||
* @returns {Vector2}
|
* @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) {
|
switch (grid.type) {
|
||||||
case "square":
|
case "square":
|
||||||
return {
|
return {
|
||||||
@ -121,7 +98,12 @@ export function getCellLocation(grid: Grid, col: number, row: number, cellSize:
|
|||||||
* @param {Size} cellSize Cell size in pixels
|
* @param {Size} cellSize Cell size in pixels
|
||||||
* @returns {Vector2}
|
* @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) {
|
switch (grid.type) {
|
||||||
case "square":
|
case "square":
|
||||||
return Vector2.divide(Vector2.floorTo({ x, y }, cellSize), cellSize);
|
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
|
* @param {Size} cellSize Cell size in pixels
|
||||||
* @returns {Vector2[]}
|
* @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);
|
const position = new Vector2(x, y);
|
||||||
switch (grid.type) {
|
switch (grid.type) {
|
||||||
case "square":
|
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
|
* @param {number} gridWidth Width of the grid in pixels after inset
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
function getGridHeightFromWidth(grid: Grid, gridWidth: number): number{
|
function getGridHeightFromWidth(grid: Grid, gridWidth: number): number {
|
||||||
switch (grid.type) {
|
switch (grid.type) {
|
||||||
case "square":
|
case "square":
|
||||||
return (grid.size.y * gridWidth) / grid.size.x;
|
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
|
* @param {number} mapHeight Height of the map in pixels before inset
|
||||||
* @returns {GridInset}
|
* @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
|
// Max the width of the inset and figure out the resulting height value
|
||||||
const insetHeightNorm = getGridHeightFromWidth(grid, mapWidth) / mapHeight;
|
const insetHeightNorm = getGridHeightFromWidth(grid, mapWidth) / mapHeight;
|
||||||
return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: insetHeightNorm } };
|
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
|
* @param {number} mapHeight Height of the map in pixels before inset
|
||||||
* @returns {GridInset}
|
* @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 = {
|
let inset = {
|
||||||
topLeft: { ...grid.inset.topLeft },
|
topLeft: { ...grid.inset.topLeft },
|
||||||
bottomRight: { ...grid.inset.bottomRight },
|
bottomRight: { ...grid.inset.bottomRight },
|
||||||
@ -263,7 +258,10 @@ export function getGridMaxZoom(grid: Grid): number {
|
|||||||
* @param {("hexVertical"|"hexHorizontal")} type
|
* @param {("hexVertical"|"hexHorizontal")} type
|
||||||
* @returns {Vector2}
|
* @returns {Vector2}
|
||||||
*/
|
*/
|
||||||
export function hexCubeToOffset(cube: Vector3, type: ("hexVertical"|"hexHorizontal")) {
|
export function hexCubeToOffset(
|
||||||
|
cube: Vector3,
|
||||||
|
type: "hexVertical" | "hexHorizontal"
|
||||||
|
) {
|
||||||
if (type === "hexVertical") {
|
if (type === "hexVertical") {
|
||||||
const x = cube.x + (cube.z + (cube.z & 1)) / 2;
|
const x = cube.x + (cube.z + (cube.z & 1)) / 2;
|
||||||
const y = cube.z;
|
const y = cube.z;
|
||||||
@ -280,7 +278,10 @@ export function hexCubeToOffset(cube: Vector3, type: ("hexVertical"|"hexHorizont
|
|||||||
* @param {("hexVertical"|"hexHorizontal")} type
|
* @param {("hexVertical"|"hexHorizontal")} type
|
||||||
* @returns {Vector3}
|
* @returns {Vector3}
|
||||||
*/
|
*/
|
||||||
export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizontal")) {
|
export function hexOffsetToCube(
|
||||||
|
offset: Vector2,
|
||||||
|
type: "hexVertical" | "hexHorizontal"
|
||||||
|
) {
|
||||||
if (type === "hexVertical") {
|
if (type === "hexVertical") {
|
||||||
const x = offset.x - (offset.y + (offset.y & 1)) / 2;
|
const x = offset.x - (offset.y + (offset.y & 1)) / 2;
|
||||||
const z = offset.y;
|
const z = offset.y;
|
||||||
@ -301,7 +302,12 @@ export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizo
|
|||||||
* @param {Vector2} b
|
* @param {Vector2} b
|
||||||
* @param {Size} cellSize
|
* @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
|
// Get grid coordinates
|
||||||
const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize);
|
const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize);
|
||||||
const bCoord = getNearestCellCoordinates(grid, b.x, b.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);
|
const min: any = Vector2.min(delta);
|
||||||
return max - min + Math.floor(1.5 * min);
|
return max - min + Math.floor(1.5 * min);
|
||||||
} else if (grid.measurement.type === "euclidean") {
|
} 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") {
|
} else if (grid.measurement.type === "manhattan") {
|
||||||
return Math.abs(aCoord.x - bCoord.x) + Math.abs(aCoord.y - bCoord.y);
|
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
|
2
|
||||||
);
|
);
|
||||||
} else if (grid.measurement.type === "euclidean") {
|
} 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`
|
* Parse a string representation of scale e.g. 5ft into a `GridScale`
|
||||||
* @param {string} scale
|
* @param {string} scale
|
||||||
@ -441,7 +438,10 @@ export function gridSizeVaild(x: number, y: number): boolean {
|
|||||||
* @param {number[]} candidates
|
* @param {number[]} candidates
|
||||||
* @returns {Vector2 | null}
|
* @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
|
// TODO: check type for Image and CanvasSourceImage
|
||||||
const width: any = image.width;
|
const width: any = image.width;
|
||||||
const height: any = image.height;
|
const height: any = image.height;
|
||||||
@ -474,7 +474,10 @@ function gridSizeHeuristic(image: CanvasImageSource, candidates: number[]): Vect
|
|||||||
* @param {number[]} candidates
|
* @param {number[]} candidates
|
||||||
* @returns {Vector2 | null}
|
* @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
|
// TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match
|
||||||
const width: any = image.width;
|
const width: any = image.width;
|
||||||
const height: any = image.height;
|
const height: any = image.height;
|
||||||
|
@ -2,34 +2,14 @@ import { v4 as uuid } from "uuid";
|
|||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
|
||||||
import { keyBy } from "./shared";
|
import { keyBy } from "./shared";
|
||||||
|
import { Group, GroupContainer, GroupItem } from "../types/Group";
|
||||||
/**
|
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform an array of group ids to their groups
|
* 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 groupsByIds = keyBy(groups, "id");
|
||||||
const filteredGroups = [];
|
const filteredGroups: Group[] = [];
|
||||||
for (let groupId of groupIds) {
|
for (let groupId of groupIds) {
|
||||||
if (groupId in groupsByIds) {
|
if (groupId in groupsByIds) {
|
||||||
filteredGroups.push(groupsByIds[groupId]);
|
filteredGroups.push(groupsByIds[groupId]);
|
||||||
@ -40,10 +20,8 @@ export function groupsFromIds(groupIds, groups) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all items from a group including all sub 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") {
|
if (group.type === "group") {
|
||||||
let groups = [];
|
let groups = [];
|
||||||
for (let item of group.items) {
|
for (let item of group.items) {
|
||||||
@ -57,14 +35,14 @@ export function getGroupItems(group) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform an array of groups into their assosiated items
|
* 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 allItemsById = keyBy(allItems, itemKey);
|
||||||
const groupedItems = [];
|
const groupedItems: Item[] = [];
|
||||||
|
|
||||||
for (let group of groups) {
|
for (let group of groups) {
|
||||||
const groupItems = getGroupItems(group);
|
const groupItems = getGroupItems(group);
|
||||||
@ -76,47 +54,52 @@ export function itemsFromGroups(groups, allItems, itemKey = "id") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combine two groups
|
* Combine a group and a group item
|
||||||
* @param {Group} a
|
|
||||||
* @param {Group} b
|
|
||||||
* @returns {GroupContainer}
|
|
||||||
*/
|
*/
|
||||||
export function combineGroups(a, b) {
|
export function combineGroups(a: Group, b: Group): GroupContainer {
|
||||||
if (a.type === "item") {
|
switch (a.type) {
|
||||||
return {
|
case "item":
|
||||||
id: uuid(),
|
if (b.type !== "item") {
|
||||||
type: "group",
|
throw new Error("Unable to combine two GroupContainers");
|
||||||
items: [a, b],
|
}
|
||||||
name: "",
|
return {
|
||||||
};
|
id: uuid(),
|
||||||
}
|
type: "group",
|
||||||
if (a.type === "group") {
|
items: [a, b],
|
||||||
return {
|
name: "",
|
||||||
id: a.id,
|
};
|
||||||
type: "group",
|
case "group":
|
||||||
items: [...a.items, b],
|
if (b.type !== "item") {
|
||||||
name: a.name,
|
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`
|
* 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 newGroups = cloneDeep(groups);
|
||||||
|
|
||||||
const intoGroup = newGroups[into];
|
const intoGroup = newGroups[into];
|
||||||
let fromGroups = [];
|
let fromGroups: Group[] = [];
|
||||||
for (let i of indices) {
|
for (let i of indices) {
|
||||||
fromGroups.push(newGroups[i]);
|
fromGroups.push(newGroups[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let combined = intoGroup;
|
let combined: Group = intoGroup;
|
||||||
for (let fromGroup of fromGroups) {
|
for (let fromGroup of fromGroups) {
|
||||||
combined = combineGroups(combined, fromGroup);
|
combined = combineGroups(combined, fromGroup);
|
||||||
}
|
}
|
||||||
@ -133,12 +116,12 @@ export function moveGroupsInto(groups, into, indices) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutably move group at indices `indices` to index `to`
|
* 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);
|
const newGroups = cloneDeep(groups);
|
||||||
|
|
||||||
let fromGroups = [];
|
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
|
* Move items from a sub group to the start of the base group
|
||||||
* @param {Group[]} groups
|
* @param fromId The id of the group to move from
|
||||||
* @param {string} fromId The id of the group to move from
|
* @param indices The indices of the items in the group
|
||||||
* @param {number[]} 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);
|
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) {
|
for (let i of indices) {
|
||||||
items.push(newGroups[fromIndex].items[i]);
|
items.push(from.items[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove items from previous group
|
// Remove items from previous group
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
const i = newGroups[fromIndex].items.findIndex((el) => el.id === item.id);
|
const i = from.items.findIndex((el) => el.id === item.id);
|
||||||
newGroups[fromIndex].items.splice(i, 1);
|
from.items.splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have no more items in the group delete it
|
// 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);
|
newGroups.splice(fromIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,11 +179,8 @@ export function ungroup(groups, fromId, indices) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively find a group within a group array
|
* 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) {
|
for (let group of groups) {
|
||||||
if (group.id === groupId) {
|
if (group.id === groupId) {
|
||||||
return group;
|
return group;
|
||||||
@ -213,11 +196,9 @@ export function findGroup(groups, groupId) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform and item array to a record of item ids to item names
|
* 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") {
|
export function getItemNames(items: any[], itemKey: string = "id") {
|
||||||
let names = {};
|
let names: Record<string, string> = {};
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
names[item[itemKey]] = item.name;
|
names[item[itemKey]] = item.name;
|
||||||
}
|
}
|
||||||
@ -226,15 +207,20 @@ export function getItemNames(items, itemKey = "id") {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Immutably rename a group
|
* 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);
|
let newGroups = cloneDeep(groups);
|
||||||
const groupIndex = newGroups.findIndex((group) => group.id === groupId);
|
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) {
|
if (groupIndex >= 0) {
|
||||||
newGroups[groupIndex].name = newName;
|
group.name = newName;
|
||||||
}
|
}
|
||||||
return newGroups;
|
return newGroups;
|
||||||
}
|
}
|
||||||
@ -244,7 +230,7 @@ export function renameGroup(groups, groupId, newName) {
|
|||||||
* @param {Group[]} groups
|
* @param {Group[]} groups
|
||||||
* @param {string[]} itemIds
|
* @param {string[]} itemIds
|
||||||
*/
|
*/
|
||||||
export function removeGroupsItems(groups, itemIds) {
|
export function removeGroupsItems(groups: Group[], itemIds: string[]): Group[] {
|
||||||
let newGroups = cloneDeep(groups);
|
let newGroups = cloneDeep(groups);
|
||||||
|
|
||||||
for (let i = newGroups.length - 1; i >= 0; i--) {
|
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--) {
|
for (let j = items.length - 1; j >= 0; j--) {
|
||||||
const item = items[j];
|
const item = items[j];
|
||||||
if (itemIds.includes(item.id)) {
|
if (itemIds.includes(item.id)) {
|
||||||
newGroups[i].items.splice(j, 1);
|
group.items.splice(j, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Remove group if no items are left
|
// Remove group if no items are left
|
||||||
if (newGroups[i].items.length === 0) {
|
if (group.items.length === 0) {
|
||||||
newGroups.splice(i, 1);
|
newGroups.splice(i, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,13 +3,15 @@ import imageOutline from "image-outline";
|
|||||||
import blobToBuffer from "./blobToBuffer";
|
import blobToBuffer from "./blobToBuffer";
|
||||||
import Vector2 from "./Vector2";
|
import Vector2 from "./Vector2";
|
||||||
|
|
||||||
|
import { Outline } from "../types/Outline";
|
||||||
|
|
||||||
const lightnessDetectionOffset = 0.1;
|
const lightnessDetectionOffset = 0.1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLImageElement} image
|
* @param {HTMLImageElement} image
|
||||||
* @returns {boolean} True is the image is light
|
* @returns {boolean} True is the image is light
|
||||||
*/
|
*/
|
||||||
export function getImageLightness(image: HTMLImageElement) {
|
export function getImageLightness(image: HTMLImageElement): boolean {
|
||||||
const width = image.width;
|
const width = image.width;
|
||||||
const height = image.height;
|
const height = image.height;
|
||||||
let canvas = document.createElement("canvas");
|
let canvas = document.createElement("canvas");
|
||||||
@ -17,8 +19,7 @@ export function getImageLightness(image: HTMLImageElement) {
|
|||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
let context = canvas.getContext("2d");
|
let context = canvas.getContext("2d");
|
||||||
if (!context) {
|
if (!context) {
|
||||||
// TODO: handle if context is null
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.drawImage(image, 0, 0);
|
context.drawImage(image, 0, 0);
|
||||||
@ -45,18 +46,12 @@ export function getImageLightness(image: HTMLImageElement) {
|
|||||||
return norm + lightnessDetectionOffset >= 0;
|
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 = {
|
type CanvasImage = {
|
||||||
blob: Blob | null,
|
file: Uint8Array;
|
||||||
width: number,
|
width: number;
|
||||||
height: number
|
height: number;
|
||||||
}
|
mime: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLCanvasElement} canvas
|
* @param {HTMLCanvasElement} canvas
|
||||||
@ -64,11 +59,25 @@ type CanvasImage = {
|
|||||||
* @param {number} quality
|
* @param {number} quality
|
||||||
* @returns {Promise<CanvasImage>}
|
* @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) => {
|
return new Promise((resolve) => {
|
||||||
canvas.toBlob(
|
canvas.toBlob(
|
||||||
(blob) => {
|
async (blob) => {
|
||||||
resolve({ blob, width: canvas.width, height: canvas.height });
|
if (blob) {
|
||||||
|
const file = await blobToBuffer(blob);
|
||||||
|
resolve({
|
||||||
|
file,
|
||||||
|
width: canvas.width,
|
||||||
|
height: canvas.height,
|
||||||
|
mime: type,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
type,
|
type,
|
||||||
quality
|
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 {number} size the size of the longest edge of the new image
|
||||||
* @param {string} type the mime type of the 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
|
* @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 width = image.width;
|
||||||
const height = image.height;
|
const height = image.height;
|
||||||
const ratio = width / height;
|
const ratio = width / height;
|
||||||
@ -96,37 +110,27 @@ export async function resizeImage(image: HTMLImageElement, size: number, type: s
|
|||||||
canvas.height = size;
|
canvas.height = size;
|
||||||
}
|
}
|
||||||
let context = canvas.getContext("2d");
|
let context = canvas.getContext("2d");
|
||||||
// TODO: Add error if context is empty
|
|
||||||
if (context) {
|
if (context) {
|
||||||
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
return await canvasToImage(canvas, type, quality);
|
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
|
* Create a image file with resolution `size`x`size` with cover cropping
|
||||||
* @param {HTMLImageElement} image the image to resize
|
* @param {HTMLImageElement} image the image to resize
|
||||||
* @param {string} type the mime type of the image
|
* @param {string} type the mime type of the image
|
||||||
* @param {number} size the width and height of the thumbnail
|
* @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
|
* @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");
|
let canvas = document.createElement("canvas");
|
||||||
canvas.width = size;
|
canvas.width = size;
|
||||||
canvas.height = size;
|
canvas.height = size;
|
||||||
@ -166,55 +170,20 @@ export async function createThumbnail(image: HTMLImageElement, type: string, siz
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumbnailImage = await canvasToImage(canvas, type, quality);
|
return 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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @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
|
* Get the outline of an image
|
||||||
* @param {HTMLImageElement} image
|
* @param {HTMLImageElement} image
|
||||||
* @returns {Outline}
|
* @returns {Outline}
|
||||||
*/
|
*/
|
||||||
export function getImageOutline(image, maxPoints = 100) {
|
export function getImageOutline(
|
||||||
|
image: HTMLImageElement,
|
||||||
|
maxPoints: number = 100
|
||||||
|
): Outline {
|
||||||
// Basic rect outline for fail conditions
|
// Basic rect outline for fail conditions
|
||||||
const defaultOutline = {
|
const defaultOutline: Outline = {
|
||||||
type: "rect",
|
type: "rect",
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -10,14 +10,16 @@ import {
|
|||||||
} from "./grid";
|
} from "./grid";
|
||||||
import Vector2 from "./Vector2";
|
import Vector2 from "./Vector2";
|
||||||
|
|
||||||
const defaultMapProps = {
|
import { Map, FileMapResolutions, FileMap } from "../types/Map";
|
||||||
showGrid: false,
|
import { Asset } from "../types/Asset";
|
||||||
snapToGrid: true,
|
|
||||||
quality: "original",
|
type Resolution = {
|
||||||
group: "",
|
size: number;
|
||||||
|
quality: number;
|
||||||
|
id: "low" | "medium" | "high" | "ultra";
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapResolutions = [
|
const mapResolutions: Resolution[] = [
|
||||||
{
|
{
|
||||||
size: 30, // Pixels per grid
|
size: 30, // Pixels per grid
|
||||||
quality: 0.5, // JPEG compression quality
|
quality: 0.5, // JPEG compression quality
|
||||||
@ -33,30 +35,35 @@ const mapResolutions = [
|
|||||||
* @param {any} map
|
* @param {any} map
|
||||||
* @returns {undefined|string}
|
* @returns {undefined|string}
|
||||||
*/
|
*/
|
||||||
export function getMapPreviewAsset(map) {
|
export function getMapPreviewAsset(map: Map): string | undefined {
|
||||||
const res = map.resolutions;
|
if (map.type === "file") {
|
||||||
switch (map.quality) {
|
const res = map.resolutions;
|
||||||
case "low":
|
switch (map.quality) {
|
||||||
return;
|
case "low":
|
||||||
case "medium":
|
return;
|
||||||
return res.low;
|
case "medium":
|
||||||
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 res.low;
|
||||||
}
|
case "high":
|
||||||
return;
|
return res.medium;
|
||||||
default:
|
case "ultra":
|
||||||
return;
|
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();
|
let image = new Image();
|
||||||
|
|
||||||
const buffer = await blobToBuffer(file);
|
const buffer = await blobToBuffer(file);
|
||||||
@ -107,10 +114,10 @@ export async function createMapFromFile(file, userId) {
|
|||||||
gridSize = { x: 22, y: 22 };
|
gridSize = { x: 22, y: 22 };
|
||||||
}
|
}
|
||||||
|
|
||||||
let assets = [];
|
let assets: Asset[] = [];
|
||||||
|
|
||||||
// Create resolutions
|
// Create resolutions
|
||||||
const resolutions = {};
|
const resolutions: FileMapResolutions = {};
|
||||||
for (let resolution of mapResolutions) {
|
for (let resolution of mapResolutions) {
|
||||||
const resolutionPixelSize = Vector2.multiply(gridSize, resolution.size);
|
const resolutionPixelSize = Vector2.multiply(gridSize, resolution.size);
|
||||||
if (
|
if (
|
||||||
@ -119,20 +126,16 @@ export async function createMapFromFile(file, userId) {
|
|||||||
) {
|
) {
|
||||||
const resized = await resizeImage(
|
const resized = await resizeImage(
|
||||||
image,
|
image,
|
||||||
Vector2.max(resolutionPixelSize),
|
Vector2.max(resolutionPixelSize) as number,
|
||||||
file.type,
|
file.type,
|
||||||
resolution.quality
|
resolution.quality
|
||||||
);
|
);
|
||||||
if (resized.blob) {
|
if (resized) {
|
||||||
const assetId = uuid();
|
const assetId = uuid();
|
||||||
resolutions[resolution.id] = assetId;
|
resolutions[resolution.id] = assetId;
|
||||||
const resizedBuffer = await blobToBuffer(resized.blob);
|
|
||||||
const asset = {
|
const asset = {
|
||||||
file: resizedBuffer,
|
...resized,
|
||||||
width: resized.width,
|
|
||||||
height: resized.height,
|
|
||||||
id: assetId,
|
id: assetId,
|
||||||
mime: file.type,
|
|
||||||
owner: userId,
|
owner: userId,
|
||||||
};
|
};
|
||||||
assets.push(asset);
|
assets.push(asset);
|
||||||
@ -141,12 +144,11 @@ export async function createMapFromFile(file, userId) {
|
|||||||
}
|
}
|
||||||
// Create thumbnail
|
// Create thumbnail
|
||||||
const thumbnailImage = await createThumbnail(image, file.type);
|
const thumbnailImage = await createThumbnail(image, file.type);
|
||||||
const thumbnail = {
|
const thumbnailId = uuid();
|
||||||
...thumbnailImage,
|
if (thumbnailImage) {
|
||||||
id: uuid(),
|
const thumbnail = { ...thumbnailImage, id: thumbnailId, owner: userId };
|
||||||
owner: userId,
|
assets.push(thumbnail);
|
||||||
};
|
}
|
||||||
assets.push(thumbnail);
|
|
||||||
|
|
||||||
const fileAsset = {
|
const fileAsset = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@ -158,11 +160,11 @@ export async function createMapFromFile(file, userId) {
|
|||||||
};
|
};
|
||||||
assets.push(fileAsset);
|
assets.push(fileAsset);
|
||||||
|
|
||||||
const map = {
|
const map: FileMap = {
|
||||||
name,
|
name,
|
||||||
resolutions,
|
resolutions,
|
||||||
file: fileAsset.id,
|
file: fileAsset.id,
|
||||||
thumbnail: thumbnail.id,
|
thumbnail: thumbnailId,
|
||||||
type: "file",
|
type: "file",
|
||||||
grid: {
|
grid: {
|
||||||
size: gridSize,
|
size: gridSize,
|
||||||
@ -183,7 +185,9 @@ export async function createMapFromFile(file, userId) {
|
|||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
owner: userId,
|
owner: userId,
|
||||||
...defaultMapProps,
|
showGrid: false,
|
||||||
|
snapToGrid: true,
|
||||||
|
quality: "original",
|
||||||
};
|
};
|
||||||
|
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
@ -1,5 +1,5 @@
|
|||||||
export function omit(obj:object, keys: string[]) {
|
export function omit(obj: Record<PropertyKey, any>, keys: string[]) {
|
||||||
let tmp: { [key: string]: any } = {};
|
let tmp: Record<PropertyKey, any> = {};
|
||||||
for (let [key, value] of Object.entries(obj)) {
|
for (let [key, value] of Object.entries(obj)) {
|
||||||
if (keys.includes(key)) {
|
if (keys.includes(key)) {
|
||||||
continue;
|
continue;
|
||||||
@ -9,18 +9,21 @@ export function omit(obj:object, keys: string[]) {
|
|||||||
return tmp;
|
return tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromEntries(iterable: any) {
|
export function fromEntries(iterable: Iterable<[string | number, any]>) {
|
||||||
if (Object.fromEntries) {
|
if (Object.fromEntries) {
|
||||||
return Object.fromEntries(iterable);
|
return Object.fromEntries(iterable);
|
||||||
}
|
}
|
||||||
return [...iterable].reduce((obj, [key, val]) => {
|
return [...iterable].reduce(
|
||||||
obj[key] = val;
|
(obj: Record<string | number, any>, [key, val]) => {
|
||||||
return obj;
|
obj[key] = val;
|
||||||
}, {});
|
return obj;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if all tracks are muted
|
// 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 });
|
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);
|
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;
|
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(
|
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) {
|
export function groupBy(array: Record<PropertyKey, any>[], key: string) {
|
||||||
return array.reduce((prev: any, current: any) => {
|
return array.reduce((prev: Record<string, any[]>, current) => {
|
||||||
const k = current[key];
|
const k = current[key];
|
||||||
(prev[k] || (prev[k] = [])).push(current);
|
(prev[k] || (prev[k] = [])).push(current);
|
||||||
return prev;
|
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 const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
||||||
|
|
||||||
export function shuffle(array) {
|
export function shuffle<Type>(array: Type[]) {
|
||||||
let temp = [...array];
|
let temp = [...array];
|
||||||
var currentIndex = temp.length,
|
var currentIndex = temp.length,
|
||||||
randomIndex;
|
randomIndex;
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import Case from "case";
|
import Case from "case";
|
||||||
|
import { Stage } from "konva/types/Stage";
|
||||||
|
|
||||||
import blobToBuffer from "./blobToBuffer";
|
import blobToBuffer from "./blobToBuffer";
|
||||||
import { createThumbnail, getImageOutline } from "./image";
|
import { createThumbnail, getImageOutline } from "./image";
|
||||||
import Vector2 from "./Vector2";
|
import Vector2 from "./Vector2";
|
||||||
|
|
||||||
export function createTokenState(token, position, userId) {
|
import { Token, FileToken } from "../types/Token";
|
||||||
let tokenState = {
|
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(),
|
id: uuid(),
|
||||||
tokenId: token.id,
|
tokenId: token.id,
|
||||||
owner: userId,
|
owner: userId,
|
||||||
@ -21,20 +31,29 @@ export function createTokenState(token, position, userId) {
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
locked: false,
|
locked: false,
|
||||||
visible: true,
|
visible: true,
|
||||||
type: token.type,
|
|
||||||
outline: token.outline,
|
outline: token.outline,
|
||||||
width: token.width,
|
width: token.width,
|
||||||
height: token.height,
|
height: token.height,
|
||||||
};
|
};
|
||||||
if (token.type === "file") {
|
if (token.type === "file") {
|
||||||
tokenState.file = token.file;
|
return {
|
||||||
} else if (token.type === "default") {
|
...tokenState,
|
||||||
tokenState.key = token.key;
|
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) {
|
if (!file) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
@ -77,10 +96,13 @@ export async function createTokenFromFile(file, userId) {
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
image.onload = async function () {
|
image.onload = async function () {
|
||||||
let assets = [];
|
let assets: Asset[] = [];
|
||||||
const thumbnailImage = await createThumbnail(image, file.type);
|
const thumbnailImage = await createThumbnail(image, file.type);
|
||||||
const thumbnail = { ...thumbnailImage, id: uuid(), owner: userId };
|
const thumbnailId = uuid();
|
||||||
assets.push(thumbnail);
|
if (thumbnailImage) {
|
||||||
|
const thumbnail = { ...thumbnailImage, id: thumbnailId, owner: userId };
|
||||||
|
assets.push(thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
const fileAsset = {
|
const fileAsset = {
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@ -94,10 +116,10 @@ export async function createTokenFromFile(file, userId) {
|
|||||||
|
|
||||||
const outline = getImageOutline(image);
|
const outline = getImageOutline(image);
|
||||||
|
|
||||||
const token = {
|
const token: FileToken = {
|
||||||
name,
|
name,
|
||||||
defaultSize,
|
defaultSize,
|
||||||
thumbnail: thumbnail.id,
|
thumbnail: thumbnailId,
|
||||||
file: fileAsset.id,
|
file: fileAsset.id,
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: "file",
|
type: "file",
|
||||||
@ -107,7 +129,6 @@ export async function createTokenFromFile(file, userId) {
|
|||||||
defaultCategory: "character",
|
defaultCategory: "character",
|
||||||
defaultLabel: "",
|
defaultLabel: "",
|
||||||
hideInSidebar: false,
|
hideInSidebar: false,
|
||||||
group: "",
|
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
height: image.height,
|
||||||
outline,
|
outline,
|
||||||
@ -122,12 +143,15 @@ export async function createTokenFromFile(file, userId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function clientPositionToMapPosition(
|
export function clientPositionToMapPosition(
|
||||||
mapStage,
|
mapStage: Stage,
|
||||||
clientPosition,
|
clientPosition: Vector2,
|
||||||
checkMapBounds = true
|
checkMapBounds = true
|
||||||
) {
|
): Vector2 | undefined {
|
||||||
const mapImage = mapStage.findOne("#mapImage");
|
const mapImage = mapStage.findOne("#mapImage");
|
||||||
const map = document.querySelector(".map");
|
const map = document.querySelector(".map");
|
||||||
|
if (!map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const mapRect = map.getBoundingClientRect();
|
const mapRect = map.getBoundingClientRect();
|
||||||
|
|
||||||
// Check map bounds
|
// Check map bounds
|
||||||
@ -158,7 +182,11 @@ export function clientPositionToMapPosition(
|
|||||||
return normalizedPosition;
|
return normalizedPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getScaledOutline(tokenState, tokenWidth, tokenHeight) {
|
export function getScaledOutline(
|
||||||
|
tokenState: TokenState,
|
||||||
|
tokenWidth: number,
|
||||||
|
tokenHeight: number
|
||||||
|
): Outline {
|
||||||
let outline = tokenState.outline;
|
let outline = tokenState.outline;
|
||||||
if (outline.type === "rect") {
|
if (outline.type === "rect") {
|
||||||
return {
|
return {
|
||||||
@ -187,14 +215,23 @@ export function getScaledOutline(tokenState, tokenWidth, tokenHeight) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Intersection {
|
export class Intersection {
|
||||||
|
outline;
|
||||||
|
position;
|
||||||
|
center;
|
||||||
|
rotation;
|
||||||
|
points: Vector2[] | undefined;
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {Outline} outline
|
* @param {Outline} outline
|
||||||
* @param {Vector2} position - Top left position of the token
|
* @param {Vector2} position - Top left position of the token
|
||||||
* @param {Vector2} center - Center position of the token
|
* @param {Vector2} center - Center position of the token
|
||||||
* @param {number} rotation - Rotation of the token in degrees
|
* @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.outline = outline;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.center = center;
|
this.center = center;
|
||||||
@ -253,8 +290,11 @@ export class Intersection {
|
|||||||
* @param {Vector2} point
|
* @param {Vector2} point
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
intersects(point) {
|
intersects(point: Vector2) {
|
||||||
if (this.outline.type === "rect" || this.outline.type === "path") {
|
if (
|
||||||
|
this.points &&
|
||||||
|
(this.outline.type === "rect" || this.outline.type === "path")
|
||||||
|
) {
|
||||||
return Vector2.pointInPolygon(point, this.points);
|
return Vector2.pointInPolygon(point, this.points);
|
||||||
} else if (this.outline.type === "circle") {
|
} else if (this.outline.type === "circle") {
|
||||||
return Vector2.distance(this.center, point) < this.outline.radius;
|
return Vector2.distance(this.center, point) < this.outline.radius;
|
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
function useDebounce(value: any, delay: number): any {
|
function useDebounce<Type>(value: Type, delay: number): Type {
|
||||||
const [debouncedValue, setDebouncedValue] = useState();
|
const [debouncedValue, setDebouncedValue] = useState<Type>(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
|
@ -5,7 +5,12 @@ import { useAuth } from "../contexts/AuthContext";
|
|||||||
|
|
||||||
import Modal from "../components/Modal";
|
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 { password, setPassword } = useAuth();
|
||||||
const [tmpPassword, setTempPassword] = useState<string>(password);
|
const [tmpPassword, setTempPassword] = useState<string>(password);
|
||||||
|
|
||||||
@ -19,7 +24,7 @@ function AuthModal({ isOpen, onSubmit }: { isOpen: boolean, onSubmit: (newPassw
|
|||||||
onSubmit(tmpPassword);
|
onSubmit(tmpPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputRef = useRef<any>();
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
function focusInput(): void {
|
function focusInput(): void {
|
||||||
inputRef.current && inputRef.current?.focus();
|
inputRef.current && inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
|
@ -3,22 +3,24 @@ import { Box, Input, Button, Label, Flex } from "theme-ui";
|
|||||||
|
|
||||||
import Modal from "../components/Modal";
|
import Modal from "../components/Modal";
|
||||||
|
|
||||||
|
type ChangeNicknameModalProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onRequestClose: () => void;
|
||||||
|
onChangeSubmit: any;
|
||||||
|
nickname: string;
|
||||||
|
onChange: any;
|
||||||
|
};
|
||||||
|
|
||||||
function ChangeNicknameModal({
|
function ChangeNicknameModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
onChangeSubmit,
|
onChangeSubmit,
|
||||||
nickname,
|
nickname,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: ChangeNicknameModalProps) {
|
||||||
isOpen: boolean,
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
onRequestClose: () => void,
|
|
||||||
onChangeSubmit: any,
|
|
||||||
nickname: string,
|
|
||||||
onChange: any,
|
|
||||||
}) {
|
|
||||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
||||||
function focusInput() {
|
function focusInput() {
|
||||||
inputRef.current && inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -3,13 +3,13 @@ import { Box, Label, Flex, Button, Text } from "theme-ui";
|
|||||||
import Modal from "../components/Modal";
|
import Modal from "../components/Modal";
|
||||||
|
|
||||||
type ConfirmModalProps = {
|
type ConfirmModalProps = {
|
||||||
isOpen: boolean,
|
isOpen: boolean;
|
||||||
onRequestClose: () => void,
|
onRequestClose: () => void;
|
||||||
onConfirm: () => void,
|
onConfirm: () => void;
|
||||||
confirmText: string,
|
confirmText: string;
|
||||||
label: string,
|
label: string;
|
||||||
description: string,
|
description: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
function ConfirmModal({
|
function ConfirmModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -18,7 +18,7 @@ function ConfirmModal({
|
|||||||
confirmText,
|
confirmText,
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
}: ConfirmModalProps ) {
|
}: ConfirmModalProps) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
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 { getGridDefaultInset } from "../helpers/grid";
|
||||||
|
|
||||||
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
||||||
import { Map, MapState } from "../components/map/Map";
|
import { Map } from "../types/Map";
|
||||||
|
import { MapState } from "../types/MapState";
|
||||||
|
|
||||||
type EditMapProps = {
|
type EditMapProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -145,7 +146,7 @@ function EditMapModal({
|
|||||||
style={{
|
style={{
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
padding: "16px",
|
padding: "16px",
|
||||||
backgroundColor: theme.colors.muted,
|
backgroundColor: theme.colors?.muted as string,
|
||||||
margin: "0 8px",
|
margin: "0 8px",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
|
@ -9,7 +9,7 @@ import TokenPreview from "../components/token/TokenPreview";
|
|||||||
import { isEmpty } from "../helpers/shared";
|
import { isEmpty } from "../helpers/shared";
|
||||||
|
|
||||||
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
||||||
import { Token } from "../tokens";
|
import { Token } from "../types/Token";
|
||||||
|
|
||||||
type EditModalProps = {
|
type EditModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -98,7 +98,7 @@ function EditTokenModal({
|
|||||||
style={{
|
style={{
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
padding: "16px",
|
padding: "16px",
|
||||||
backgroundColor: theme.colors.muted,
|
backgroundColor: theme.colors?.muted as string,
|
||||||
margin: "0 8px",
|
margin: "0 8px",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
|
@ -2,7 +2,12 @@ import { Box, Label, Flex, Button, Text } from "theme-ui";
|
|||||||
|
|
||||||
import Modal from "../components/Modal";
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
@ -7,7 +7,15 @@ import Link from "../components/Link";
|
|||||||
|
|
||||||
const gettingStarted = raw("../docs/howTo/gettingStarted.md");
|
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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
@ -3,25 +3,37 @@ import { Box, Input, Button, Label, Flex } from "theme-ui";
|
|||||||
|
|
||||||
import Modal from "../components/Modal";
|
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);
|
const [tmpName, setTempName] = useState(name);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTempName(name);
|
setTempName(name);
|
||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
function handleChange(event) {
|
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
setTempName(event.target.value);
|
setTempName(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(event) {
|
function handleSubmit(event: React.FormEvent<HTMLDivElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onSubmit(tmpName);
|
onSubmit(tmpName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputRef = useRef();
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
function focusInput() {
|
function focusInput() {
|
||||||
inputRef.current && inputRef.current.focus();
|
inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
@ -14,11 +14,13 @@ import ErrorBanner from "../components/banner/ErrorBanner";
|
|||||||
import { useUserId } from "../contexts/UserIdContext";
|
import { useUserId } from "../contexts/UserIdContext";
|
||||||
import { useDatabase } from "../contexts/DatabaseContext";
|
import { useDatabase } from "../contexts/DatabaseContext";
|
||||||
|
|
||||||
import SelectDataModal from "./SelectDataModal";
|
import SelectDataModal, { SelectData } from "./SelectDataModal";
|
||||||
|
|
||||||
import { getDatabase } from "../database";
|
import { getDatabase } from "../database";
|
||||||
import { Map, MapState, TokenState } from "../components/map/Map";
|
import { Map } from "../types/Map";
|
||||||
import { Token } from "../tokens";
|
import { MapState } from "../types/MapState";
|
||||||
|
import { Token } from "../types/Token";
|
||||||
|
import { Group } from "../types/Group";
|
||||||
|
|
||||||
const importDBName = "OwlbearRodeoImportDB";
|
const importDBName = "OwlbearRodeoImportDB";
|
||||||
|
|
||||||
@ -49,7 +51,11 @@ function ImportExportModal({
|
|||||||
const [showExportSelector, setShowExportSelector] = useState(false);
|
const [showExportSelector, setShowExportSelector] = useState(false);
|
||||||
|
|
||||||
const { addToast } = useToasts();
|
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 mapText = `${maps.length} map${maps.length > 1 ? "s" : ""}`;
|
||||||
const tokenText = `${tokens.length} token${tokens.length > 1 ? "s" : ""}`;
|
const tokenText = `${tokens.length} token${tokens.length > 1 ? "s" : ""}`;
|
||||||
if (maps.length > 0 && tokens.length > 0) {
|
if (maps.length > 0 && tokens.length > 0) {
|
||||||
@ -145,10 +151,10 @@ function ImportExportModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleImportSelectorConfirm(
|
async function handleImportSelectorConfirm(
|
||||||
checkedMaps: Map[],
|
checkedMaps: SelectData[],
|
||||||
checkedTokens: Token[],
|
checkedTokens: SelectData[],
|
||||||
checkedMapGroups: any[],
|
checkedMapGroups: Group[],
|
||||||
checkedTokenGroups: any[]
|
checkedTokenGroups: Group[]
|
||||||
) {
|
) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
backgroundTaskRunningRef.current = true;
|
backgroundTaskRunningRef.current = true;
|
||||||
@ -204,16 +210,15 @@ function ImportExportModal({
|
|||||||
// Apply new token ids to imported state
|
// Apply new token ids to imported state
|
||||||
for (let tokenState of Object.values(state.tokens)) {
|
for (let tokenState of Object.values(state.tokens)) {
|
||||||
if (tokenState.tokenId in newTokenIds) {
|
if (tokenState.tokenId in newTokenIds) {
|
||||||
state.tokens[tokenState.id].tokenId =
|
tokenState.tokenId = newTokenIds[tokenState.tokenId];
|
||||||
newTokenIds[tokenState.tokenId];
|
|
||||||
}
|
}
|
||||||
// Change token state file asset id
|
// Change token state file asset id
|
||||||
if (tokenState.type === "file" && tokenState.file in newAssetIds) {
|
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
|
// Change token state owner if owned by the user of the map
|
||||||
if (tokenState.owner === map.owner) {
|
if (tokenState.owner === map.owner && userId) {
|
||||||
state.tokens[tokenState.id].owner = userId;
|
tokenState.owner = userId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Generate new ids
|
// Generate new ids
|
||||||
@ -368,8 +373,8 @@ function ImportExportModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleExportSelectorConfirm(
|
async function handleExportSelectorConfirm(
|
||||||
checkedMaps: Map[],
|
checkedMaps: SelectData[],
|
||||||
checkedTokens: TokenState[]
|
checkedTokens: SelectData[]
|
||||||
) {
|
) {
|
||||||
setShowExportSelector(false);
|
setShowExportSelector(false);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
@ -4,7 +4,12 @@ import { useHistory } from "react-router-dom";
|
|||||||
|
|
||||||
import Modal from "../components/Modal";
|
import Modal from "../components/Modal";
|
||||||
|
|
||||||
function JoinModal({ isOpen, onRequestClose }: any) {
|
type JoinModalProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onRequestClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function JoinModal({ isOpen, onRequestClose }: JoinModalProps) {
|
||||||
let history = useHistory();
|
let history = useHistory();
|
||||||
const [gameId, setGameId] = useState("");
|
const [gameId, setGameId] = useState("");
|
||||||
|
|
||||||
@ -17,9 +22,9 @@ function JoinModal({ isOpen, onRequestClose }: any) {
|
|||||||
history.push(`/game/${gameId}`);
|
history.push(`/game/${gameId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputRef = useRef<any>();
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
function focusInput() {
|
function focusInput() {
|
||||||
inputRef.current && inputRef.current.focus();
|
inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -27,7 +32,6 @@ function JoinModal({ isOpen, onRequestClose }: any) {
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
onAfterOpen={focusInput}
|
onAfterOpen={focusInput}
|
||||||
|
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -8,23 +8,35 @@ import Divider from "../components/Divider";
|
|||||||
|
|
||||||
import { getDatabase } from "../database";
|
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 = {
|
type SelectDataProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onRequestClose: () => void;
|
onRequestClose: () => void;
|
||||||
onConfirm: any;
|
onConfirm: (
|
||||||
|
checkedMaps: SelectData[],
|
||||||
|
checkedTokens: SelectData[],
|
||||||
|
checkedMapGroups: Group[],
|
||||||
|
checkedTokenGroups: Group[]
|
||||||
|
) => void;
|
||||||
confirmText: string;
|
confirmText: string;
|
||||||
label: string;
|
label: string;
|
||||||
databaseName: string;
|
databaseName: string;
|
||||||
filter: any;
|
filter: (table: string, data: Map | MapState | Token, id: string) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SelectData = {
|
export type SelectData = {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
type: "default" | "file";
|
type: "default" | "file";
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DataRecord = Record<string, SelectData>;
|
||||||
|
|
||||||
function SelectDataModal({
|
function SelectDataModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
@ -34,13 +46,13 @@ function SelectDataModal({
|
|||||||
databaseName,
|
databaseName,
|
||||||
filter,
|
filter,
|
||||||
}: SelectDataProps) {
|
}: SelectDataProps) {
|
||||||
const [maps, setMaps] = useState<Record<string, SelectData>>({});
|
const [maps, setMaps] = useState<DataRecord>({});
|
||||||
const [mapGroups, setMapGroups] = useState<any[]>([]);
|
const [mapGroups, setMapGroups] = useState<Group[]>([]);
|
||||||
const [tokensByMap, setTokensByMap] = useState<Record<string, Set<string>>>(
|
const [tokensByMap, setTokensByMap] = useState<Record<string, Set<string>>>(
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const [tokens, setTokens] = useState<Record<string, SelectData>>({});
|
const [tokens, setTokens] = useState<DataRecord>({});
|
||||||
const [tokenGroups, setTokenGroups] = useState<any[]>([]);
|
const [tokenGroups, setTokenGroups] = useState<Group[]>([]);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const hasMaps = Object.values(maps).length > 0;
|
const hasMaps = Object.values(maps).length > 0;
|
||||||
@ -51,13 +63,13 @@ function SelectDataModal({
|
|||||||
if (isOpen && databaseName) {
|
if (isOpen && databaseName) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const db = getDatabase({ addons: [] }, databaseName);
|
const db = getDatabase({ addons: [] }, databaseName);
|
||||||
let loadedMaps: Record<string, SelectData> = {};
|
let loadedMaps: DataRecord = {};
|
||||||
let loadedTokensByMap: Record<string, Set<string>> = {};
|
let loadedTokensByMap: Record<string, Set<string>> = {};
|
||||||
let loadedTokens: Record<string, SelectData> = {};
|
let loadedTokens: DataRecord = {};
|
||||||
await db
|
await db
|
||||||
.table("maps")
|
.table("maps")
|
||||||
.filter((map) => filter("maps", map, map.id))
|
.filter((map: Map) => filter("maps", map, map.id))
|
||||||
.each((map) => {
|
.each((map: Map) => {
|
||||||
loadedMaps[map.id] = {
|
loadedMaps[map.id] = {
|
||||||
name: map.name,
|
name: map.name,
|
||||||
id: map.id,
|
id: map.id,
|
||||||
@ -67,19 +79,19 @@ function SelectDataModal({
|
|||||||
});
|
});
|
||||||
await db
|
await db
|
||||||
.table("states")
|
.table("states")
|
||||||
.filter((state) => filter("states", state, state.mapId))
|
.filter((state: MapState) => filter("states", state, state.mapId))
|
||||||
.each((state) => {
|
.each((state: MapState) => {
|
||||||
loadedTokensByMap[state.mapId] = new Set(
|
loadedTokensByMap[state.mapId] = new Set(
|
||||||
Object.values(state.tokens).map(
|
Object.values(state.tokens).map(
|
||||||
(tokenState: any) => tokenState.tokenId
|
(tokenState) => tokenState.tokenId
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.table("tokens")
|
.table("tokens")
|
||||||
.filter((token) => filter("tokens", token, token.id))
|
.filter((token: Token) => filter("tokens", token, token.id))
|
||||||
.each((token) => {
|
.each((token: Token) => {
|
||||||
loadedTokens[token.id] = {
|
loadedTokens[token.id] = {
|
||||||
name: token.name,
|
name: token.name,
|
||||||
id: token.id,
|
id: token.id,
|
||||||
@ -110,9 +122,11 @@ function SelectDataModal({
|
|||||||
}, [isOpen, databaseName, filter]);
|
}, [isOpen, databaseName, filter]);
|
||||||
|
|
||||||
// An object mapping a tokenId to how many checked maps it is currently used in
|
// 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(() => {
|
useEffect(() => {
|
||||||
let tokensUsed: any = {};
|
let tokensUsed: Record<string, number> = {};
|
||||||
for (let mapId in maps) {
|
for (let mapId in maps) {
|
||||||
if (maps[mapId].checked && mapId in tokensByMap) {
|
if (maps[mapId].checked && mapId in tokensByMap) {
|
||||||
for (let tokenId of tokensByMap[mapId]) {
|
for (let tokenId of tokensByMap[mapId]) {
|
||||||
@ -126,7 +140,7 @@ function SelectDataModal({
|
|||||||
}
|
}
|
||||||
setTokenUsedCount(tokensUsed);
|
setTokenUsedCount(tokensUsed);
|
||||||
// Update tokens to ensure used tokens are checked
|
// Update tokens to ensure used tokens are checked
|
||||||
setTokens((prevTokens: any) => {
|
setTokens((prevTokens) => {
|
||||||
let newTokens = { ...prevTokens };
|
let newTokens = { ...prevTokens };
|
||||||
for (let id in newTokens) {
|
for (let id in newTokens) {
|
||||||
if (id in tokensUsed && newTokens[id].type !== "default") {
|
if (id in tokensUsed && newTokens[id].type !== "default") {
|
||||||
@ -137,7 +151,7 @@ function SelectDataModal({
|
|||||||
});
|
});
|
||||||
}, [maps, tokensByMap]);
|
}, [maps, tokensByMap]);
|
||||||
|
|
||||||
function getCheckedGroups(groups: any[], data: Record<string, SelectData>) {
|
function getCheckedGroups(groups: Group[], data: DataRecord) {
|
||||||
let checkedGroups = [];
|
let checkedGroups = [];
|
||||||
for (let group of groups) {
|
for (let group of groups) {
|
||||||
if (group.type === "item") {
|
if (group.type === "item") {
|
||||||
@ -181,7 +195,7 @@ function SelectDataModal({
|
|||||||
});
|
});
|
||||||
// If all token select is unchecked then ensure all tokens are unchecked
|
// If all token select is unchecked then ensure all tokens are unchecked
|
||||||
if (!event.target.checked && !tokensSelectChecked) {
|
if (!event.target.checked && !tokensSelectChecked) {
|
||||||
setTokens((prevTokens: any) => {
|
setTokens((prevTokens) => {
|
||||||
let newTokens = { ...prevTokens };
|
let newTokens = { ...prevTokens };
|
||||||
let tempUsedCount = { ...tokenUsedCount };
|
let tempUsedCount = { ...tokenUsedCount };
|
||||||
for (let id in newTokens) {
|
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
|
// Some tokens are checked not by maps or all tokens are checked by maps
|
||||||
const tokensSelectChecked =
|
const tokensSelectChecked =
|
||||||
Object.values(tokens).some(
|
Object.values(tokens).some(
|
||||||
(token: any) => !(token.id in tokenUsedCount) && token.checked
|
(token) => !(token.id in tokenUsedCount) && token.checked
|
||||||
) ||
|
) || Object.values(tokens).every((token) => token.id in tokenUsedCount);
|
||||||
Object.values(tokens).every((token: any) => token.id in tokenUsedCount);
|
|
||||||
|
|
||||||
function renderGroupContainer(
|
function renderGroupContainer(
|
||||||
group: any,
|
group: GroupContainer,
|
||||||
checked: boolean,
|
checked: boolean,
|
||||||
renderItem: (group: any) => React.ReactNode,
|
renderItem: (group: Group) => React.ReactNode,
|
||||||
onGroupChange: (
|
onGroupChange: (
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
group: any
|
group: GroupContainer
|
||||||
) => void
|
) => void
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
@ -270,7 +283,7 @@ function SelectDataModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMapGroup(group: any) {
|
function renderMapGroup(group: Group) {
|
||||||
if (group.type === "item") {
|
if (group.type === "item") {
|
||||||
const map = maps[group.id];
|
const map = maps[group.id];
|
||||||
if (map) {
|
if (map) {
|
||||||
@ -287,24 +300,24 @@ function SelectDataModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (group.items.some((item: any) => item.id in maps)) {
|
if (group.items.some((item) => item.id in maps)) {
|
||||||
return renderGroupContainer(
|
return renderGroupContainer(
|
||||||
group,
|
group,
|
||||||
group.items.some((item: any) => maps[item.id]?.checked),
|
group.items.some((item) => maps[item.id]?.checked),
|
||||||
renderMapGroup,
|
renderMapGroup,
|
||||||
(e, group) =>
|
(e, group) =>
|
||||||
handleMapsChanged(
|
handleMapsChanged(
|
||||||
e,
|
e,
|
||||||
group.items
|
group.items
|
||||||
.filter((group: any) => group.id in maps)
|
.filter((group) => group.id in maps)
|
||||||
.map((group: any) => maps[group.id])
|
.map((group) => maps[group.id])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTokenGroup(group: any) {
|
function renderTokenGroup(group: Group) {
|
||||||
if (group.type === "item") {
|
if (group.type === "item") {
|
||||||
const token = tokens[group.id];
|
const token = tokens[group.id];
|
||||||
if (token) {
|
if (token) {
|
||||||
@ -332,12 +345,11 @@ function SelectDataModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (group.items.some((item: any) => item.id in tokens)) {
|
if (group.items.some((item) => item.id in tokens)) {
|
||||||
const checked =
|
const checked =
|
||||||
group.items.some(
|
group.items.some(
|
||||||
(item: any) =>
|
(item) => !(item.id in tokenUsedCount) && tokens[item.id]?.checked
|
||||||
!(item.id in tokenUsedCount) && tokens[item.id]?.checked
|
) || group.items.every((item) => item.id in tokenUsedCount);
|
||||||
) || group.items.every((item: any) => item.id in tokenUsedCount);
|
|
||||||
return renderGroupContainer(
|
return renderGroupContainer(
|
||||||
group,
|
group,
|
||||||
checked,
|
checked,
|
||||||
@ -346,8 +358,8 @@ function SelectDataModal({
|
|||||||
handleTokensChanged(
|
handleTokensChanged(
|
||||||
e,
|
e,
|
||||||
group.items
|
group.items
|
||||||
.filter((group: any) => group.id in tokens)
|
.filter((group) => group.id in tokens)
|
||||||
.map((group: any) => tokens[group.id])
|
.map((group) => tokens[group.id])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -423,8 +435,8 @@ function SelectDataModal({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
!Object.values(maps).some((map: any) => map.checked) &&
|
!Object.values(maps).some((map) => map.checked) &&
|
||||||
!Object.values(tokens).some((token: any) => token.checked)
|
!Object.values(tokens).some((token) => token.checked)
|
||||||
}
|
}
|
||||||
sx={{ flexGrow: 1 }}
|
sx={{ flexGrow: 1 }}
|
||||||
m={1}
|
m={1}
|
||||||
|
@ -7,16 +7,22 @@ import DiceTiles from "../components/dice/DiceTiles";
|
|||||||
import { dice } from "../dice";
|
import { dice } from "../dice";
|
||||||
|
|
||||||
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
||||||
import Dice from "../dice/Dice";
|
|
||||||
|
import { DefaultDice } from "../types/Dice";
|
||||||
|
|
||||||
type SelectDiceProps = {
|
type SelectDiceProps = {
|
||||||
isOpen: boolean,
|
isOpen: boolean;
|
||||||
onRequestClose: () => void,
|
onRequestClose: () => void;
|
||||||
onDone: any,
|
onDone: (dice: DefaultDice) => void;
|
||||||
defaultDice: Dice
|
defaultDice: DefaultDice;
|
||||||
}
|
};
|
||||||
|
|
||||||
function SelectDiceModal({ isOpen, onRequestClose, onDone, defaultDice }: SelectDiceProps) {
|
function SelectDiceModal({
|
||||||
|
isOpen,
|
||||||
|
onRequestClose,
|
||||||
|
onDone,
|
||||||
|
defaultDice,
|
||||||
|
}: SelectDiceProps) {
|
||||||
const [selectedDice, setSelectedDice] = useState(defaultDice);
|
const [selectedDice, setSelectedDice] = useState(defaultDice);
|
||||||
const layout = useResponsiveLayout();
|
const layout = useResponsiveLayout();
|
||||||
|
|
||||||
@ -24,7 +30,9 @@ function SelectDiceModal({ isOpen, onRequestClose, onDone, defaultDice }: Select
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
style={{ content: { maxWidth: layout.modalSize, width: "calc(100% - 16px)" } }}
|
style={{
|
||||||
|
content: { maxWidth: layout.modalSize, width: "calc(100% - 16px)" },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -140,11 +140,13 @@ function SelectMapModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleImageUpload(file: File) {
|
async function handleImageUpload(file: File) {
|
||||||
setIsLoading(true);
|
if (userId) {
|
||||||
const { map, assets } = await createMapFromFile(file, userId);
|
setIsLoading(true);
|
||||||
await addMap(map);
|
const { map, assets } = await createMapFromFile(file, userId);
|
||||||
await addAssets(assets);
|
await addMap(map);
|
||||||
setIsLoading(false);
|
await addAssets(assets);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,7 +185,7 @@ function SelectMapModal({
|
|||||||
if (groupIds.length === 1) {
|
if (groupIds.length === 1) {
|
||||||
// Only allow adding a map from dragging if there is a single group item selected
|
// Only allow adding a map from dragging if there is a single group item selected
|
||||||
const group = findGroup(mapGroups, groupIds[0]);
|
const group = findGroup(mapGroups, groupIds[0]);
|
||||||
setCanAddDraggedMap(group && group.type === "item");
|
setCanAddDraggedMap(group !== undefined && group.type === "item");
|
||||||
} else {
|
} else {
|
||||||
setCanAddDraggedMap(false);
|
setCanAddDraggedMap(false);
|
||||||
}
|
}
|
||||||
@ -198,8 +200,10 @@ function SelectMapModal({
|
|||||||
const layout = useResponsiveLayout();
|
const layout = useResponsiveLayout();
|
||||||
|
|
||||||
const [modalSize, setModalSize] = useState({ width: 0, height: 0 });
|
const [modalSize, setModalSize] = useState({ width: 0, height: 0 });
|
||||||
function handleModalResize(width: number, height: number) {
|
function handleModalResize(width?: number, height?: number) {
|
||||||
setModalSize({ width, height });
|
if (width && height) {
|
||||||
|
setModalSize({ width, height });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editingMap =
|
const editingMap =
|
||||||
@ -249,7 +253,7 @@ function SelectMapModal({
|
|||||||
<TileActionBar onAdd={openImageDialog} addTitle="Import Map(s)" />
|
<TileActionBar onAdd={openImageDialog} addTitle="Import Map(s)" />
|
||||||
<Box sx={{ position: "relative" }}>
|
<Box sx={{ position: "relative" }}>
|
||||||
<TileDragProvider
|
<TileDragProvider
|
||||||
onDragAdd={canAddDraggedMap && handleDragAdd}
|
onDragAdd={(canAddDraggedMap && handleDragAdd) || undefined}
|
||||||
onDragStart={() => setIsDraggingMap(true)}
|
onDragStart={() => setIsDraggingMap(true)}
|
||||||
onDragEnd={() => setIsDraggingMap(false)}
|
onDragEnd={() => setIsDraggingMap(false)}
|
||||||
onDragCancel={() => setIsDraggingMap(false)}
|
onDragCancel={() => setIsDraggingMap(false)}
|
||||||
@ -263,7 +267,7 @@ function SelectMapModal({
|
|||||||
</TilesContainer>
|
</TilesContainer>
|
||||||
</TileDragProvider>
|
</TileDragProvider>
|
||||||
<TileDragProvider
|
<TileDragProvider
|
||||||
onDragAdd={canAddDraggedMap && handleDragAdd}
|
onDragAdd={(canAddDraggedMap && handleDragAdd) || undefined}
|
||||||
onDragStart={() => setIsDraggingMap(true)}
|
onDragStart={() => setIsDraggingMap(true)}
|
||||||
onDragEnd={() => setIsDraggingMap(false)}
|
onDragEnd={() => setIsDraggingMap(false)}
|
||||||
onDragCancel={() => setIsDraggingMap(false)}
|
onDragCancel={() => setIsDraggingMap(false)}
|
||||||
|
@ -35,7 +35,7 @@ import { GroupProvider } from "../contexts/GroupContext";
|
|||||||
import { TileDragProvider } from "../contexts/TileDragContext";
|
import { TileDragProvider } from "../contexts/TileDragContext";
|
||||||
import { useMapStage } from "../contexts/MapStageContext";
|
import { useMapStage } from "../contexts/MapStageContext";
|
||||||
|
|
||||||
import { TokenState } from "../components/map/Map";
|
import { TokenState } from "../types/TokenState";
|
||||||
|
|
||||||
type SelectTokensModalProps = {
|
type SelectTokensModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -146,6 +146,9 @@ function SelectTokensModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleImageUpload(file: File) {
|
async function handleImageUpload(file: File) {
|
||||||
|
if (!userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const { token, assets } = await createTokenFromFile(file, userId);
|
const { token, assets } = await createTokenFromFile(file, userId);
|
||||||
await addToken(token);
|
await addToken(token);
|
||||||
@ -161,7 +164,7 @@ function SelectTokensModal({
|
|||||||
const [isDraggingToken, setIsDraggingToken] = useState(false);
|
const [isDraggingToken, setIsDraggingToken] = useState(false);
|
||||||
|
|
||||||
const mapStageRef = useMapStage();
|
const mapStageRef = useMapStage();
|
||||||
function handleTokensAddToMap(groupIds: string[], rect: any) {
|
function handleTokensAddToMap(groupIds: string[], rect: DOMRect) {
|
||||||
let clientPosition = new Vector2(
|
let clientPosition = new Vector2(
|
||||||
rect.width / 2 + rect.left,
|
rect.width / 2 + rect.left,
|
||||||
rect.height / 2 + rect.top
|
rect.height / 2 + rect.top
|
||||||
@ -172,11 +175,11 @@ function SelectTokensModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let position = clientPositionToMapPosition(mapStage, clientPosition, false);
|
let position = clientPositionToMapPosition(mapStage, clientPosition, false);
|
||||||
if (!position) {
|
if (!position || !userId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newTokenStates = [];
|
let newTokenStates: TokenState[] = [];
|
||||||
|
|
||||||
for (let id of groupIds) {
|
for (let id of groupIds) {
|
||||||
if (id in tokensById) {
|
if (id in tokensById) {
|
||||||
@ -210,8 +213,10 @@ function SelectTokensModal({
|
|||||||
const layout = useResponsiveLayout();
|
const layout = useResponsiveLayout();
|
||||||
|
|
||||||
const [modalSize, setModalSize] = useState({ width: 0, height: 0 });
|
const [modalSize, setModalSize] = useState({ width: 0, height: 0 });
|
||||||
function handleModalResize(width: number, height: number) {
|
function handleModalResize(width?: number, height?: number) {
|
||||||
setModalSize({ width, height });
|
if (width && height) {
|
||||||
|
setModalSize({ width, height });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const editingToken =
|
const editingToken =
|
||||||
|
@ -32,7 +32,6 @@ import undead from "./Undead.png";
|
|||||||
import warlock from "./Warlock.png";
|
import warlock from "./Warlock.png";
|
||||||
import wizard from "./Wizard.png";
|
import wizard from "./Wizard.png";
|
||||||
import unknown from "./Unknown.png";
|
import unknown from "./Unknown.png";
|
||||||
import { ImageFile } from "../helpers/image";
|
|
||||||
|
|
||||||
export const tokenSources = {
|
export const tokenSources = {
|
||||||
barbarian,
|
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) {
|
export function getDefaultTokens(userId: string) {
|
||||||
const tokenKeys = Object.keys(tokenSources);
|
const tokenKeys = Object.keys(tokenSources);
|
||||||
let tokens = [];
|
let tokens = [];
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import { InstancedMesh } from "@babylonjs/core";
|
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 = {
|
export type DiceRoll = {
|
||||||
type: DiceType;
|
type: DiceType;
|
||||||
roll: number | "unknown";
|
roll: number | "unknown";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Dice = {
|
export type DiceMesh = {
|
||||||
type: DiceType;
|
type: DiceType;
|
||||||
instance: InstancedMesh;
|
instance: InstancedMesh;
|
||||||
asleep: boolean;
|
asleep: boolean;
|
||||||
|
sleepTimeout?: NodeJS.Timeout;
|
||||||
d10Instance?: InstancedMesh;
|
d10Instance?: InstancedMesh;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,3 +20,10 @@ export type DiceState = {
|
|||||||
share: boolean;
|
share: boolean;
|
||||||
rolls: DiceRoll[];
|
rolls: DiceRoll[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DefaultDice = {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
class: typeof Dice;
|
||||||
|
preview: string;
|
||||||
|
};
|
||||||
|
@ -32,6 +32,8 @@ export type CircleData = {
|
|||||||
radius: number;
|
radius: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShapeData = PointsData | RectData | CircleData;
|
||||||
|
|
||||||
export type BaseDrawing = {
|
export type BaseDrawing = {
|
||||||
blend: boolean;
|
blend: boolean;
|
||||||
color: string;
|
color: string;
|
||||||
@ -43,6 +45,8 @@ export type BaseShape = BaseDrawing & {
|
|||||||
type: "shape";
|
type: "shape";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShapeType = "line" | "rectangle" | "circle" | "triangle";
|
||||||
|
|
||||||
export type Line = BaseShape & {
|
export type Line = BaseShape & {
|
||||||
shapeType: "line";
|
shapeType: "line";
|
||||||
data: PointsData;
|
data: PointsData;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import Vector2 from "../helpers/Vector2";
|
import Vector2 from "../helpers/Vector2";
|
||||||
|
|
||||||
export type GridInset = {
|
export type GridInset = {
|
||||||
|
/** Top left position of the inset */
|
||||||
topLeft: Vector2;
|
topLeft: Vector2;
|
||||||
|
/** Bottom right position of the inset */
|
||||||
bottomRight: Vector2;
|
bottomRight: Vector2;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -19,8 +21,19 @@ export type GridMeasurement = {
|
|||||||
export type GridType = "square" | "hexVertical" | "hexHorizontal";
|
export type GridType = "square" | "hexVertical" | "hexHorizontal";
|
||||||
|
|
||||||
export type Grid = {
|
export type Grid = {
|
||||||
|
/** The inset of the grid from the map */
|
||||||
inset: GridInset;
|
inset: GridInset;
|
||||||
|
/** The number of columns and rows of the grid as `x` and `y` */
|
||||||
size: Vector2;
|
size: Vector2;
|
||||||
type: GridType;
|
type: GridType;
|
||||||
measurement: GridMeasurement;
|
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;
|
file: string;
|
||||||
resolutions: FileMapResolutions;
|
resolutions: FileMapResolutions;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
|
quality: "low" | "medium" | "high" | "ultra" | "original";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Map = DefaultMap | FileMap;
|
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,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"typeRoots": [
|
||||||
|
"src/types/external",
|
||||||
|
"node_modules/@types"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*"
|
"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"
|
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
|
||||||
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
|
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":
|
"@dnd-kit/accessibility@^3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz#b56e3750414fd907b7d6972b3116aa8f96d07fde"
|
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz#b56e3750414fd907b7d6972b3116aa8f96d07fde"
|
||||||
@ -1852,6 +1834,24 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.0.0"
|
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":
|
"@emotion/cache@^10.0.27":
|
||||||
version "10.0.29"
|
version "10.0.29"
|
||||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
|
||||||
@ -2748,76 +2748,76 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
|
|
||||||
"@theme-ui/color-modes@0.8.4":
|
"@theme-ui/color-modes@0.10.0":
|
||||||
version "0.8.4"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@theme-ui/color-modes/-/color-modes-0.8.4.tgz#d09bff439e990bc8751677b1d06adf73534a5571"
|
resolved "https://registry.yarnpkg.com/@theme-ui/color-modes/-/color-modes-0.10.0.tgz#85071f16d7d4458f3dab5a7af8b9ea459da4dcd0"
|
||||||
integrity sha512-3Ueb6yKBFkyHsLEFlLH3Igl6ZHbVamJHn6YoAwIut0QQrAOAfTSG1vZr/4LJUCILc/U0y9kPvTw7MBpUKi1hWg==
|
integrity sha512-6sZaagCFK48p2YjecLljFwPkiB3/R9dMNKUQC3+fnaH3N9FcsflNWpjKAYhtQ5QLKvYacFdqczT4YaMtGwKb/Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/react" "^11.1.1"
|
"@emotion/react" "^11.1.1"
|
||||||
"@theme-ui/core" "0.8.4"
|
"@theme-ui/core" "0.10.0"
|
||||||
"@theme-ui/css" "0.8.4"
|
"@theme-ui/css" "0.10.0"
|
||||||
deepmerge "^4.2.2"
|
deepmerge "^4.2.2"
|
||||||
|
|
||||||
"@theme-ui/components@0.8.4":
|
"@theme-ui/components@0.10.0":
|
||||||
version "0.8.4"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@theme-ui/components/-/components-0.8.4.tgz#a362d0625f6a6efacc35a2db29fb6c2368257fd2"
|
resolved "https://registry.yarnpkg.com/@theme-ui/components/-/components-0.10.0.tgz#c1aa9cade71e5a6cf7c19f9e0ade900122ef23f9"
|
||||||
integrity sha512-JOP/rABNS2Bu/hWA68Tdt6pUyCtCU+nMMWZAyvj2qDIn2mBrLwBKvvxyrwaGT5tHniIX4oVG57GH1Sb94Rw+mg==
|
integrity sha512-zPA+16fP+R140kns+3FBhybsPzNjcCWHgXcwIPjww1dfDnlXRa7al9Nz4Y8zyWvk1wNiGqUa09Y1sabK6EYspQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/react" "^11.1.1"
|
"@emotion/react" "^11.1.1"
|
||||||
"@emotion/styled" "^11.0.0"
|
"@emotion/styled" "^11.0.0"
|
||||||
"@styled-system/color" "^5.1.2"
|
"@styled-system/color" "^5.1.2"
|
||||||
"@styled-system/should-forward-prop" "^5.1.2"
|
"@styled-system/should-forward-prop" "^5.1.2"
|
||||||
"@styled-system/space" "^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"
|
"@types/styled-system" "^5.1.10"
|
||||||
|
|
||||||
"@theme-ui/core@0.8.4":
|
"@theme-ui/core@0.10.0":
|
||||||
version "0.8.4"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@theme-ui/core/-/core-0.8.4.tgz#64ce2db0b2d50768cb8726e61f9d391cabae0448"
|
resolved "https://registry.yarnpkg.com/@theme-ui/core/-/core-0.10.0.tgz#2373d53368da5fa561915414ade070a9de0e9678"
|
||||||
integrity sha512-zHOLJ/Zw024SlQEl7+mpIk2wuNaWRFCr9brYtV+2kyLwQeETIIBXEdmQ6yJ6wc1nSJNs0VOHk/sLRPvreb+5uQ==
|
integrity sha512-3DeTHGqyqIi05JCsJ+G+fqW6YsX/oGJiaAvMgLfd86tGdJOnDseEBQG41oDFHSWtQSJDpBcoFgAWMGITmYdH+g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/react" "^11.1.1"
|
"@emotion/react" "^11.1.1"
|
||||||
"@theme-ui/css" "0.8.4"
|
"@theme-ui/css" "0.10.0"
|
||||||
"@theme-ui/parse-props" "0.8.4"
|
"@theme-ui/parse-props" "0.10.0"
|
||||||
deepmerge "^4.2.2"
|
deepmerge "^4.2.2"
|
||||||
|
|
||||||
"@theme-ui/css@0.8.4":
|
"@theme-ui/css@0.10.0":
|
||||||
version "0.8.4"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@theme-ui/css/-/css-0.8.4.tgz#2539c8ccb52054d54593786e5f1e89f118e908c0"
|
resolved "https://registry.yarnpkg.com/@theme-ui/css/-/css-0.10.0.tgz#871f63334fb138491406c32f8347d53b19846a9b"
|
||||||
integrity sha512-ZubYp4glaDpsJSd2z38FlwkItvXk8+t0i0323ZWNO7Liqg4t/hJT+7RmtWj1NQ2IPVqTUEmsH/hVdz5SIPu2LA==
|
integrity sha512-Up3HqXoy2ERn/9gVxApCSl2n9vwtHBwPrYlMyEjX0YPs/rxmo+Aqe3kAxO+SG9idMw08mtdaDfMIFaPsBE5ovA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/react" "^11.1.1"
|
"@emotion/react" "^11.1.1"
|
||||||
csstype "^3.0.5"
|
csstype "^3.0.5"
|
||||||
|
|
||||||
"@theme-ui/mdx@0.8.4":
|
"@theme-ui/mdx@0.10.0":
|
||||||
version "0.8.4"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@theme-ui/mdx/-/mdx-0.8.4.tgz#86bee495402216f65bd3244303f2248d91ca90d0"
|
resolved "https://registry.yarnpkg.com/@theme-ui/mdx/-/mdx-0.10.0.tgz#14124cb194df8023f7253391900d8d17a6011a92"
|
||||||
integrity sha512-pI2XalkIV+Ky2q/y+RBuHi9fBxzCECXZDSMYH98FExO3C6X5sdPDJ1u9kDKgbqJxUxTBQlZKSvU+fG9hjN3oQQ==
|
integrity sha512-IcDrQONVrOFQFCFdyrlNoTTKmhw7ELtrLktRYmmWtCEz+KHpBiEVdxNo2yvz/05zF2BPGKOqu4wkMpUR13wNSQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/react" "^11.1.1"
|
"@emotion/react" "^11.1.1"
|
||||||
"@emotion/styled" "^11.0.0"
|
"@emotion/styled" "^11.0.0"
|
||||||
"@mdx-js/react" "^1.6.22"
|
"@mdx-js/react" "^1.6.22"
|
||||||
"@theme-ui/core" "0.8.4"
|
"@theme-ui/core" "0.10.0"
|
||||||
"@theme-ui/css" "0.8.4"
|
"@theme-ui/css" "0.10.0"
|
||||||
|
|
||||||
"@theme-ui/parse-props@0.8.4":
|
"@theme-ui/parse-props@0.10.0":
|
||||||
version "0.8.4"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@theme-ui/parse-props/-/parse-props-0.8.4.tgz#398e54e11768248938c2c505c04bc44042b66409"
|
resolved "https://registry.yarnpkg.com/@theme-ui/parse-props/-/parse-props-0.10.0.tgz#8d2f4f3b3edafd925c3872ddd559e2b62006f43f"
|
||||||
integrity sha512-2CgPKsApLVRu9wYzaaC/+ZhmKwohQ5uZylbf0HzVA3X4rGIfs7aktsM16FvCeiTyAn+7Z8MTShgSOSsD0S8l3Q==
|
integrity sha512-UfcLyThXYsB9azc8qbsZVgbF7xf+GLF2Hhy+suyjwQ3XSVOx97B5ZsuzCNUGbggtBw4dXayJgRmMz0FHyp0L8Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/react" "^11.1.1"
|
"@emotion/react" "^11.1.1"
|
||||||
"@theme-ui/css" "0.8.4"
|
"@theme-ui/css" "0.10.0"
|
||||||
|
|
||||||
"@theme-ui/theme-provider@0.8.4":
|
"@theme-ui/theme-provider@0.10.0":
|
||||||
version "0.8.4"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/@theme-ui/theme-provider/-/theme-provider-0.8.4.tgz#87ba378f2bd4d07af5c04a215db193eb78843e89"
|
resolved "https://registry.yarnpkg.com/@theme-ui/theme-provider/-/theme-provider-0.10.0.tgz#16586af579bef804f1e4997a131134d2465bfd5d"
|
||||||
integrity sha512-xYXqs2y9hkZmqSTA76X2dfrAfUw8LDHnBa1Xp6J41Zb4iXcxbLp3Aq1yRT1xGBAAVRGyhfMkYqxSXfNxN8R1ig==
|
integrity sha512-1AVsegjEAw7uidr0/qJMoKktKbdXuXRjfukI9712GZleft3dzoHhkQUO7IefXjbafyu/plzo/WTXkbz0A4uhmA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/react" "^11.1.1"
|
"@emotion/react" "^11.1.1"
|
||||||
"@theme-ui/color-modes" "0.8.4"
|
"@theme-ui/color-modes" "0.10.0"
|
||||||
"@theme-ui/core" "0.8.4"
|
"@theme-ui/core" "0.10.0"
|
||||||
"@theme-ui/css" "0.8.4"
|
"@theme-ui/css" "0.10.0"
|
||||||
"@theme-ui/mdx" "0.8.4"
|
"@theme-ui/mdx" "0.10.0"
|
||||||
|
|
||||||
"@types/anymatch@*":
|
"@types/anymatch@*":
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
@ -3096,6 +3096,11 @@
|
|||||||
"@types/scheduler" "*"
|
"@types/scheduler" "*"
|
||||||
csstype "^3.0.2"
|
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":
|
"@types/resolve@0.0.8":
|
||||||
version "0.0.8"
|
version "0.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
|
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
|
||||||
@ -3161,6 +3166,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
source-map "^0.6.1"
|
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":
|
"@types/webgl-ext@0.0.30":
|
||||||
version "0.0.30"
|
version "0.0.30"
|
||||||
resolved "https://registry.yarnpkg.com/@types/webgl-ext/-/webgl-ext-0.0.30.tgz#0ce498c16a41a23d15289e0b844d945b25f0fb9d"
|
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:
|
dependencies:
|
||||||
p-locate "^4.1.0"
|
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:
|
lodash._reinterpolate@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
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"
|
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3"
|
||||||
integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==
|
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:
|
raf@^3.4.1:
|
||||||
version "3.4.1"
|
version "3.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
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"
|
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||||
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
|
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
|
||||||
|
|
||||||
react-resize-detector@4.2.3:
|
react-resize-detector@^6.7.4:
|
||||||
version "4.2.3"
|
version "6.7.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-4.2.3.tgz#7df258668a30bdfd88e655bbdb27db7fd7b23127"
|
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-6.7.4.tgz#594cc026115af05484e8011157b5dc2137492680"
|
||||||
integrity sha512-4AeS6lxdz2KOgDZaOVt1duoDHrbYwSrUX32KeM9j6t9ISyRphoJbTRCMS1aPFxZHFqcCGLT1gMl3lEcSWZNW0A==
|
integrity sha512-wzvGmUdEDMhiUHVZGnl4kuyj/TEQhvbB5LyAGkbYXetwJ2O+u/zftmPvU+kxiO1h+d9aUqQBKcNLS7TvB3ytqA==
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.17.15"
|
"@types/resize-observer-browser" "^0.1.5"
|
||||||
lodash-es "^4.17.15"
|
lodash.debounce "^4.0.8"
|
||||||
prop-types "^15.7.2"
|
lodash.throttle "^4.1.1"
|
||||||
raf-schd "^4.0.2"
|
|
||||||
resize-observer-polyfill "^1.5.1"
|
resize-observer-polyfill "^1.5.1"
|
||||||
|
|
||||||
react-router-dom@^5.1.2:
|
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"
|
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||||
|
|
||||||
theme-ui@^0.8.4:
|
theme-ui@^0.10.0:
|
||||||
version "0.8.4"
|
version "0.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/theme-ui/-/theme-ui-0.8.4.tgz#c2e2aebd6266d71a9b6be70cfff018fa8337866e"
|
resolved "https://registry.yarnpkg.com/theme-ui/-/theme-ui-0.10.0.tgz#4fce8fbe7ad008ec07b383eaf5f468b0317fcfa1"
|
||||||
integrity sha512-vBIixheMRmFf3YuqqonYeWg0dhbshWqUzTIx4ROYLSlkbsGwcCAxSwZBtwxwnTvieaInwNUBvQ+KOZ3HpWbT6A==
|
integrity sha512-6uj9/4n6gZrlhrfQKt+QoLdtVc46ETJZv42iqedCatXaaTA5tHN1j7TJDmvYD9ooD/CT0+hsvOrx2d2etb/kYg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@theme-ui/color-modes" "0.8.4"
|
"@theme-ui/color-modes" "0.10.0"
|
||||||
"@theme-ui/components" "0.8.4"
|
"@theme-ui/components" "0.10.0"
|
||||||
"@theme-ui/core" "0.8.4"
|
"@theme-ui/core" "0.10.0"
|
||||||
"@theme-ui/css" "0.8.4"
|
"@theme-ui/css" "0.10.0"
|
||||||
"@theme-ui/mdx" "0.8.4"
|
"@theme-ui/mdx" "0.10.0"
|
||||||
"@theme-ui/theme-provider" "0.8.4"
|
"@theme-ui/theme-provider" "0.10.0"
|
||||||
|
|
||||||
throat@^5.0.0:
|
throat@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user