Added types to helpers
This commit is contained in:
parent
5212c94a3d
commit
86f15e9274
@ -1,8 +1,7 @@
|
||||
import React from "react";
|
||||
import { ThemeProvider } from "theme-ui";
|
||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||
|
||||
import theme from "./theme.js";
|
||||
import theme from "./theme";
|
||||
import Home from "./routes/Home";
|
||||
import Game from "./routes/Game";
|
||||
import About from "./routes/About";
|
@ -1,16 +1,16 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import Dexie, { Version, DexieOptions } from "dexie";
|
||||
import Dexie, { Version, DexieOptions, Transaction } from "dexie";
|
||||
import "dexie-observable";
|
||||
import shortid from "shortid";
|
||||
|
||||
import blobToBuffer from "./helpers/blobToBuffer";
|
||||
import { getGridDefaultInset } from "./helpers/grid";
|
||||
import { getGridDefaultInset, Grid } from "./helpers/grid";
|
||||
import { convertOldActionsToShapes } from "./actions";
|
||||
import { createThumbnail } from "./helpers/image";
|
||||
|
||||
// Helper to create a thumbnail for a file in a db
|
||||
async function createDataThumbnail(data) {
|
||||
let url;
|
||||
async function createDataThumbnail(data: any) {
|
||||
let url: string;
|
||||
if (data?.resolutions?.low?.file) {
|
||||
url = URL.createObjectURL(new Blob([data.resolutions.low.file]));
|
||||
} else {
|
||||
@ -20,7 +20,8 @@ async function createDataThumbnail(data) {
|
||||
new Promise((resolve) => {
|
||||
let image = new Image();
|
||||
image.onload = async () => {
|
||||
const thumbnail = await createThumbnail(image);
|
||||
// TODO: confirm parameter for type here
|
||||
const thumbnail = await createThumbnail(image, "file");
|
||||
resolve(thumbnail);
|
||||
};
|
||||
image.src = url;
|
||||
@ -34,13 +35,16 @@ async function createDataThumbnail(data) {
|
||||
* @param {Version} version
|
||||
*/
|
||||
|
||||
type VersionCallback = (version: Version) => void
|
||||
|
||||
/**
|
||||
* Mapping of version number to their upgrade function
|
||||
* @type {Object.<number, VersionCallback>}
|
||||
*/
|
||||
const versions = {
|
||||
|
||||
const versions: Record<number, VersionCallback> = {
|
||||
// v1.2.0
|
||||
1(v) {
|
||||
1(v: Version) {
|
||||
v.stores({
|
||||
maps: "id, owner",
|
||||
states: "mapId",
|
||||
@ -49,29 +53,29 @@ const versions = {
|
||||
});
|
||||
},
|
||||
// v1.2.1 - Move from blob files to array buffers
|
||||
2(v) {
|
||||
v.stores({}).upgrade(async (tx) => {
|
||||
2(v: Version) {
|
||||
v.stores({}).upgrade(async (tx: Transaction) => {
|
||||
const maps = await Dexie.waitFor(tx.table("maps").toArray());
|
||||
let mapBuffers = {};
|
||||
let mapBuffers: any = {};
|
||||
for (let map of maps) {
|
||||
mapBuffers[map.id] = await Dexie.waitFor(blobToBuffer(map.file));
|
||||
}
|
||||
return tx
|
||||
.table("maps")
|
||||
.toCollection()
|
||||
.modify((map) => {
|
||||
.modify((map: any) => {
|
||||
map.file = mapBuffers[map.id];
|
||||
});
|
||||
});
|
||||
},
|
||||
// v1.3.0 - Added new default tokens
|
||||
3(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
3(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
function mapTokenId(id) {
|
||||
.modify((state: any) => {
|
||||
function mapTokenId(id: any) {
|
||||
switch (id) {
|
||||
case "__default-Axes":
|
||||
return "__default-Barbarian";
|
||||
@ -128,23 +132,23 @@ const versions = {
|
||||
});
|
||||
},
|
||||
// v1.3.1 - Added show grid option
|
||||
4(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
4(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("maps")
|
||||
.toCollection()
|
||||
.modify((map) => {
|
||||
.modify((map: any) => {
|
||||
map.showGrid = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
// v1.4.0 - Added fog subtraction
|
||||
5(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
5(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
.modify((state: any) => {
|
||||
for (let fogAction of state.fogDrawActions) {
|
||||
if (fogAction.type === "add" || fogAction.type === "edit") {
|
||||
for (let shape of fogAction.shapes) {
|
||||
@ -156,24 +160,24 @@ const versions = {
|
||||
});
|
||||
},
|
||||
// v1.4.2 - Added map resolutions
|
||||
6(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
6(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("maps")
|
||||
.toCollection()
|
||||
.modify((map) => {
|
||||
.modify((map: any) => {
|
||||
map.resolutions = {};
|
||||
map.quality = "original";
|
||||
});
|
||||
});
|
||||
},
|
||||
// v1.5.0 - Fixed default token rogue spelling
|
||||
7(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
7(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
.modify((state: any) => {
|
||||
for (let id in state.tokens) {
|
||||
if (state.tokens[id].tokenId === "__default-Rouge") {
|
||||
state.tokens[id].tokenId = "__default-Rogue";
|
||||
@ -183,23 +187,23 @@ const versions = {
|
||||
});
|
||||
},
|
||||
// v1.5.0 - Added map snap to grid option
|
||||
8(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
8(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("maps")
|
||||
.toCollection()
|
||||
.modify((map) => {
|
||||
.modify((map: any) => {
|
||||
map.snapToGrid = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
// v1.5.1 - Added lock, visibility and modified to tokens
|
||||
9(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
9(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
.modify((state: any) => {
|
||||
for (let id in state.tokens) {
|
||||
state.tokens[id].lastModifiedBy = state.tokens[id].lastEditedBy;
|
||||
delete state.tokens[id].lastEditedBy;
|
||||
@ -211,51 +215,51 @@ const versions = {
|
||||
});
|
||||
},
|
||||
// v1.5.1 - Added token prop category and remove isVehicle bool
|
||||
10(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
10(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("tokens")
|
||||
.toCollection()
|
||||
.modify((token) => {
|
||||
.modify((token: any) => {
|
||||
token.category = token.isVehicle ? "vehicle" : "character";
|
||||
delete token.isVehicle;
|
||||
});
|
||||
});
|
||||
},
|
||||
// v1.5.2 - Added automatic cache invalidation to maps
|
||||
11(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
11(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("maps")
|
||||
.toCollection()
|
||||
.modify((map) => {
|
||||
.modify((map: any) => {
|
||||
map.lastUsed = map.lastModified;
|
||||
});
|
||||
});
|
||||
},
|
||||
// v1.5.2 - Added automatic cache invalidation to tokens
|
||||
12(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
12(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("tokens")
|
||||
.toCollection()
|
||||
.modify((token) => {
|
||||
.modify((token: any) => {
|
||||
token.lastUsed = token.lastModified;
|
||||
});
|
||||
});
|
||||
},
|
||||
// v1.6.0 - Added map grouping and grid scale and offset
|
||||
13(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
13(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("maps")
|
||||
.toCollection()
|
||||
.modify((map) => {
|
||||
.modify((map: any) => {
|
||||
map.group = "";
|
||||
map.grid = {
|
||||
size: { x: map.gridX, y: map.gridY },
|
||||
inset: getGridDefaultInset(
|
||||
{ size: { x: map.gridX, y: map.gridY }, type: "square" },
|
||||
{ size: { x: map.gridX, y: map.gridY }, type: "square" } as Grid,
|
||||
map.width,
|
||||
map.height
|
||||
),
|
||||
@ -268,21 +272,21 @@ const versions = {
|
||||
});
|
||||
},
|
||||
// v1.6.0 - Added token grouping
|
||||
14(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
14(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("tokens")
|
||||
.toCollection()
|
||||
.modify((token) => {
|
||||
.modify((token: any) => {
|
||||
token.group = "";
|
||||
});
|
||||
});
|
||||
},
|
||||
// v1.6.1 - Added width and height to tokens
|
||||
15(v) {
|
||||
v.stores({}).upgrade(async (tx) => {
|
||||
15(v: Version) {
|
||||
v.stores({}).upgrade(async (tx: Transaction) => {
|
||||
const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
|
||||
let tokenSizes = {};
|
||||
let tokenSizes: any = {};
|
||||
for (let token of tokens) {
|
||||
const url = URL.createObjectURL(new Blob([token.file]));
|
||||
let image = new Image();
|
||||
@ -298,31 +302,31 @@ const versions = {
|
||||
return tx
|
||||
.table("tokens")
|
||||
.toCollection()
|
||||
.modify((token) => {
|
||||
.modify((token: any) => {
|
||||
token.width = tokenSizes[token.id].width;
|
||||
token.height = tokenSizes[token.id].height;
|
||||
});
|
||||
});
|
||||
},
|
||||
// v1.7.0 - Added note tool
|
||||
16(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
16(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
.modify((state: any) => {
|
||||
state.notes = {};
|
||||
state.editFlags = [...state.editFlags, "notes"];
|
||||
});
|
||||
});
|
||||
},
|
||||
// 1.7.0 (hotfix) - Optimized fog shape edits to only include needed data
|
||||
17(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
17(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
.modify((state: any) => {
|
||||
for (let i = 0; i < state.fogDrawActions.length; i++) {
|
||||
const action = state.fogDrawActions[i];
|
||||
if (action && action.type === "edit") {
|
||||
@ -340,12 +344,12 @@ const versions = {
|
||||
});
|
||||
},
|
||||
// 1.8.0 - Added note text only mode, converted draw and fog representations
|
||||
18(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
18(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
.modify((state: any) => {
|
||||
for (let id in state.notes) {
|
||||
state.notes[id].textOnly = false;
|
||||
}
|
||||
@ -367,12 +371,12 @@ const versions = {
|
||||
});
|
||||
},
|
||||
// 1.8.0 - Add thumbnail to maps and add measurement to grid
|
||||
19(v) {
|
||||
v.stores({}).upgrade(async (tx) => {
|
||||
19(v: Version) {
|
||||
v.stores({}).upgrade(async (tx: Transaction) => {
|
||||
const userId = (await Dexie.waitFor(tx.table("user").get("userId")))
|
||||
.value;
|
||||
const maps = await Dexie.waitFor(tx.table("maps").toArray());
|
||||
const thumbnails = {};
|
||||
const thumbnails: any = {};
|
||||
for (let map of maps) {
|
||||
try {
|
||||
if (map.owner === userId) {
|
||||
@ -383,19 +387,19 @@ const versions = {
|
||||
return tx
|
||||
.table("maps")
|
||||
.toCollection()
|
||||
.modify((map) => {
|
||||
.modify((map: any) => {
|
||||
map.thumbnail = thumbnails[map.id];
|
||||
map.grid.measurement = { type: "chebyshev", scale: "5ft" };
|
||||
});
|
||||
});
|
||||
},
|
||||
// 1.8.0 - Add thumbnail to tokens
|
||||
20(v) {
|
||||
v.stores({}).upgrade(async (tx) => {
|
||||
20(v: Version) {
|
||||
v.stores({}).upgrade(async (tx: Transaction) => {
|
||||
const userId = (await Dexie.waitFor(tx.table("user").get("userId")))
|
||||
.value;
|
||||
const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
|
||||
const thumbnails = {};
|
||||
const thumbnails: any = {};
|
||||
for (let token of tokens) {
|
||||
try {
|
||||
if (token.owner === userId) {
|
||||
@ -406,22 +410,22 @@ const versions = {
|
||||
return tx
|
||||
.table("tokens")
|
||||
.toCollection()
|
||||
.modify((token) => {
|
||||
.modify((token: any) => {
|
||||
token.thumbnail = thumbnails[token.id];
|
||||
});
|
||||
});
|
||||
},
|
||||
// 1.8.0 - Upgrade for Dexie.Observable
|
||||
21(v) {
|
||||
21(v: Version) {
|
||||
v.stores({});
|
||||
},
|
||||
// v1.8.1 - Shorten fog shape ids
|
||||
22(v) {
|
||||
v.stores({}).upgrade((tx) => {
|
||||
22(v: Version) {
|
||||
v.stores({}).upgrade((tx: Transaction) => {
|
||||
return tx
|
||||
.table("states")
|
||||
.toCollection()
|
||||
.modify((state) => {
|
||||
.modify((state: any) => {
|
||||
for (let id of Object.keys(state.fogShapes)) {
|
||||
const newId = shortid.generate();
|
||||
state.fogShapes[newId] = state.fogShapes[id];
|
||||
@ -440,7 +444,7 @@ const latestVersion = 22;
|
||||
* @param {Dexie} db
|
||||
* @param {number=} upTo version number to load up to, latest version if undefined
|
||||
*/
|
||||
export function loadVersions(db, upTo = latestVersion) {
|
||||
export function loadVersions(db: Dexie, upTo = latestVersion) {
|
||||
for (let versionNumber = 1; versionNumber <= upTo; versionNumber++) {
|
||||
versions[versionNumber](db.version(versionNumber));
|
||||
}
|
||||
@ -454,7 +458,7 @@ export function loadVersions(db, upTo = latestVersion) {
|
||||
* @returns {Dexie}
|
||||
*/
|
||||
export function getDatabase(
|
||||
options,
|
||||
options: DexieOptions,
|
||||
name = "OwlbearRodeoDB",
|
||||
versionNumber = latestVersion
|
||||
) {
|
2
src/global.d.ts
vendored
Normal file
2
src/global.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
declare module 'pepjs';
|
||||
declare module 'socket.io-msgpack-parser';
|
@ -2,17 +2,17 @@
|
||||
* A faked local or session storage used when the user has disabled storage
|
||||
*/
|
||||
class FakeStorage {
|
||||
data = {};
|
||||
key(index) {
|
||||
data: { [keyName: string ]: any} = {};
|
||||
key(index: number) {
|
||||
return Object.keys(this.data)[index] || null;
|
||||
}
|
||||
getItem(keyName) {
|
||||
getItem(keyName: string ) {
|
||||
return this.data[keyName] || null;
|
||||
}
|
||||
setItem(keyName, keyValue) {
|
||||
setItem(keyName: string, keyValue: any) {
|
||||
this.data[keyName] = keyValue;
|
||||
}
|
||||
removeItem(keyName) {
|
||||
removeItem(keyName: string) {
|
||||
delete this.data[keyName];
|
||||
}
|
||||
clear() {
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext } from "react";
|
||||
import { useContext } from "react";
|
||||
|
||||
import {
|
||||
InteractionEmitterContext,
|
||||
@ -47,7 +47,7 @@ import {
|
||||
/**
|
||||
* Provide a bridge for konva that forwards our contexts
|
||||
*/
|
||||
function KonvaBridge({ stageRender, children }) {
|
||||
function KonvaBridge({ stageRender, children }: { stageRender: any, children: any}) {
|
||||
const mapStageRef = useMapStage();
|
||||
const auth = useAuth();
|
||||
const settings = useSettings();
|
@ -8,7 +8,7 @@ class Settings {
|
||||
currentVersion;
|
||||
storage;
|
||||
|
||||
constructor(name) {
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
// Try and use local storage if it is available, if not mock it with an in memory storage
|
||||
try {
|
||||
@ -22,30 +22,30 @@ class Settings {
|
||||
this.currentVersion = this.get("__version");
|
||||
}
|
||||
|
||||
version(versionNumber, upgradeFunction) {
|
||||
version(versionNumber: number, upgradeFunction: Function) {
|
||||
if (versionNumber > this.currentVersion) {
|
||||
this.currentVersion = versionNumber;
|
||||
this.setAll(upgradeFunction(this.getAll()));
|
||||
}
|
||||
}
|
||||
|
||||
getAll() {
|
||||
getAll(): any {
|
||||
return JSON.parse(this.storage.getItem(this.name));
|
||||
}
|
||||
|
||||
get(key) {
|
||||
get(key: string) {
|
||||
const settings = this.getAll();
|
||||
return settings && settings[key];
|
||||
}
|
||||
|
||||
setAll(newSettings) {
|
||||
setAll(newSettings: any) {
|
||||
this.storage.setItem(
|
||||
this.name,
|
||||
JSON.stringify({ ...newSettings, __version: this.currentVersion })
|
||||
);
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
set(key: string, value: string) {
|
||||
let settings = this.getAll();
|
||||
settings[key] = value;
|
||||
this.setAll(settings);
|
@ -8,9 +8,9 @@ class Size extends Vector2 {
|
||||
/**
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {number=} radius Used to represent hexagon sizes
|
||||
* @param {number} radius Used to represent hexagon sizes
|
||||
*/
|
||||
constructor(width, height, radius) {
|
||||
constructor(width: number, height: number, radius?: number) {
|
||||
super(width, height);
|
||||
this._radius = radius;
|
||||
}
|
||||
@ -18,35 +18,35 @@ class Size extends Vector2 {
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get width() {
|
||||
get width(): number {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} width
|
||||
*/
|
||||
set width(width) {
|
||||
set width(width: number) {
|
||||
this.x = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get height() {
|
||||
get height(): number {
|
||||
return this.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} height
|
||||
*/
|
||||
set height(height) {
|
||||
set height(height: number) {
|
||||
this.y = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
get radius() {
|
||||
get radius(): number {
|
||||
if (this._radius) {
|
||||
return this._radius;
|
||||
} else {
|
||||
@ -57,7 +57,7 @@ class Size extends Vector2 {
|
||||
/**
|
||||
* @param {number} radius
|
||||
*/
|
||||
set radius(radius) {
|
||||
set radius(radius: number) {
|
||||
this._radius = radius;
|
||||
}
|
||||
}
|
@ -5,6 +5,14 @@ import {
|
||||
floorTo as floorToNumber,
|
||||
} from "./shared";
|
||||
|
||||
export type BoundingBox = {
|
||||
min: Vector2,
|
||||
max: Vector2,
|
||||
width: number,
|
||||
height: number,
|
||||
center: Vector2
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector class with x, y and static helper methods
|
||||
*/
|
||||
@ -12,17 +20,17 @@ class Vector2 {
|
||||
/**
|
||||
* @type {number} x - X component of the vector
|
||||
*/
|
||||
x;
|
||||
x: number;
|
||||
/**
|
||||
* @type {number} y - Y component of the vector
|
||||
*/
|
||||
y;
|
||||
y: number;
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
*/
|
||||
constructor(x, y) {
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
@ -31,7 +39,7 @@ class Vector2 {
|
||||
* @param {Vector2} p
|
||||
* @returns {number} Length squared of `p`
|
||||
*/
|
||||
static lengthSquared(p) {
|
||||
static lengthSquared(p: Vector2): number {
|
||||
return p.x * p.x + p.y * p.y;
|
||||
}
|
||||
|
||||
@ -39,7 +47,7 @@ class Vector2 {
|
||||
* @param {Vector2} p
|
||||
* @returns {number} Length of `p`
|
||||
*/
|
||||
static length(p) {
|
||||
static setLength(p: Vector2): number {
|
||||
return Math.sqrt(this.lengthSquared(p));
|
||||
}
|
||||
|
||||
@ -47,8 +55,8 @@ class Vector2 {
|
||||
* @param {Vector2} p
|
||||
* @returns {Vector2} `p` normalized, if length of `p` is 0 `{x: 0, y: 0}` is returned
|
||||
*/
|
||||
static normalize(p) {
|
||||
const l = this.length(p);
|
||||
static normalize(p: Vector2): Vector2 {
|
||||
const l = this.setLength(p);
|
||||
if (l === 0) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
@ -60,7 +68,7 @@ class Vector2 {
|
||||
* @param {Vector2} b
|
||||
* @returns {number} Dot product between `a` and `b`
|
||||
*/
|
||||
static dot(a, b) {
|
||||
static dot(a: Vector2, b: Vector2): number {
|
||||
return a.x * b.x + a.y * b.y;
|
||||
}
|
||||
|
||||
@ -69,7 +77,7 @@ class Vector2 {
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a - b
|
||||
*/
|
||||
static subtract(a, b) {
|
||||
static subtract(a: Vector2, b: Vector2 | number): Vector2 {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x - b, y: a.y - b };
|
||||
} else {
|
||||
@ -82,7 +90,7 @@ class Vector2 {
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a + b
|
||||
*/
|
||||
static add(a, b) {
|
||||
static add(a: Vector2, b: Vector2 | number): Vector2 {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x + b, y: a.y + b };
|
||||
} else {
|
||||
@ -95,7 +103,7 @@ class Vector2 {
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a * b
|
||||
*/
|
||||
static multiply(a, b) {
|
||||
static multiply(a: Vector2, b: Vector2 | number): Vector2 {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x * b, y: a.y * b };
|
||||
} else {
|
||||
@ -108,7 +116,7 @@ class Vector2 {
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} a / b
|
||||
*/
|
||||
static divide(a, b) {
|
||||
static divide(a: Vector2, b: Vector2 | number): Vector2 {
|
||||
if (typeof b === "number") {
|
||||
return { x: a.x / b, y: a.y / b };
|
||||
} else {
|
||||
@ -123,7 +131,7 @@ class Vector2 {
|
||||
* @param {number} angle Angle of rotation in degrees
|
||||
* @returns {Vector2} Rotated point
|
||||
*/
|
||||
static rotate(point, origin, angle) {
|
||||
static rotate(point: Vector2, origin: Vector2, angle: number): Vector2 {
|
||||
const cos = Math.cos(toRadians(angle));
|
||||
const sin = Math.sin(toRadians(angle));
|
||||
const dif = this.subtract(point, origin);
|
||||
@ -139,7 +147,7 @@ class Vector2 {
|
||||
* @param {number} angle Angle of rotation in degrees
|
||||
* @returns {Vector2} Rotated direction
|
||||
*/
|
||||
static rotateDirection(direction, angle) {
|
||||
static rotateDirection(direction: Vector2, angle: number): Vector2 {
|
||||
return this.rotate(direction, { x: 0, y: 0 }, angle);
|
||||
}
|
||||
|
||||
@ -149,7 +157,7 @@ class Vector2 {
|
||||
* @param {(Vector2 | number)} [minimum] Value to compare
|
||||
* @returns {(Vector2 | number)}
|
||||
*/
|
||||
static min(a, minimum) {
|
||||
static min(a: Vector2, minimum?: Vector2 | number): Vector2 | number {
|
||||
if (minimum === undefined) {
|
||||
return a.x < a.y ? a.x : a.y;
|
||||
} else if (typeof minimum === "number") {
|
||||
@ -164,7 +172,7 @@ class Vector2 {
|
||||
* @param {(Vector2 | number)} [maximum] Value to compare
|
||||
* @returns {(Vector2 | number)}
|
||||
*/
|
||||
static max(a, maximum) {
|
||||
static max(a: Vector2, maximum?: Vector2 | number): Vector2 | number {
|
||||
if (maximum === undefined) {
|
||||
return a.x > a.y ? a.x : a.y;
|
||||
} else if (typeof maximum === "number") {
|
||||
@ -180,7 +188,7 @@ class Vector2 {
|
||||
* @param {Vector2} to
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static roundTo(p, to) {
|
||||
static roundTo(p: Vector2, to: Vector2): Vector2 {
|
||||
return {
|
||||
x: roundToNumber(p.x, to.x),
|
||||
y: roundToNumber(p.y, to.y),
|
||||
@ -193,7 +201,7 @@ class Vector2 {
|
||||
* @param {Vector2} to
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static floorTo(p, to) {
|
||||
static floorTo(p: Vector2, to: Vector2): Vector2 {
|
||||
return {
|
||||
x: floorToNumber(p.x, to.x),
|
||||
y: floorToNumber(p.y, to.y),
|
||||
@ -204,7 +212,7 @@ class Vector2 {
|
||||
* @param {Vector2} a
|
||||
* @returns {Vector2} The component wise sign of `a`
|
||||
*/
|
||||
static sign(a) {
|
||||
static sign(a: Vector2): Vector2 {
|
||||
return { x: Math.sign(a.x), y: Math.sign(a.y) };
|
||||
}
|
||||
|
||||
@ -212,7 +220,7 @@ class Vector2 {
|
||||
* @param {Vector2} a
|
||||
* @returns {Vector2} The component wise absolute of `a`
|
||||
*/
|
||||
static abs(a) {
|
||||
static abs(a: Vector2): Vector2 {
|
||||
return { x: Math.abs(a.x), y: Math.abs(a.y) };
|
||||
}
|
||||
|
||||
@ -221,7 +229,7 @@ class Vector2 {
|
||||
* @param {(Vector2 | number)} b
|
||||
* @returns {Vector2} `a` to the power of `b`
|
||||
*/
|
||||
static pow(a, b) {
|
||||
static pow(a: Vector2, b: Vector2 | number): Vector2 {
|
||||
if (typeof b === "number") {
|
||||
return { x: Math.pow(a.x, b), y: Math.pow(a.y, b) };
|
||||
} else {
|
||||
@ -233,7 +241,7 @@ class Vector2 {
|
||||
* @param {Vector2} a
|
||||
* @returns {number} The dot product between `a` and `a`
|
||||
*/
|
||||
static dot2(a) {
|
||||
static dot2(a: Vector2): number {
|
||||
return this.dot(a, a);
|
||||
}
|
||||
|
||||
@ -244,7 +252,7 @@ class Vector2 {
|
||||
* @param {number} max
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static clamp(a, min, max) {
|
||||
static clamp(a: Vector2, min: number, max: number): Vector2 {
|
||||
return {
|
||||
x: Math.min(Math.max(a.x, min), max),
|
||||
y: Math.min(Math.max(a.y, min), max),
|
||||
@ -259,11 +267,11 @@ class Vector2 {
|
||||
* @param {Vector2} b End of the line
|
||||
* @returns {Object} The distance to and the closest point on the line segment
|
||||
*/
|
||||
static distanceToLine(p, a, b) {
|
||||
static distanceToLine(p: Vector2, a: Vector2, b: Vector2): Object {
|
||||
const pa = this.subtract(p, a);
|
||||
const ba = this.subtract(b, a);
|
||||
const h = Math.min(Math.max(this.dot(pa, ba) / this.dot(ba, ba), 0), 1);
|
||||
const distance = this.length(this.subtract(pa, this.multiply(ba, h)));
|
||||
const distance = this.setLength(this.subtract(pa, this.multiply(ba, h)));
|
||||
const point = this.add(a, this.multiply(ba, h));
|
||||
return { distance, point };
|
||||
}
|
||||
@ -278,7 +286,7 @@ class Vector2 {
|
||||
* @param {Vector2} C End of the curve
|
||||
* @returns {Object} The distance to and the closest point on the curve
|
||||
*/
|
||||
static distanceToQuadraticBezier(pos, A, B, C) {
|
||||
static distanceToQuadraticBezier(pos: Vector2, A: Vector2, B: Vector2, C: Vector2): Object {
|
||||
let distance = 0;
|
||||
let point = { x: pos.x, y: pos.y };
|
||||
|
||||
@ -358,7 +366,7 @@ class Vector2 {
|
||||
* @param {Vector2[]} points
|
||||
* @returns {BoundingBox}
|
||||
*/
|
||||
static getBoundingBox(points) {
|
||||
static getBoundingBox(points: Vector2[]): BoundingBox {
|
||||
let minX = Number.MAX_VALUE;
|
||||
let maxX = Number.MIN_VALUE;
|
||||
let minY = Number.MAX_VALUE;
|
||||
@ -389,7 +397,7 @@ class Vector2 {
|
||||
* @param {Vector2[]} points
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static pointInPolygon(p, points) {
|
||||
static pointInPolygon(p: Vector2, points: Vector2[]): boolean {
|
||||
const bounds = this.getBoundingBox(points);
|
||||
if (
|
||||
p.x < bounds.min.x ||
|
||||
@ -422,8 +430,9 @@ class Vector2 {
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @param {number} threshold
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static compare(a, b, threshold) {
|
||||
static compare(a: Vector2, b: Vector2, threshold: number): boolean {
|
||||
return this.lengthSquared(this.subtract(a, b)) < threshold * threshold;
|
||||
}
|
||||
|
||||
@ -431,9 +440,10 @@ class Vector2 {
|
||||
* Returns the distance between two vectors
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @returns {number}
|
||||
*/
|
||||
static distance(a, b) {
|
||||
return this.length(this.subtract(a, b));
|
||||
static distance(a: Vector2, b: Vector2): number {
|
||||
return this.setLength(this.subtract(a, b));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -443,15 +453,16 @@ class Vector2 {
|
||||
* @param {number} alpha
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static lerp(a, b, alpha) {
|
||||
static lerp(a: Vector2, b: Vector2, alpha: number): Vector2 {
|
||||
return { x: lerpNumber(a.x, b.x, alpha), y: lerpNumber(a.y, b.y, alpha) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns total length of a an array of points treated as a path
|
||||
* @param {Vector2[]} points the array of points in the path
|
||||
* @returns {number}
|
||||
*/
|
||||
static pathLength(points) {
|
||||
static pathLength(points: Vector2[]): number {
|
||||
let l = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
l += this.distance(points[i - 1], points[i]);
|
||||
@ -464,8 +475,9 @@ class Vector2 {
|
||||
* based off of http://depts.washington.edu/acelab/proj/dollar/index.html
|
||||
* @param {Vector2[]} points the points to resample
|
||||
* @param {number} n the number of new points
|
||||
* @returns {Vector2[]}
|
||||
*/
|
||||
static resample(points, n) {
|
||||
static resample(points: Vector2[], n: number): Vector2[] {
|
||||
if (points.length === 0 || n <= 0) {
|
||||
return [];
|
||||
}
|
||||
@ -501,7 +513,7 @@ class Vector2 {
|
||||
* @param {("counterClockwise"|"clockwise")=} direction Direction to rotate the vector
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static rotate90(p, direction = "clockwise") {
|
||||
static rotate90(p: Vector2, direction: "counterClockwise" | "clockwise" = "clockwise"): Vector2 {
|
||||
if (direction === "clockwise") {
|
||||
return { x: p.y, y: -p.x };
|
||||
} else {
|
@ -5,22 +5,22 @@ class Vector3 {
|
||||
/**
|
||||
* @type {number} x - X component of the vector
|
||||
*/
|
||||
x;
|
||||
x: number;
|
||||
/**
|
||||
* @type {number} y - Y component of the vector
|
||||
*/
|
||||
y;
|
||||
y: number;
|
||||
/**
|
||||
* @type {number} z - Z component of the vector
|
||||
*/
|
||||
z;
|
||||
z: number;
|
||||
|
||||
/**
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @param {number} z
|
||||
*/
|
||||
constructor(x, y, z) {
|
||||
constructor(x: number, y: number, z: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
@ -31,7 +31,7 @@ class Vector3 {
|
||||
* @param {Vector3} cube
|
||||
* @returns {Vector3}
|
||||
*/
|
||||
static cubeRound(cube) {
|
||||
static cubeRound(cube: Vector3): Vector3 {
|
||||
var rX = Math.round(cube.x);
|
||||
var rY = Math.round(cube.y);
|
||||
var rZ = Math.round(cube.z);
|
@ -1,17 +1,17 @@
|
||||
import shortid from "shortid";
|
||||
|
||||
export function addPolygonDifferenceToShapes(shape, difference, shapes) {
|
||||
export function addPolygonDifferenceToShapes(shape: any, difference: any, shapes: any) {
|
||||
for (let i = 0; i < difference.length; i++) {
|
||||
let newId = shortid.generate();
|
||||
// Holes detected
|
||||
let holes = [];
|
||||
if (difference[i].length > 1) {
|
||||
for (let j = 1; j < difference[i].length; j++) {
|
||||
holes.push(difference[i][j].map(([x, y]) => ({ x, y })));
|
||||
holes.push(difference[i][j].map(([x, y]: [ x: number, y: number ]) => ({ x, y })));
|
||||
}
|
||||
}
|
||||
|
||||
const points = difference[i][0].map(([x, y]) => ({ x, y }));
|
||||
const points = difference[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y }));
|
||||
|
||||
shapes[newId] = {
|
||||
...shape,
|
||||
@ -24,11 +24,11 @@ export function addPolygonDifferenceToShapes(shape, difference, shapes) {
|
||||
}
|
||||
}
|
||||
|
||||
export function addPolygonIntersectionToShapes(shape, intersection, shapes) {
|
||||
export function addPolygonIntersectionToShapes(shape: any, intersection: any, shapes: any) {
|
||||
for (let i = 0; i < intersection.length; i++) {
|
||||
let newId = shortid.generate();
|
||||
|
||||
const points = intersection[i][0].map(([x, y]) => ({ x, y }));
|
||||
const points = intersection[i][0].map(([x, y]: [ x: number, y: number ]) => ({ x, y }));
|
||||
|
||||
shapes[newId] = {
|
||||
...shape,
|
@ -1,7 +1,7 @@
|
||||
import { Texture } from "@babylonjs/core/Materials/Textures/texture";
|
||||
|
||||
// Turn texture load into an async function so it can be awaited
|
||||
export async function importTextureAsync(url) {
|
||||
export async function importTextureAsync(url: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let texture = new Texture(
|
||||
url,
|
@ -2,7 +2,7 @@
|
||||
* @param {Blob} blob
|
||||
* @returns {Promise<Uint8Array>}
|
||||
*/
|
||||
async function blobToBuffer(blob) {
|
||||
async function blobToBuffer(blob: Blob): Promise<Uint8Array> {
|
||||
if (blob.arrayBuffer) {
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
return new Uint8Array(arrayBuffer);
|
||||
@ -10,12 +10,12 @@ async function blobToBuffer(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
function onLoadEnd(event) {
|
||||
function onLoadEnd(event: any) {
|
||||
reader.removeEventListener("loadend", onLoadEnd, false);
|
||||
if (event.error) {
|
||||
reject(event.error);
|
||||
} else {
|
||||
resolve(Buffer.from(reader.result));
|
||||
resolve(Buffer.from(reader.result as ArrayBuffer));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,20 @@
|
||||
export type Colors = {
|
||||
blue: string;
|
||||
orange: string;
|
||||
red: string;
|
||||
yellow: string;
|
||||
purple: string;
|
||||
green: string;
|
||||
pink: string;
|
||||
teal: string;
|
||||
black: string;
|
||||
darkGray: string;
|
||||
lightGray: string;
|
||||
white: string;
|
||||
}
|
||||
|
||||
// Colors used for the game for theme general UI colors look at theme.js
|
||||
const colors = {
|
||||
const colors: Colors = {
|
||||
blue: "rgb(26, 106, 255)",
|
||||
orange: "rgb(255, 116, 51)",
|
||||
red: "rgb(255, 77, 77)",
|
@ -4,7 +4,7 @@ import { Vector3 } from "@babylonjs/core/Maths/math";
|
||||
* Find the number facing up on a mesh instance of a dice
|
||||
* @param {Object} instance The dice instance
|
||||
*/
|
||||
export function getDiceInstanceRoll(instance) {
|
||||
export function getDiceInstanceRoll(instance: any) {
|
||||
let highestDot = -1;
|
||||
let highestLocator;
|
||||
for (let locator of instance.getChildTransformNodes()) {
|
||||
@ -25,7 +25,7 @@ export function getDiceInstanceRoll(instance) {
|
||||
* Find the number facing up on a dice object
|
||||
* @param {Object} dice The Dice object
|
||||
*/
|
||||
export function getDiceRoll(dice) {
|
||||
export function getDiceRoll(dice: any) {
|
||||
let number = getDiceInstanceRoll(dice.instance);
|
||||
// If the dice is a d100 add the d10
|
||||
if (dice.type === "d100") {
|
||||
@ -42,8 +42,8 @@ export function getDiceRoll(dice) {
|
||||
return { type: dice.type, roll: number };
|
||||
}
|
||||
|
||||
export function getDiceRollTotal(diceRolls) {
|
||||
return diceRolls.reduce((accumulator, dice) => {
|
||||
export function getDiceRollTotal(diceRolls: []) {
|
||||
return diceRolls.reduce((accumulator: number, dice: any) => {
|
||||
if (dice.roll === "unknown") {
|
||||
return accumulator;
|
||||
} else {
|
@ -1,7 +1,7 @@
|
||||
import { applyChange, revertChange, diff as deepDiff } from "deep-diff";
|
||||
import { applyChange, Diff, revertChange, diff as deepDiff }from "deep-diff";
|
||||
import get from "lodash.get";
|
||||
|
||||
export function applyChanges(target, changes) {
|
||||
export function applyChanges<LHS>(target: LHS, changes: Diff<LHS, any>[]) {
|
||||
for (let change of changes) {
|
||||
if (change.path && (change.kind === "E" || change.kind === "A")) {
|
||||
// If editing an object or array ensure that the value exists
|
||||
@ -15,7 +15,7 @@ export function applyChanges(target, changes) {
|
||||
}
|
||||
}
|
||||
|
||||
export function revertChanges(target, changes) {
|
||||
export function revertChanges<LHS>(target: LHS, changes: Diff<LHS, any>[]) {
|
||||
for (let change of changes) {
|
||||
revertChange(target, true, change);
|
||||
}
|
@ -1,15 +1,20 @@
|
||||
import simplify from "simplify-js";
|
||||
import polygonClipping from "polygon-clipping";
|
||||
import polygonClipping, { Geom, Polygon, Ring } from "polygon-clipping";
|
||||
|
||||
import Vector2 from "./Vector2";
|
||||
import Vector2, { BoundingBox } from "./Vector2";
|
||||
import Size from "./Size"
|
||||
import { toDegrees } from "./shared";
|
||||
import { getNearestCellCoordinates, getCellLocation } from "./grid";
|
||||
import { Grid, getNearestCellCoordinates, getCellLocation } from "./grid";
|
||||
|
||||
/**
|
||||
* @typedef PointsData
|
||||
* @property {Vector2[]} points
|
||||
*/
|
||||
|
||||
type PointsData = {
|
||||
points: Vector2[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RectData
|
||||
* @property {number} x
|
||||
@ -18,30 +23,55 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid";
|
||||
* @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
|
||||
* @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
|
||||
@ -53,6 +83,16 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid";
|
||||
* @property {"path"} type
|
||||
*/
|
||||
|
||||
// type Path = {
|
||||
// blend: boolean,
|
||||
// color: string,
|
||||
// data: PointsData,
|
||||
// id: string,
|
||||
// pathType: PathType,
|
||||
// strokeWidth: number,
|
||||
// type: "path"
|
||||
// }
|
||||
|
||||
/**
|
||||
* @typedef Shape
|
||||
* @property {boolean} blend
|
||||
@ -64,6 +104,16 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid";
|
||||
* @property {"shape"} type
|
||||
*/
|
||||
|
||||
// type Shape = {
|
||||
// blend: boolean,
|
||||
// color: string,
|
||||
// data: ShapeData,
|
||||
// id: string,
|
||||
// shapeType: ShapeType,
|
||||
// strokeWidth: number,
|
||||
// type: "shape"
|
||||
// }
|
||||
|
||||
/**
|
||||
* @typedef Fog
|
||||
* @property {string} color
|
||||
@ -74,29 +124,39 @@ import { getNearestCellCoordinates, getCellLocation } from "./grid";
|
||||
* @property {boolean} visible
|
||||
*/
|
||||
|
||||
type Fog = {
|
||||
color: string,
|
||||
data: FogData,
|
||||
id: string,
|
||||
strokeWidth: number,
|
||||
type: "fog",
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {ShapeType} type
|
||||
* @param {Vector2} brushPosition
|
||||
* @returns {ShapeData}
|
||||
*/
|
||||
export function getDefaultShapeData(type, brushPosition) {
|
||||
export function getDefaultShapeData(type: ShapeType, brushPosition: Vector2): ShapeData | undefined{
|
||||
// TODO: handle undefined if no type found
|
||||
if (type === "line") {
|
||||
return {
|
||||
points: [
|
||||
{ x: brushPosition.x, y: brushPosition.y },
|
||||
{ x: brushPosition.x, y: brushPosition.y },
|
||||
],
|
||||
};
|
||||
} as PointsData;
|
||||
} else if (type === "circle") {
|
||||
return { x: brushPosition.x, y: brushPosition.y, radius: 0 };
|
||||
return { x: brushPosition.x, y: brushPosition.y, radius: 0 } as CircleData;
|
||||
} else if (type === "rectangle") {
|
||||
return {
|
||||
x: brushPosition.x,
|
||||
y: brushPosition.y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
} as RectData;
|
||||
} else if (type === "triangle") {
|
||||
return {
|
||||
points: [
|
||||
@ -104,7 +164,7 @@ export function getDefaultShapeData(type, brushPosition) {
|
||||
{ x: brushPosition.x, y: brushPosition.y },
|
||||
{ x: brushPosition.x, y: brushPosition.y },
|
||||
],
|
||||
};
|
||||
} as PointsData;
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +172,7 @@ export function getDefaultShapeData(type, brushPosition) {
|
||||
* @param {Vector2} cellSize
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export function getGridCellRatio(cellSize) {
|
||||
export function getGridCellRatio(cellSize: Vector2): Vector2 {
|
||||
if (cellSize.x < cellSize.y) {
|
||||
return { x: cellSize.y / cellSize.x, y: 1 };
|
||||
} else if (cellSize.y < cellSize.x) {
|
||||
@ -131,30 +191,34 @@ export function getGridCellRatio(cellSize) {
|
||||
* @returns {ShapeData}
|
||||
*/
|
||||
export function getUpdatedShapeData(
|
||||
type,
|
||||
data,
|
||||
brushPosition,
|
||||
gridCellNormalizedSize,
|
||||
mapWidth,
|
||||
mapHeight
|
||||
) {
|
||||
type: ShapeType,
|
||||
data: ShapeData,
|
||||
brushPosition: Vector2,
|
||||
gridCellNormalizedSize: Vector2,
|
||||
mapWidth: number,
|
||||
mapHeight: number
|
||||
): ShapeData | undefined {
|
||||
// TODO: handle undefined type
|
||||
if (type === "line") {
|
||||
data = data as PointsData;
|
||||
return {
|
||||
points: [data.points[0], { x: brushPosition.x, y: brushPosition.y }],
|
||||
};
|
||||
} as PointsData;
|
||||
} else if (type === "circle") {
|
||||
data = data as CircleData;
|
||||
const gridRatio = getGridCellRatio(gridCellNormalizedSize);
|
||||
const dif = Vector2.subtract(brushPosition, {
|
||||
x: data.x,
|
||||
y: data.y,
|
||||
});
|
||||
const scaled = Vector2.multiply(dif, gridRatio);
|
||||
const distance = Vector2.length(scaled);
|
||||
const distance = Vector2.setLength(scaled);
|
||||
return {
|
||||
...data,
|
||||
radius: distance,
|
||||
};
|
||||
} else if (type === "rectangle") {
|
||||
data = data as RectData;
|
||||
const dif = Vector2.subtract(brushPosition, { x: data.x, y: data.y });
|
||||
return {
|
||||
...data,
|
||||
@ -162,6 +226,7 @@ export function getUpdatedShapeData(
|
||||
height: dif.y,
|
||||
};
|
||||
} else if (type === "triangle") {
|
||||
data = data as PointsData;
|
||||
// Convert to absolute coordinates
|
||||
const mapSize = { x: mapWidth, y: mapHeight };
|
||||
const brushPositionPixel = Vector2.multiply(brushPosition, mapSize);
|
||||
@ -169,7 +234,7 @@ export function getUpdatedShapeData(
|
||||
const points = data.points;
|
||||
const startPixel = Vector2.multiply(points[0], mapSize);
|
||||
const dif = Vector2.subtract(brushPositionPixel, startPixel);
|
||||
const length = Vector2.length(dif);
|
||||
const length = Vector2.setLength(dif);
|
||||
const direction = Vector2.normalize(dif);
|
||||
// Get the angle for a triangle who's width is the same as it's length
|
||||
const angle = Math.atan(length / 2 / (length === 0 ? 1 : length));
|
||||
@ -199,10 +264,10 @@ const defaultSimplifySize = 1 / 100;
|
||||
* @param {Vector2} gridCellSize
|
||||
* @param {number} scale
|
||||
*/
|
||||
export function simplifyPoints(points, gridCellSize, scale) {
|
||||
export function simplifyPoints(points: Vector2[], gridCellSize: Vector2, scale: number): any {
|
||||
return simplify(
|
||||
points,
|
||||
(Vector2.min(gridCellSize) * defaultSimplifySize) / scale
|
||||
(Vector2.min(gridCellSize) as number * defaultSimplifySize) / scale
|
||||
);
|
||||
}
|
||||
|
||||
@ -212,43 +277,50 @@ export function simplifyPoints(points, gridCellSize, scale) {
|
||||
* @param {boolean} ignoreHidden
|
||||
* @returns {Fog[]}
|
||||
*/
|
||||
export function mergeFogShapes(shapes, ignoreHidden = true) {
|
||||
export function mergeFogShapes(shapes: Fog[], ignoreHidden: boolean = true): Fog[] {
|
||||
if (shapes.length === 0) {
|
||||
return shapes;
|
||||
}
|
||||
let geometries = [];
|
||||
let geometries: Geom[] = [];
|
||||
for (let shape of shapes) {
|
||||
if (ignoreHidden && !shape.visible) {
|
||||
continue;
|
||||
}
|
||||
const shapePoints = shape.data.points.map(({ x, y }) => [x, y]);
|
||||
const shapeHoles = shape.data.holes.map((hole) =>
|
||||
hole.map(({ x, y }) => [x, y])
|
||||
const shapePoints: Ring = shape.data.points.map(({ x, y }) => [x, y]);
|
||||
const shapeHoles: Polygon = shape.data.holes.map((hole) =>
|
||||
hole.map(({ x, y }: { x: number, y: number }) => [x, y])
|
||||
);
|
||||
let shapeGeom = [[shapePoints, ...shapeHoles]];
|
||||
let shapeGeom: Geom = [[shapePoints, ...shapeHoles]];
|
||||
geometries.push(shapeGeom);
|
||||
}
|
||||
if (geometries.length === 0) {
|
||||
return geometries;
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
let union = polygonClipping.union(...geometries);
|
||||
let merged = [];
|
||||
let union = polygonClipping.union(geometries[0], ...geometries.slice(1));
|
||||
let merged: Fog[] = [];
|
||||
for (let i = 0; i < union.length; i++) {
|
||||
let holes = [];
|
||||
let holes: Vector2[][] = [];
|
||||
if (union[i].length > 1) {
|
||||
for (let j = 1; j < union[i].length; j++) {
|
||||
holes.push(union[i][j].map(([x, y]) => ({ x, y })));
|
||||
}
|
||||
}
|
||||
// find the first visible shape
|
||||
let visibleShape = shapes.find((shape) => ignoreHidden || shape.visible);
|
||||
if (!visibleShape) {
|
||||
// TODO: handle if visible shape not found
|
||||
throw Error;
|
||||
}
|
||||
merged.push({
|
||||
// Use the data of the first visible shape as the merge
|
||||
...shapes.find((shape) => ignoreHidden || shape.visible),
|
||||
...visibleShape,
|
||||
id: `merged-${i}`,
|
||||
data: {
|
||||
points: union[i][0].map(([x, y]) => ({ x, y })),
|
||||
holes,
|
||||
},
|
||||
type: "fog"
|
||||
});
|
||||
}
|
||||
return merged;
|
||||
@ -263,7 +335,7 @@ export function mergeFogShapes(shapes, ignoreHidden = true) {
|
||||
* @param {boolean} maxPoints Max amount of points per shape to get bounds for
|
||||
* @returns {Vector2.BoundingBox[]}
|
||||
*/
|
||||
export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) {
|
||||
export function getFogShapesBoundingBoxes(shapes: Fog[], maxPoints = 0): BoundingBox[] {
|
||||
let boxes = [];
|
||||
for (let shape of shapes) {
|
||||
if (maxPoints > 0 && shape.data.points.length > maxPoints) {
|
||||
@ -280,14 +352,26 @@ export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) {
|
||||
* @property {Vector2} end
|
||||
*/
|
||||
|
||||
// type Edge = {
|
||||
// start: Vector2,
|
||||
// end: Vector2
|
||||
// }
|
||||
|
||||
/**
|
||||
* @typedef Guide
|
||||
* @property {Vector2} start
|
||||
* @property {Vector2} end
|
||||
* @property {("horizontal"|"vertical")} orientation
|
||||
* @property {number}
|
||||
* @property {number} distance
|
||||
*/
|
||||
|
||||
type Guide = {
|
||||
start: Vector2,
|
||||
end: Vector2,
|
||||
orientation: "horizontal" | "vertical",
|
||||
distance: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} brushPosition Brush position in pixels
|
||||
* @param {Vector2} grid
|
||||
@ -299,14 +383,14 @@ export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) {
|
||||
* @returns {Guide[]}
|
||||
*/
|
||||
export function getGuidesFromGridCell(
|
||||
brushPosition,
|
||||
grid,
|
||||
gridCellSize,
|
||||
gridOffset,
|
||||
gridCellOffset,
|
||||
snappingSensitivity,
|
||||
mapSize
|
||||
) {
|
||||
brushPosition: Vector2,
|
||||
grid: Grid,
|
||||
gridCellSize: Size,
|
||||
gridOffset: Vector2,
|
||||
gridCellOffset: Vector2,
|
||||
snappingSensitivity: number,
|
||||
mapSize: Vector2
|
||||
): Guide[] {
|
||||
let boundingBoxes = [];
|
||||
// Add map bounds
|
||||
boundingBoxes.push(
|
||||
@ -366,11 +450,11 @@ export function getGuidesFromGridCell(
|
||||
* @returns {Guide[]}
|
||||
*/
|
||||
export function getGuidesFromBoundingBoxes(
|
||||
brushPosition,
|
||||
boundingBoxes,
|
||||
gridCellSize,
|
||||
snappingSensitivity
|
||||
) {
|
||||
brushPosition: Vector2,
|
||||
boundingBoxes: BoundingBox[],
|
||||
gridCellSize: Vector2, // TODO: check if this was meant to be of type Size
|
||||
snappingSensitivity: number
|
||||
): Guide[] {
|
||||
let horizontalEdges = [];
|
||||
let verticalEdges = [];
|
||||
for (let bounds of boundingBoxes) {
|
||||
@ -400,7 +484,7 @@ export function getGuidesFromBoundingBoxes(
|
||||
end: { x: bounds.max.x, y: bounds.max.y },
|
||||
});
|
||||
}
|
||||
let guides = [];
|
||||
let guides: Guide[] = [];
|
||||
for (let edge of verticalEdges) {
|
||||
const distance = Math.abs(brushPosition.x - edge.start.x);
|
||||
if (distance / gridCellSize.x < snappingSensitivity) {
|
||||
@ -421,8 +505,8 @@ export function getGuidesFromBoundingBoxes(
|
||||
* @param {Guide[]} guides
|
||||
* @returns {Guide[]}
|
||||
*/
|
||||
export function findBestGuides(brushPosition, guides) {
|
||||
let bestGuides = [];
|
||||
export function findBestGuides(brushPosition: Vector2, guides: Guide[]): Guide[] {
|
||||
let bestGuides: Guide[] = [];
|
||||
let verticalGuide = guides
|
||||
.filter((guide) => guide.orientation === "vertical")
|
||||
.sort((a, b) => a.distance - b.distance)[0];
|
@ -14,12 +14,22 @@ const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented");
|
||||
* @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
|
||||
@ -27,6 +37,12 @@ const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented");
|
||||
* @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
|
||||
@ -35,7 +51,7 @@ const GRID_TYPE_NOT_IMPLEMENTED = new Error("Grid type not implemented");
|
||||
* @param {number} baseHeight Height of the grid in pixels before inset
|
||||
* @returns {Size}
|
||||
*/
|
||||
export function getGridPixelSize(grid, baseWidth, baseHeight) {
|
||||
export function getGridPixelSize(grid: Grid, baseWidth: number, baseHeight: number): Size {
|
||||
const width = (grid.inset.bottomRight.x - grid.inset.topLeft.x) * baseWidth;
|
||||
const height = (grid.inset.bottomRight.y - grid.inset.topLeft.y) * baseHeight;
|
||||
return new Size(width, height);
|
||||
@ -48,7 +64,7 @@ export function getGridPixelSize(grid, baseWidth, baseHeight) {
|
||||
* @param {number} gridHeight Height of the grid in pixels after inset
|
||||
* @returns {Size}
|
||||
*/
|
||||
export function getCellPixelSize(grid, gridWidth, gridHeight) {
|
||||
export function getCellPixelSize(grid: Grid, gridWidth: number, gridHeight: number): Size {
|
||||
switch (grid.type) {
|
||||
case "square":
|
||||
return new Size(gridWidth / grid.size.x, gridHeight / grid.size.y);
|
||||
@ -72,7 +88,7 @@ export function getCellPixelSize(grid, gridWidth, gridHeight) {
|
||||
* @param {Size} cellSize Cell size in pixels
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export function getCellLocation(grid, col, row, cellSize) {
|
||||
export function getCellLocation(grid: Grid, col: number, row: number, cellSize: Size): Vector2 {
|
||||
switch (grid.type) {
|
||||
case "square":
|
||||
return {
|
||||
@ -102,7 +118,7 @@ export function getCellLocation(grid, col, row, cellSize) {
|
||||
* @param {Size} cellSize Cell size in pixels
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export function getNearestCellCoordinates(grid, x, y, cellSize) {
|
||||
export function getNearestCellCoordinates(grid: Grid, x: number, y: number, cellSize: Size): Vector2 {
|
||||
switch (grid.type) {
|
||||
case "square":
|
||||
return Vector2.divide(Vector2.floorTo({ x, y }, cellSize), cellSize);
|
||||
@ -132,7 +148,7 @@ export function getNearestCellCoordinates(grid, x, y, cellSize) {
|
||||
* @param {Size} cellSize Cell size in pixels
|
||||
* @returns {Vector2[]}
|
||||
*/
|
||||
export function getCellCorners(grid, x, y, cellSize) {
|
||||
export function getCellCorners(grid: Grid, x: number, y: number, cellSize: Size): Vector2[] {
|
||||
const position = new Vector2(x, y);
|
||||
switch (grid.type) {
|
||||
case "square":
|
||||
@ -172,8 +188,9 @@ export function getCellCorners(grid, x, y, cellSize) {
|
||||
* Get the height of a grid based off of its width
|
||||
* @param {Grid} grid
|
||||
* @param {number} gridWidth Width of the grid in pixels after inset
|
||||
* @returns {number}
|
||||
*/
|
||||
function getGridHeightFromWidth(grid, gridWidth) {
|
||||
function getGridHeightFromWidth(grid: Grid, gridWidth: number): number{
|
||||
switch (grid.type) {
|
||||
case "square":
|
||||
return (grid.size.y * gridWidth) / grid.size.x;
|
||||
@ -195,7 +212,7 @@ function getGridHeightFromWidth(grid, gridWidth) {
|
||||
* @param {number} mapHeight Height of the map in pixels before inset
|
||||
* @returns {GridInset}
|
||||
*/
|
||||
export function getGridDefaultInset(grid, mapWidth, mapHeight) {
|
||||
export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset {
|
||||
// Max the width of the inset and figure out the resulting height value
|
||||
const insetHeightNorm = getGridHeightFromWidth(grid, mapWidth) / mapHeight;
|
||||
return { topLeft: { x: 0, y: 0 }, bottomRight: { x: 1, y: insetHeightNorm } };
|
||||
@ -208,7 +225,7 @@ export function getGridDefaultInset(grid, mapWidth, mapHeight) {
|
||||
* @param {number} mapHeight Height of the map in pixels before inset
|
||||
* @returns {GridInset}
|
||||
*/
|
||||
export function getGridUpdatedInset(grid, mapWidth, mapHeight) {
|
||||
export function getGridUpdatedInset(grid: Grid, mapWidth: number, mapHeight: number): GridInset {
|
||||
let inset = grid.inset;
|
||||
// Take current inset width and use it to calculate the new height
|
||||
if (grid.size.x > 0 && grid.size.x > 0) {
|
||||
@ -226,7 +243,7 @@ export function getGridUpdatedInset(grid, mapWidth, mapHeight) {
|
||||
* @param {Grid} grid
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getGridMaxZoom(grid) {
|
||||
export function getGridMaxZoom(grid: Grid): number {
|
||||
if (!grid) {
|
||||
return 10;
|
||||
}
|
||||
@ -240,7 +257,7 @@ export function getGridMaxZoom(grid) {
|
||||
* @param {("hexVertical"|"hexHorizontal")} type
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export function hexCubeToOffset(cube, type) {
|
||||
export function hexCubeToOffset(cube: Vector3, type: ("hexVertical"|"hexHorizontal")) {
|
||||
if (type === "hexVertical") {
|
||||
const x = cube.x + (cube.z + (cube.z & 1)) / 2;
|
||||
const y = cube.z;
|
||||
@ -257,7 +274,7 @@ export function hexCubeToOffset(cube, type) {
|
||||
* @param {("hexVertical"|"hexHorizontal")} type
|
||||
* @returns {Vector3}
|
||||
*/
|
||||
export function hexOffsetToCube(offset, type) {
|
||||
export function hexOffsetToCube(offset: Vector2, type: ("hexVertical"|"hexHorizontal")) {
|
||||
if (type === "hexVertical") {
|
||||
const x = offset.x - (offset.y + (offset.y & 1)) / 2;
|
||||
const z = offset.y;
|
||||
@ -276,8 +293,9 @@ export function hexOffsetToCube(offset, type) {
|
||||
* @param {Grid} grid
|
||||
* @param {Vector2} a
|
||||
* @param {Vector2} b
|
||||
* @param {Size} cellSize
|
||||
*/
|
||||
export function gridDistance(grid, a, b, cellSize) {
|
||||
export function gridDistance(grid: Grid, a: Vector2, b: Vector2, cellSize: Size) {
|
||||
// Get grid coordinates
|
||||
const aCoord = getNearestCellCoordinates(grid, a.x, a.y, cellSize);
|
||||
const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize);
|
||||
@ -290,8 +308,8 @@ export function gridDistance(grid, a, b, cellSize) {
|
||||
} else if (grid.measurement.type === "alternating") {
|
||||
// Alternating diagonal distance like D&D 3.5 and Pathfinder
|
||||
const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord));
|
||||
const max = Vector2.max(delta);
|
||||
const min = Vector2.min(delta);
|
||||
const max: any = Vector2.max(delta);
|
||||
const min: any = Vector2.min(delta);
|
||||
return max - min + Math.floor(1.5 * min);
|
||||
} else if (grid.measurement.type === "euclidean") {
|
||||
return Vector2.distance(aCoord, bCoord);
|
||||
@ -322,15 +340,25 @@ export function gridDistance(grid, a, b, cellSize) {
|
||||
* @property {number} digits The precision of the scale
|
||||
*/
|
||||
|
||||
type GridScale = {
|
||||
multiplier: number,
|
||||
unit: string,
|
||||
digits: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string representation of scale e.g. 5ft into a `GridScale`
|
||||
* @param {string} scale
|
||||
* @returns {GridScale}
|
||||
*/
|
||||
|
||||
export function parseGridScale(scale) {
|
||||
export function parseGridScale(scale: string): GridScale {
|
||||
if (typeof scale === "string") {
|
||||
const match = scale.match(/(\d*)(\.\d*)?([a-zA-Z]*)/);
|
||||
// TODO: handle case where match is not found
|
||||
if (!match) {
|
||||
throw Error;
|
||||
}
|
||||
const integer = parseFloat(match[1]);
|
||||
const fractional = parseFloat(match[2]);
|
||||
const unit = match[3] || "";
|
||||
@ -352,7 +380,7 @@ export function parseGridScale(scale) {
|
||||
* @param {number} n
|
||||
* @returns {number[]}
|
||||
*/
|
||||
function factors(n) {
|
||||
function factors(n: number): number[] {
|
||||
const numbers = Array.from(Array(n + 1), (_, i) => i);
|
||||
return numbers.filter((i) => n % i === 0);
|
||||
}
|
||||
@ -364,7 +392,7 @@ function factors(n) {
|
||||
* @param {number} b
|
||||
* @returns {number}
|
||||
*/
|
||||
function gcd(a, b) {
|
||||
function gcd(a: number, b: number): number {
|
||||
while (b !== 0) {
|
||||
const t = b;
|
||||
b = a % b;
|
||||
@ -379,7 +407,7 @@ function gcd(a, b) {
|
||||
* @param {number} b
|
||||
* @returns {number[]}
|
||||
*/
|
||||
function dividers(a, b) {
|
||||
function dividers(a: number, b: number): number[] {
|
||||
const d = gcd(a, b);
|
||||
return factors(d);
|
||||
}
|
||||
@ -398,7 +426,7 @@ const maxGridSize = 200;
|
||||
* @param {number} y
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function gridSizeVaild(x, y) {
|
||||
export function gridSizeVaild(x: number, y: number): boolean {
|
||||
return (
|
||||
x > minGridSize && y > minGridSize && x < maxGridSize && y < maxGridSize
|
||||
);
|
||||
@ -408,11 +436,12 @@ export function gridSizeVaild(x, y) {
|
||||
* Finds a grid size for an image by finding the closest size to the average grid size
|
||||
* @param {Image} image
|
||||
* @param {number[]} candidates
|
||||
* @returns {Vector2}
|
||||
* @returns {Vector2 | null}
|
||||
*/
|
||||
function gridSizeHeuristic(image, candidates) {
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
function gridSizeHeuristic(image: CanvasImageSource, candidates: number[]): Vector2 | null {
|
||||
// TODO: check type for Image and CanvasSourceImage
|
||||
const width: any = image.width;
|
||||
const height: any = image.height;
|
||||
// Find the best candidate by comparing the absolute z-scores of each axis
|
||||
let bestX = 1;
|
||||
let bestY = 1;
|
||||
@ -440,17 +469,23 @@ function gridSizeHeuristic(image, candidates) {
|
||||
* Finds the grid size of an image by running the image through a machine learning model
|
||||
* @param {Image} image
|
||||
* @param {number[]} candidates
|
||||
* @returns {Vector2}
|
||||
* @returns {Vector2 | null}
|
||||
*/
|
||||
async function gridSizeML(image, candidates) {
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
async function gridSizeML(image: CanvasImageSource, candidates: number[]): Promise<Vector2 | null> {
|
||||
// TODO: check this function because of context and CanvasImageSource -> JSDoc and Typescript do not match
|
||||
const width: any = image.width;
|
||||
const height: any = image.height;
|
||||
const ratio = width / height;
|
||||
let canvas = document.createElement("canvas");
|
||||
let context = canvas.getContext("2d");
|
||||
canvas.width = 2048;
|
||||
canvas.height = Math.floor(2048 / ratio);
|
||||
|
||||
// TODO: handle if context is null
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
let imageData = context.getImageData(
|
||||
@ -507,8 +542,10 @@ async function gridSizeML(image, candidates) {
|
||||
* @param {Image} image
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export async function getGridSizeFromImage(image) {
|
||||
const candidates = dividers(image.width, image.height);
|
||||
export async function getGridSizeFromImage(image: CanvasImageSource) {
|
||||
const width: any = image.width;
|
||||
const height: any = image.height;
|
||||
const candidates = dividers(width, height);
|
||||
let prediction;
|
||||
|
||||
// Try and use ML grid detection
|
@ -6,13 +6,18 @@ const lightnessDetectionOffset = 0.1;
|
||||
* @param {HTMLImageElement} image
|
||||
* @returns {boolean} True is the image is light
|
||||
*/
|
||||
export function getImageLightness(image) {
|
||||
export function getImageLightness(image: HTMLImageElement) {
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
let context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
// TODO: handle if context is null
|
||||
return;
|
||||
}
|
||||
|
||||
context.drawImage(image, 0, 0);
|
||||
const imageData = context.getImageData(0, 0, width, height);
|
||||
|
||||
@ -44,13 +49,19 @@ export function getImageLightness(image) {
|
||||
* @property {number} height
|
||||
*/
|
||||
|
||||
type CanvasImage = {
|
||||
blob: Blob | null,
|
||||
width: number,
|
||||
height: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
* @param {string} type
|
||||
* @param {number} quality
|
||||
* @returns {Promise<CanvasImage>}
|
||||
*/
|
||||
export async function canvasToImage(canvas, type, quality) {
|
||||
export async function canvasToImage(canvas: HTMLCanvasElement, type: string, quality: number): Promise<CanvasImage> {
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
@ -69,7 +80,7 @@ export async function canvasToImage(canvas, type, quality) {
|
||||
* @param {number} quality if image is a jpeg or webp this is the quality setting
|
||||
* @returns {Promise<CanvasImage>}
|
||||
*/
|
||||
export async function resizeImage(image, size, type, quality) {
|
||||
export async function resizeImage(image: HTMLImageElement, size: number, type: string, quality: number): Promise<CanvasImage> {
|
||||
const width = image.width;
|
||||
const height = image.height;
|
||||
const ratio = width / height;
|
||||
@ -82,8 +93,10 @@ export async function resizeImage(image, size, type, quality) {
|
||||
canvas.height = size;
|
||||
}
|
||||
let context = canvas.getContext("2d");
|
||||
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
// TODO: Add error if context is empty
|
||||
if (context) {
|
||||
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
return await canvasToImage(canvas, type, quality);
|
||||
}
|
||||
|
||||
@ -96,6 +109,13 @@ export async function resizeImage(image, size, type, quality) {
|
||||
* @property {string} id
|
||||
*/
|
||||
|
||||
type ImageFile = {
|
||||
file: Uint8Array | null,
|
||||
width: number,
|
||||
height: number,
|
||||
type: "file",
|
||||
id: string
|
||||
}
|
||||
/**
|
||||
* Create a image file with resolution `size`x`size` with cover cropping
|
||||
* @param {HTMLImageElement} image the image to resize
|
||||
@ -104,7 +124,7 @@ export async function resizeImage(image, size, type, quality) {
|
||||
* @param {number} quality if image is a jpeg or webp this is the quality setting
|
||||
* @returns {Promise<ImageFile>}
|
||||
*/
|
||||
export async function createThumbnail(image, type, size = 300, quality = 0.5) {
|
||||
export async function createThumbnail(image: HTMLImageElement, type: string, size = 300, quality = 0.5): Promise<ImageFile> {
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
@ -113,31 +133,35 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) {
|
||||
if (ratio > 1) {
|
||||
const center = image.width / 2;
|
||||
const halfHeight = image.height / 2;
|
||||
context.drawImage(
|
||||
image,
|
||||
center - halfHeight,
|
||||
0,
|
||||
image.height,
|
||||
image.height,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
if (context) {
|
||||
context.drawImage(
|
||||
image,
|
||||
center - halfHeight,
|
||||
0,
|
||||
image.height,
|
||||
image.height,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const center = image.height / 2;
|
||||
const halfWidth = image.width / 2;
|
||||
context.drawImage(
|
||||
image,
|
||||
0,
|
||||
center - halfWidth,
|
||||
image.width,
|
||||
image.width,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
if (context) {
|
||||
context.drawImage(
|
||||
image,
|
||||
0,
|
||||
center - halfWidth,
|
||||
image.width,
|
||||
image.width,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnailImage = await canvasToImage(canvas, type, quality);
|
@ -6,9 +6,9 @@ import Color from "color";
|
||||
import Vector2 from "./Vector2";
|
||||
|
||||
// Holes should be wound in the opposite direction as the containing points array
|
||||
export function HoleyLine({ holes, ...props }) {
|
||||
export function HoleyLine({ holes, ...props }: { holes: any, props: []}) {
|
||||
// Converted from https://github.com/rfestag/konva/blob/master/src/shapes/Line.ts
|
||||
function drawLine(points, context, shape) {
|
||||
function drawLine(points: number[], context: any, shape: any) {
|
||||
const length = points.length;
|
||||
const tension = shape.tension();
|
||||
const closed = shape.closed();
|
||||
@ -76,7 +76,7 @@ export function HoleyLine({ holes, ...props }) {
|
||||
}
|
||||
|
||||
// Draw points and holes
|
||||
function sceneFunc(context, shape) {
|
||||
function sceneFunc(context: any, shape: any) {
|
||||
const points = shape.points();
|
||||
const closed = shape.closed();
|
||||
|
||||
@ -109,7 +109,7 @@ export function HoleyLine({ holes, ...props }) {
|
||||
return <Line sceneFunc={sceneFunc} {...props} />;
|
||||
}
|
||||
|
||||
export function Tick({ x, y, scale, onClick, cross }) {
|
||||
export function Tick({ x, y, scale, onClick, cross }: { x: any, y: any, scale: any, onClick: any, cross: any}) {
|
||||
const [fill, setFill] = useState("white");
|
||||
function handleEnter() {
|
||||
setFill("hsl(260, 100%, 80%)");
|
||||
@ -144,13 +144,17 @@ export function Tick({ x, y, scale, onClick, cross }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Trail({ position, size, duration, segments, color }) {
|
||||
const trailRef = useRef();
|
||||
const pointsRef = useRef([]);
|
||||
interface TrailPoint extends Vector2 {
|
||||
lifetime: number
|
||||
}
|
||||
|
||||
export function Trail({ position, size, duration, segments, color }: { position: Vector2, size: any, duration: number, segments: any, color: string }) {
|
||||
const trailRef: React.MutableRefObject<Konva.Line | undefined> = useRef();
|
||||
const pointsRef: React.MutableRefObject<TrailPoint[]> = useRef([]);
|
||||
const prevPositionRef = useRef(position);
|
||||
const positionRef = useRef(position);
|
||||
const circleRef = useRef();
|
||||
// Color of the end of the trial
|
||||
const circleRef: React.MutableRefObject<Konva.Circle | undefined> = useRef();
|
||||
// Color of the end of the trail
|
||||
const transparentColorRef = useRef(
|
||||
Color(color).lighten(0.5).alpha(0).string()
|
||||
);
|
||||
@ -178,7 +182,7 @@ export function Trail({ position, size, duration, segments, color }) {
|
||||
useEffect(() => {
|
||||
let prevTime = performance.now();
|
||||
let request = requestAnimationFrame(animate);
|
||||
function animate(time) {
|
||||
function animate(time: any) {
|
||||
request = requestAnimationFrame(animate);
|
||||
const deltaTime = time - prevTime;
|
||||
prevTime = time;
|
||||
@ -199,13 +203,13 @@ export function Trail({ position, size, duration, segments, color }) {
|
||||
}
|
||||
|
||||
// Update the circle position to keep it in sync with the trail
|
||||
if (circleRef.current) {
|
||||
if (circleRef && circleRef.current) {
|
||||
circleRef.current.x(positionRef.current.x);
|
||||
circleRef.current.y(positionRef.current.y);
|
||||
}
|
||||
|
||||
if (trailRef.current) {
|
||||
trailRef.current.getLayer().draw();
|
||||
if (trailRef && trailRef.current) {
|
||||
trailRef.current.getLayer()?.draw();
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,14 +219,15 @@ export function Trail({ position, size, duration, segments, color }) {
|
||||
}, []);
|
||||
|
||||
// Custom scene function for drawing a trail from a line
|
||||
function sceneFunc(context) {
|
||||
function sceneFunc(context: any) {
|
||||
// Resample points to ensure a smooth trail
|
||||
const resampledPoints = Vector2.resample(pointsRef.current, segments);
|
||||
if (resampledPoints.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Draws a line offset in the direction perpendicular to its travel direction
|
||||
const drawOffsetLine = (from, to, alpha) => {
|
||||
// TODO: check alpha type
|
||||
const drawOffsetLine = (from: Vector2, to: Vector2, alpha: number) => {
|
||||
const forward = Vector2.normalize(Vector2.subtract(from, to));
|
||||
// Rotate the forward vector 90 degrees based off of the direction
|
||||
const side = Vector2.rotate90(forward);
|
||||
@ -254,7 +259,7 @@ export function Trail({ position, size, duration, segments, color }) {
|
||||
// Create a radial gradient from the center of the trail to the tail
|
||||
const gradientCenter = resampledPoints[resampledPoints.length - 1];
|
||||
const gradientEnd = resampledPoints[0];
|
||||
const gradientRadius = Vector2.length(
|
||||
const gradientRadius = Vector2.setLength(
|
||||
Vector2.subtract(gradientCenter, gradientEnd)
|
||||
);
|
||||
let gradient = context.createRadialGradient(
|
||||
@ -297,15 +302,24 @@ Trail.defaultProps = {
|
||||
* @param {Konva.Node} node
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
export function getRelativePointerPosition(node) {
|
||||
export function getRelativePointerPosition(node: Konva.Node): { x: number, y: number } | undefined {
|
||||
let transform = node.getAbsoluteTransform().copy();
|
||||
transform.invert();
|
||||
let position = node.getStage().getPointerPosition();
|
||||
// TODO: handle possible null value
|
||||
let position = node.getStage()?.getPointerPosition();
|
||||
if (!position) {
|
||||
// TODO: handle possible null value
|
||||
return;
|
||||
}
|
||||
return transform.point(position);
|
||||
}
|
||||
|
||||
export function getRelativePointerPositionNormalized(node) {
|
||||
export function getRelativePointerPositionNormalized(node: Konva.Node): { x: number, y: number } | undefined {
|
||||
const relativePosition = getRelativePointerPosition(node);
|
||||
if (!relativePosition) {
|
||||
// TODO: handle possible null value
|
||||
return;
|
||||
}
|
||||
return {
|
||||
x: relativePosition.x / node.width(),
|
||||
y: relativePosition.y / node.height(),
|
||||
@ -317,8 +331,8 @@ export function getRelativePointerPositionNormalized(node) {
|
||||
* @param {number[]} points points in an x, y alternating array
|
||||
* @returns {Vector2[]} a `Vector2` array
|
||||
*/
|
||||
export function convertPointArray(points) {
|
||||
return points.reduce((acc, _, i, arr) => {
|
||||
export function convertPointArray(points: number[]) {
|
||||
return points.reduce((acc: any[], _, i, arr) => {
|
||||
if (i % 2 === 0) {
|
||||
acc.push({ x: arr[i], y: arr[i + 1] });
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { captureException } from "@sentry/react";
|
||||
|
||||
export function logError(error) {
|
||||
export function logError(error: any): void {
|
||||
console.error(error);
|
||||
if (process.env.REACT_APP_LOGGING === "true") {
|
||||
captureException(error);
|
@ -1,4 +1,4 @@
|
||||
const monsters = [
|
||||
const monsters: string[] = [
|
||||
"Aboleth",
|
||||
"Acolyte",
|
||||
"Black Dragon",
|
||||
@ -295,6 +295,6 @@ const monsters = [
|
||||
|
||||
export default monsters;
|
||||
|
||||
export function getRandomMonster() {
|
||||
export function getRandomMonster(): string {
|
||||
return monsters[Math.floor(Math.random() * monsters.length)];
|
||||
}
|
@ -8,10 +8,20 @@ import { groupBy } from "./shared";
|
||||
*/
|
||||
|
||||
// Helper for generating search results for items
|
||||
export function useSearch(items, search) {
|
||||
const [filteredItems, setFilteredItems] = useState([]);
|
||||
const [filteredItemScores, setFilteredItemScores] = useState({});
|
||||
const [fuse, setFuse] = useState();
|
||||
export function useSearch(items: [], search: string) {
|
||||
// TODO: add types to search items -> don't like the never type
|
||||
const [filteredItems, setFilteredItems]: [
|
||||
filteredItems: any,
|
||||
setFilteredItems: any
|
||||
] = useState([]);
|
||||
const [filteredItemScores, setFilteredItemScores]: [
|
||||
filteredItemScores: {},
|
||||
setFilteredItemScores: React.Dispatch<React.SetStateAction<{}>>
|
||||
] = useState({});
|
||||
const [fuse, setFuse]: [
|
||||
fuse: Fuse<never> | undefined,
|
||||
setFuse: React.Dispatch<Fuse<never> | undefined>
|
||||
] = useState();
|
||||
|
||||
// Update search index when items change
|
||||
useEffect(() => {
|
||||
@ -21,14 +31,15 @@ export function useSearch(items, search) {
|
||||
// Perform search when search changes
|
||||
useEffect(() => {
|
||||
if (search) {
|
||||
const query = fuse.search(search);
|
||||
setFilteredItems(query.map((result) => result.item));
|
||||
setFilteredItemScores(
|
||||
query.reduce(
|
||||
(acc, value) => ({ ...acc, [value.item.id]: value.score }),
|
||||
{}
|
||||
)
|
||||
const query = fuse?.search(search);
|
||||
setFilteredItems(query?.map((result: any) => result.item));
|
||||
let reduceResult: {} | undefined = query?.reduce(
|
||||
(acc: {}, value: any) => ({ ...acc, [value.item.id]: value.score }),
|
||||
{}
|
||||
);
|
||||
if (reduceResult) {
|
||||
setFilteredItemScores(reduceResult);
|
||||
}
|
||||
}
|
||||
}, [search, items, fuse]);
|
||||
|
||||
@ -36,7 +47,12 @@ export function useSearch(items, search) {
|
||||
}
|
||||
|
||||
// Helper for grouping items
|
||||
export function useGroup(items, filteredItems, useFiltered, filteredScores) {
|
||||
export function useGroup(
|
||||
items: any[],
|
||||
filteredItems: any[],
|
||||
useFiltered: boolean,
|
||||
filteredScores: any[]
|
||||
) {
|
||||
const itemsByGroup = groupBy(useFiltered ? filteredItems : items, "group");
|
||||
// Get the groups of the items sorting by the average score if we're filtering or the alphabetical order
|
||||
// with "" at the start and "default" at the end if not
|
||||
@ -44,10 +60,10 @@ export function useGroup(items, filteredItems, useFiltered, filteredScores) {
|
||||
if (useFiltered) {
|
||||
itemGroups.sort((a, b) => {
|
||||
const aScore = itemsByGroup[a].reduce(
|
||||
(acc, item) => (acc + filteredScores[item.id]) / 2
|
||||
(acc: any, item: any) => (acc + filteredScores[item.id]) / 2
|
||||
);
|
||||
const bScore = itemsByGroup[b].reduce(
|
||||
(acc, item) => (acc + filteredScores[item.id]) / 2
|
||||
(acc: any, item: any) => (acc + filteredScores[item.id]) / 2
|
||||
);
|
||||
return aScore - bScore;
|
||||
});
|
||||
@ -67,12 +83,12 @@ export function useGroup(items, filteredItems, useFiltered, filteredScores) {
|
||||
|
||||
// Helper for handling selecting items
|
||||
export function handleItemSelect(
|
||||
item,
|
||||
selectMode,
|
||||
selectedIds,
|
||||
setSelectedIds,
|
||||
itemsByGroup,
|
||||
itemGroups
|
||||
item: any,
|
||||
selectMode: any,
|
||||
selectedIds: number[],
|
||||
setSelectedIds: any,
|
||||
itemsByGroup: any,
|
||||
itemGroups: any
|
||||
) {
|
||||
if (!item) {
|
||||
setSelectedIds([]);
|
||||
@ -83,9 +99,9 @@ export function handleItemSelect(
|
||||
setSelectedIds([item.id]);
|
||||
break;
|
||||
case "multiple":
|
||||
setSelectedIds((prev) => {
|
||||
setSelectedIds((prev: any[]) => {
|
||||
if (prev.includes(item.id)) {
|
||||
return prev.filter((id) => id !== item.id);
|
||||
return prev.filter((id: number) => id !== item.id);
|
||||
} else {
|
||||
return [...prev, item.id];
|
||||
}
|
||||
@ -94,32 +110,32 @@ export function handleItemSelect(
|
||||
case "range":
|
||||
// Create items array
|
||||
let items = itemGroups.reduce(
|
||||
(acc, group) => [...acc, ...itemsByGroup[group]],
|
||||
(acc: [], group: any) => [...acc, ...itemsByGroup[group]],
|
||||
[]
|
||||
);
|
||||
|
||||
// Add all items inbetween the previous selected item and the current selected
|
||||
if (selectedIds.length > 0) {
|
||||
const mapIndex = items.findIndex((m) => m.id === item.id);
|
||||
const mapIndex = items.findIndex((m: any) => m.id === item.id);
|
||||
const lastIndex = items.findIndex(
|
||||
(m) => m.id === selectedIds[selectedIds.length - 1]
|
||||
(m: any) => m.id === selectedIds[selectedIds.length - 1]
|
||||
);
|
||||
let idsToAdd = [];
|
||||
let idsToRemove = [];
|
||||
let idsToAdd: number[] = [];
|
||||
let idsToRemove: number[] = [];
|
||||
const direction = mapIndex > lastIndex ? 1 : -1;
|
||||
for (
|
||||
let i = lastIndex + direction;
|
||||
direction < 0 ? i >= mapIndex : i <= mapIndex;
|
||||
i += direction
|
||||
) {
|
||||
const itemId = items[i].id;
|
||||
const itemId: number = items[i].id;
|
||||
if (selectedIds.includes(itemId)) {
|
||||
idsToRemove.push(itemId);
|
||||
} else {
|
||||
idsToAdd.push(itemId);
|
||||
}
|
||||
}
|
||||
setSelectedIds((prev) => {
|
||||
setSelectedIds((prev: any[]) => {
|
||||
let ids = [...prev, ...idsToAdd];
|
||||
return ids.filter((id) => !idsToRemove.includes(id));
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
export function omit(obj, keys) {
|
||||
let tmp = {};
|
||||
export function omit(obj:object, keys: string[]) {
|
||||
let tmp: { [key: string]: any } = {};
|
||||
for (let [key, value] of Object.entries(obj)) {
|
||||
if (keys.includes(key)) {
|
||||
continue;
|
||||
@ -9,7 +9,7 @@ export function omit(obj, keys) {
|
||||
return tmp;
|
||||
}
|
||||
|
||||
export function fromEntries(iterable) {
|
||||
export function fromEntries(iterable: any) {
|
||||
if (Object.fromEntries) {
|
||||
return Object.fromEntries(iterable);
|
||||
}
|
||||
@ -20,32 +20,32 @@ export function fromEntries(iterable) {
|
||||
}
|
||||
|
||||
// Check to see if all tracks are muted
|
||||
export function isStreamStopped(stream) {
|
||||
return stream.getTracks().reduce((a, b) => a && b, { mute: true });
|
||||
export function isStreamStopped(stream: any) {
|
||||
return stream.getTracks().reduce((a: any, b: any) => a && b, { mute: true });
|
||||
}
|
||||
|
||||
export function roundTo(x, to) {
|
||||
export function roundTo(x: number, to: number): number {
|
||||
return Math.round(x / to) * to;
|
||||
}
|
||||
|
||||
export function floorTo(x, to) {
|
||||
export function floorTo(x: number, to: number): number {
|
||||
return Math.floor(x / to) * to;
|
||||
}
|
||||
|
||||
export function toRadians(angle) {
|
||||
export function toRadians(angle: number): number {
|
||||
return angle * (Math.PI / 180);
|
||||
}
|
||||
|
||||
export function toDegrees(angle) {
|
||||
export function toDegrees(angle: number): number {
|
||||
return angle * (180 / Math.PI);
|
||||
}
|
||||
|
||||
export function lerp(a, b, alpha) {
|
||||
export function lerp(a: number, b: number, alpha: number): number {
|
||||
return a * (1 - alpha) + b * alpha;
|
||||
}
|
||||
|
||||
// Console log an image
|
||||
export function logImage(url, width, height) {
|
||||
export function logImage(url: string, width: number, height: number): void {
|
||||
const style = [
|
||||
"font-size: 1px;",
|
||||
`padding: ${height}px ${width}px;`,
|
||||
@ -55,19 +55,19 @@ export function logImage(url, width, height) {
|
||||
console.log("%c ", style);
|
||||
}
|
||||
|
||||
export function isEmpty(obj) {
|
||||
export function isEmpty(obj: any): boolean {
|
||||
return Object.keys(obj).length === 0 && obj.constructor === Object;
|
||||
}
|
||||
|
||||
export function keyBy(array, key) {
|
||||
export function keyBy(array: any, key: any) {
|
||||
return array.reduce(
|
||||
(prev, current) => ({ ...prev, [key ? current[key] : current]: current }),
|
||||
(prev: any, current: any) => ({ ...prev, [key ? current[key] : current]: current }),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export function groupBy(array, key) {
|
||||
return array.reduce((prev, current) => {
|
||||
export function groupBy(array: any, key: string) {
|
||||
return array.reduce((prev: any, current: any) => {
|
||||
const k = current[key];
|
||||
(prev[k] || (prev[k] = [])).push(current);
|
||||
return prev;
|
@ -3,10 +3,22 @@ const MILLISECONDS_IN_MINUTE = 60000;
|
||||
const MILLISECONDS_IN_SECOND = 1000;
|
||||
|
||||
/**
|
||||
* Returns a timers duration in milliseconds
|
||||
* @param {Object} t The object with an hour, minute and second property
|
||||
* @typedef Time
|
||||
* @property {number} hour
|
||||
* @property {number} minute
|
||||
* @property {number} second
|
||||
*/
|
||||
export function getHMSDuration(t) {
|
||||
type Time = {
|
||||
hour: number,
|
||||
minute: number,
|
||||
second: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a timers duration in milliseconds
|
||||
* @param {Time} t The object with an hour, minute and second property
|
||||
*/
|
||||
export function getHMSDuration(t: Time) {
|
||||
if (!t) {
|
||||
return 0;
|
||||
}
|
||||
@ -21,7 +33,7 @@ export function getHMSDuration(t) {
|
||||
* Returns an object with an hour, minute and second property
|
||||
* @param {number} duration The duration in milliseconds
|
||||
*/
|
||||
export function getDurationHMS(duration) {
|
||||
export function getDurationHMS(duration: number) {
|
||||
let workingDuration = duration;
|
||||
const hour = Math.floor(workingDuration / MILLISECONDS_IN_HOUR);
|
||||
workingDuration -= hour * MILLISECONDS_IN_HOUR;
|
@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import App from "./App";
|
@ -9,23 +9,26 @@ import blobToBuffer from "../helpers/blobToBuffer";
|
||||
const MAX_BUFFER_SIZE = 16000;
|
||||
|
||||
class Connection extends SimplePeer {
|
||||
constructor(props) {
|
||||
currentChunks: any;
|
||||
dataChannels: any;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.currentChunks = {};
|
||||
this.currentChunks = {} as Blob;
|
||||
this.dataChannels = {};
|
||||
this.on("data", this.handleData);
|
||||
this.on("datachannel", this.handleDataChannel);
|
||||
}
|
||||
|
||||
// Intercept the data event with decoding and chunking support
|
||||
handleData(packed) {
|
||||
const unpacked = decode(packed);
|
||||
handleData(packed: any) {
|
||||
const unpacked: any = decode(packed);
|
||||
// If the special property __chunked is set and true
|
||||
// The data is a partial chunk of the a larger file
|
||||
// So wait until all chunks are collected and assembled
|
||||
// before emitting the dataComplete event
|
||||
if (unpacked.__chunked) {
|
||||
let chunk = this.currentChunks[unpacked.id] || {
|
||||
let chunk: any = this.currentChunks[unpacked.id] || {
|
||||
data: [],
|
||||
count: 0,
|
||||
total: unpacked.total,
|
||||
@ -57,7 +60,7 @@ class Connection extends SimplePeer {
|
||||
|
||||
// Custom send function with encoding, chunking and data channel support
|
||||
// Uses `write` to send the data to allow for buffer / backpressure handling
|
||||
sendObject(object, channel) {
|
||||
sendObject(object: any, channel: any) {
|
||||
try {
|
||||
const packedData = encode(object);
|
||||
if (packedData.byteLength > MAX_BUFFER_SIZE) {
|
||||
@ -84,23 +87,25 @@ class Connection extends SimplePeer {
|
||||
|
||||
// Override the create data channel function to store our own named reference to it
|
||||
// and to use our custom data handler
|
||||
createDataChannel(channelName, channelConfig, opts) {
|
||||
createDataChannel(channelName: string, channelConfig: any, opts: any) {
|
||||
// TODO: resolve createDataChannel
|
||||
// @ts-ignore
|
||||
const channel = super.createDataChannel(channelName, channelConfig, opts);
|
||||
this.handleDataChannel(channel);
|
||||
return channel;
|
||||
}
|
||||
|
||||
handleDataChannel(channel) {
|
||||
handleDataChannel(channel: any) {
|
||||
const channelName = channel.channelName;
|
||||
this.dataChannels[channelName] = channel;
|
||||
channel.on("data", this.handleData.bind(this));
|
||||
channel.on("error", (error) => {
|
||||
channel.on("error", (error: any) => {
|
||||
this.emit("error", error);
|
||||
});
|
||||
}
|
||||
|
||||
// Converted from https://github.com/peers/peerjs/
|
||||
chunk(data) {
|
||||
chunk(data: any) {
|
||||
const chunks = [];
|
||||
const size = data.byteLength;
|
||||
const total = Math.ceil(size / MAX_BUFFER_SIZE);
|
@ -1,4 +1,4 @@
|
||||
import io from "socket.io-client";
|
||||
import io, { Socket } from "socket.io-client";
|
||||
import msgParser from "socket.io-msgpack-parser";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
@ -6,6 +6,7 @@ import Connection from "./Connection";
|
||||
|
||||
import { omit } from "../helpers/shared";
|
||||
import { logError } from "../helpers/logging";
|
||||
import { SimplePeerData } from "simple-peer";
|
||||
|
||||
/**
|
||||
* @typedef {object} SessionPeer
|
||||
@ -14,6 +15,12 @@ import { logError } from "../helpers/logging";
|
||||
* @property {boolean} initiator - Is this peer the initiator of the connection
|
||||
* @property {boolean} ready - Ready for data to be sent
|
||||
*/
|
||||
type SessionPeer = {
|
||||
id: string;
|
||||
connection: Connection;
|
||||
initiator: boolean;
|
||||
ready: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @callback peerReply
|
||||
@ -22,6 +29,8 @@ import { logError } from "../helpers/logging";
|
||||
* @param {string} channel - The channel to send to
|
||||
*/
|
||||
|
||||
type peerReply = (id: string, data: SimplePeerData, channel: string) => void;
|
||||
|
||||
/**
|
||||
* Session Status Event - Status of the session has changed
|
||||
*
|
||||
@ -50,24 +59,24 @@ class Session extends EventEmitter {
|
||||
*
|
||||
* @type {io.Socket}
|
||||
*/
|
||||
socket;
|
||||
socket: Socket = io();
|
||||
|
||||
/**
|
||||
* A mapping of socket ids to session peers
|
||||
*
|
||||
* @type {Object.<string, SessionPeer>}
|
||||
*/
|
||||
peers;
|
||||
peers: Record<string, SessionPeer>;
|
||||
|
||||
get id() {
|
||||
return this.socket && this.socket.id;
|
||||
}
|
||||
|
||||
_iceServers;
|
||||
_iceServers: string[] = [];
|
||||
|
||||
// Store party id and password for reconnect
|
||||
_gameId;
|
||||
_password;
|
||||
_gameId: string = "";
|
||||
_password: string = "";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -81,6 +90,9 @@ class Session extends EventEmitter {
|
||||
*/
|
||||
async connect() {
|
||||
try {
|
||||
if (!process.env.REACT_APP_ICE_SERVERS_URL) {
|
||||
return;
|
||||
}
|
||||
const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL);
|
||||
if (!response.ok) {
|
||||
throw Error("Unable to fetch ICE servers");
|
||||
@ -88,6 +100,9 @@ class Session extends EventEmitter {
|
||||
const data = await response.json();
|
||||
this._iceServers = data.iceServers;
|
||||
|
||||
if (!process.env.REACT_APP_BROKER_URL) {
|
||||
return;
|
||||
}
|
||||
this.socket = io(process.env.REACT_APP_BROKER_URL, {
|
||||
withCredentials: true,
|
||||
parser: msgParser,
|
||||
@ -122,7 +137,7 @@ class Session extends EventEmitter {
|
||||
* @param {object} data
|
||||
* @param {string} channel
|
||||
*/
|
||||
sendTo(sessionId, eventId, data, channel) {
|
||||
sendTo(sessionId: string, eventId: string, data: SimplePeerData, channel: string) {
|
||||
if (!(sessionId in this.peers)) {
|
||||
if (!this._addPeer(sessionId, true)) {
|
||||
return;
|
||||
@ -151,7 +166,11 @@ class Session extends EventEmitter {
|
||||
* @param {MediaStreamTrack} track
|
||||
* @param {MediaStream} stream
|
||||
*/
|
||||
startStreamTo(sessionId, track, stream) {
|
||||
startStreamTo(
|
||||
sessionId: string,
|
||||
track: MediaStreamTrack,
|
||||
stream: MediaStream
|
||||
) {
|
||||
if (!(sessionId in this.peers)) {
|
||||
if (!this._addPeer(sessionId, true)) {
|
||||
return;
|
||||
@ -174,7 +193,7 @@ class Session extends EventEmitter {
|
||||
* @param {MediaStreamTrack} track
|
||||
* @param {MediaStream} stream
|
||||
*/
|
||||
endStreamTo(sessionId, track, stream) {
|
||||
endStreamTo(sessionId: string, track: MediaStreamTrack, stream: MediaStream) {
|
||||
if (sessionId in this.peers) {
|
||||
this.peers[sessionId].connection.removeTrack(track, stream);
|
||||
}
|
||||
@ -186,7 +205,7 @@ class Session extends EventEmitter {
|
||||
* @param {string} gameId - the id of the party to join
|
||||
* @param {string} password - the password of the party
|
||||
*/
|
||||
async joinGame(gameId, password) {
|
||||
async joinGame(gameId: string, password: string) {
|
||||
if (typeof gameId !== "string" || typeof password !== "string") {
|
||||
console.error(
|
||||
"Unable to join game: invalid game ID or password",
|
||||
@ -198,7 +217,12 @@ class Session extends EventEmitter {
|
||||
|
||||
this._gameId = gameId;
|
||||
this._password = password;
|
||||
this.socket.emit("join_game", gameId, password, process.env.REACT_APP_VERSION);
|
||||
this.socket.emit(
|
||||
"join_game",
|
||||
gameId,
|
||||
password,
|
||||
process.env.REACT_APP_VERSION
|
||||
);
|
||||
this.emit("status", "joining");
|
||||
}
|
||||
|
||||
@ -208,7 +232,7 @@ class Session extends EventEmitter {
|
||||
* @param {boolean} initiator
|
||||
* @returns {boolean} True if peer was added successfully
|
||||
*/
|
||||
_addPeer(id, initiator) {
|
||||
_addPeer(id: string, initiator: boolean) {
|
||||
try {
|
||||
const connection = new Connection({
|
||||
initiator,
|
||||
@ -221,15 +245,15 @@ class Session extends EventEmitter {
|
||||
|
||||
const peer = { id, connection, initiator, ready: false };
|
||||
|
||||
function sendPeer(id, data, channel) {
|
||||
const sendPeer = (id: string, data: SimplePeerData, channel: any) => {
|
||||
peer.connection.sendObject({ id, data }, channel);
|
||||
}
|
||||
};
|
||||
|
||||
function handleSignal(signal) {
|
||||
const handleSignal = (signal: any) => {
|
||||
this.socket.emit("signal", JSON.stringify({ to: peer.id, signal }));
|
||||
}
|
||||
};
|
||||
|
||||
function handleConnect() {
|
||||
const handleConnect = () => {
|
||||
if (peer.id in this.peers) {
|
||||
this.peers[peer.id].ready = true;
|
||||
}
|
||||
@ -241,10 +265,14 @@ class Session extends EventEmitter {
|
||||
* @property {SessionPeer} peer
|
||||
* @property {peerReply} reply
|
||||
*/
|
||||
this.emit("peerConnect", { peer, reply: sendPeer });
|
||||
}
|
||||
const peerConnectEvent: { peer: SessionPeer; reply: peerReply } = {
|
||||
peer,
|
||||
reply: sendPeer,
|
||||
};
|
||||
this.emit("peerConnect", peerConnectEvent);
|
||||
};
|
||||
|
||||
function handleDataComplete(data) {
|
||||
const handleDataComplete = (data: any) => {
|
||||
/**
|
||||
* Peer Data Event - Data received by a peer
|
||||
*
|
||||
@ -255,15 +283,30 @@ class Session extends EventEmitter {
|
||||
* @property {object} data
|
||||
* @property {peerReply} reply
|
||||
*/
|
||||
this.emit("peerData", {
|
||||
let peerDataEvent: {
|
||||
peer: SessionPeer;
|
||||
id: string;
|
||||
data: any;
|
||||
reply: peerReply;
|
||||
} = {
|
||||
peer,
|
||||
id: data.id,
|
||||
data: data.data,
|
||||
reply: sendPeer,
|
||||
});
|
||||
}
|
||||
};
|
||||
console.log(`Data: ${JSON.stringify(data)}`)
|
||||
this.emit("peerData", peerDataEvent);
|
||||
};
|
||||
|
||||
function handleDataProgress({ id, count, total }) {
|
||||
const handleDataProgress = ({
|
||||
id,
|
||||
count,
|
||||
total,
|
||||
}: {
|
||||
id: string;
|
||||
count: number;
|
||||
total: number;
|
||||
}) => {
|
||||
this.emit("peerDataProgress", {
|
||||
peer,
|
||||
id,
|
||||
@ -271,9 +314,9 @@ class Session extends EventEmitter {
|
||||
total,
|
||||
reply: sendPeer,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function handleTrack(track, stream) {
|
||||
const handleTrack = (track: MediaStreamTrack, stream: MediaStream) => {
|
||||
/**
|
||||
* Peer Track Added Event - A `MediaStreamTrack` was added by a peer
|
||||
*
|
||||
@ -283,7 +326,12 @@ class Session extends EventEmitter {
|
||||
* @property {MediaStreamTrack} track
|
||||
* @property {MediaStream} stream
|
||||
*/
|
||||
this.emit("peerTrackAdded", { peer, track, stream });
|
||||
let peerTrackAddedEvent: {
|
||||
peer: SessionPeer;
|
||||
track: MediaStreamTrack;
|
||||
stream: MediaStream;
|
||||
} = { peer, track, stream };
|
||||
this.emit("peerTrackAdded", peerTrackAddedEvent);
|
||||
track.addEventListener("mute", () => {
|
||||
/**
|
||||
* Peer Track Removed Event - A `MediaStreamTrack` was removed by a peer
|
||||
@ -294,11 +342,16 @@ class Session extends EventEmitter {
|
||||
* @property {MediaStreamTrack} track
|
||||
* @property {MediaStream} stream
|
||||
*/
|
||||
this.emit("peerTrackRemoved", { peer, track, stream });
|
||||
let peerTrackRemovedEvent: {
|
||||
peer: SessionPeer;
|
||||
track: MediaStreamTrack;
|
||||
stream: MediaStream;
|
||||
} = { peer, track, stream };
|
||||
this.emit("peerTrackRemoved", peerTrackRemovedEvent);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function handleClose() {
|
||||
const handleClose = () => {
|
||||
/**
|
||||
* Peer Disconnect Event - A peer has disconnected
|
||||
*
|
||||
@ -306,14 +359,15 @@ class Session extends EventEmitter {
|
||||
* @type {object}
|
||||
* @property {SessionPeer} peer
|
||||
*/
|
||||
this.emit("peerDisconnect", { peer });
|
||||
let peerDisconnectEvent: { peer: SessionPeer } = { peer };
|
||||
this.emit("peerDisconnect", peerDisconnectEvent);
|
||||
if (peer.id in this.peers) {
|
||||
peer.connection.destroy();
|
||||
this.peers = omit(this.peers, [peer.id]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function handleError(error) {
|
||||
const handleError = (error: Error) => {
|
||||
/**
|
||||
* Peer Error Event - An error occured with a peer connection
|
||||
*
|
||||
@ -322,12 +376,16 @@ class Session extends EventEmitter {
|
||||
* @property {SessionPeer} peer
|
||||
* @property {Error} error
|
||||
*/
|
||||
this.emit("peerError", { peer, error });
|
||||
let peerErrorEvent: { peer: SessionPeer; error: Error } = {
|
||||
peer,
|
||||
error,
|
||||
};
|
||||
this.emit("peerError", peerErrorEvent);
|
||||
if (peer.id in this.peers) {
|
||||
peer.connection.destroy();
|
||||
this.peers = omit(this.peers, [peer.id]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
peer.connection.on("signal", handleSignal.bind(this));
|
||||
peer.connection.on("connect", handleConnect.bind(this));
|
||||
@ -363,7 +421,7 @@ class Session extends EventEmitter {
|
||||
this.emit("gameExpired");
|
||||
}
|
||||
|
||||
_handlePlayerJoined(id) {
|
||||
_handlePlayerJoined(id: string) {
|
||||
/**
|
||||
* Player Joined Event - A player has joined the game
|
||||
*
|
||||
@ -373,7 +431,7 @@ class Session extends EventEmitter {
|
||||
this.emit("playerJoined", id);
|
||||
}
|
||||
|
||||
_handlePlayerLeft(id) {
|
||||
_handlePlayerLeft(id: string) {
|
||||
/**
|
||||
* Player Left Event - A player has left the game
|
||||
*
|
||||
@ -387,7 +445,7 @@ class Session extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_handleSignal(data) {
|
||||
_handleSignal(data: any) {
|
||||
const { from, signal } = data;
|
||||
if (!(from in this.peers)) {
|
||||
if (!this._addPeer(from, false)) {
|
1
src/react-app-env.d.ts
vendored
Normal file
1
src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
@ -20,9 +20,13 @@ const isLocalhost = Boolean(
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
export function register(config: any) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
if (!process.env.PUBLIC_URL) {
|
||||
// TODO: handle is PUBLIC_URL has not been set
|
||||
return;
|
||||
}
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
@ -54,9 +58,9 @@ export function register(config) {
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
function registerValidSW(swUrl: string | URL, config: any) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.register(swUrl as string)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
@ -98,7 +102,8 @@ function registerValidSW(swUrl, config) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// TODO: handle swUrl -> type has to be handled as RequestInfo OR string | URL
|
||||
function checkValidServiceWorker(swUrl: any, config: any) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' }
|
@ -1,6 +1,6 @@
|
||||
import Settings from "./helpers/Settings";
|
||||
|
||||
function loadVersions(settings) {
|
||||
function loadVersions(settings: Settings) {
|
||||
settings.version(1, () => ({
|
||||
fog: {
|
||||
type: "polygon",
|
||||
@ -28,17 +28,17 @@ function loadVersions(settings) {
|
||||
},
|
||||
}));
|
||||
// v1.5.2 - Added full screen support for map and label size
|
||||
settings.version(2, (prev) => ({
|
||||
settings.version(2, (prev: any) => ({
|
||||
...prev,
|
||||
map: { fullScreen: false, labelSize: 1 },
|
||||
}));
|
||||
// v1.7.0 - Added game password
|
||||
settings.version(3, (prev) => ({
|
||||
settings.version(3, (prev: any) => ({
|
||||
...prev,
|
||||
game: { usePassword: true },
|
||||
}));
|
||||
// v1.8.0 - Added pointer color, grid snapping sensitivity and remove measure
|
||||
settings.version(4, (prev) => {
|
||||
settings.version(4, (prev: any) => {
|
||||
let newSettings = {
|
||||
...prev,
|
||||
pointer: { color: "red" },
|
||||
@ -48,19 +48,19 @@ function loadVersions(settings) {
|
||||
return newSettings;
|
||||
});
|
||||
// v1.8.0 - Removed edge snapping for multilayer
|
||||
settings.version(5, (prev) => {
|
||||
settings.version(5, (prev: any) => {
|
||||
let newSettings = { ...prev };
|
||||
delete newSettings.fog.useEdgeSnapping;
|
||||
newSettings.fog.multilayer = false;
|
||||
return newSettings;
|
||||
});
|
||||
// v1.8.1 - Add show guides toggle
|
||||
settings.version(6, (prev) => ({
|
||||
settings.version(6, (prev: any) => ({
|
||||
...prev,
|
||||
fog: { ...prev.fog, showGuides: true },
|
||||
}));
|
||||
// v1.8.1 - Add fog edit opacity
|
||||
settings.version(7, (prev) => ({
|
||||
settings.version(7, (prev: any) => ({
|
||||
...prev,
|
||||
fog: { ...prev.fog, editOpacity: 0.5 },
|
||||
}));
|
110
src/shortcuts.js
110
src/shortcuts.js
@ -1,110 +0,0 @@
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasModifier(event) {
|
||||
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key press without any modifiers and ignoring capitals
|
||||
* @param {KeyboardEvent} event
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function singleKey(event, key) {
|
||||
return (
|
||||
!hasModifier(event) &&
|
||||
(event.key === key || event.key === key.toUpperCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Keyboard} event
|
||||
*/
|
||||
function undo(event) {
|
||||
const { key, ctrlKey, metaKey, shiftKey } = event;
|
||||
return (key === "z" || key === "Z") && (ctrlKey || metaKey) && !shiftKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Keyboard} event
|
||||
*/
|
||||
function redo(event) {
|
||||
const { key, ctrlKey, metaKey, shiftKey } = event;
|
||||
return (key === "z" || key === "Z") && (ctrlKey || metaKey) && shiftKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Keyboard} event
|
||||
*/
|
||||
function zoomIn(event) {
|
||||
const { key, ctrlKey, metaKey } = event;
|
||||
return (key === "=" || key === "+") && !ctrlKey && !metaKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Keyboard} event
|
||||
*/
|
||||
function zoomOut(event) {
|
||||
const { key, ctrlKey, metaKey } = event;
|
||||
return (key === "-" || key === "_") && !ctrlKey && !metaKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback shortcut
|
||||
* @param {KeyboardEvent} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Object.<string, shortcut>}
|
||||
*/
|
||||
const shortcuts = {
|
||||
// Tools
|
||||
move: (event) => singleKey(event, " "),
|
||||
moveTool: (event) => singleKey(event, "w"),
|
||||
drawingTool: (event) => singleKey(event, "d"),
|
||||
fogTool: (event) => singleKey(event, "f"),
|
||||
measureTool: (event) => singleKey(event, "m"),
|
||||
pointerTool: (event) => singleKey(event, "q"),
|
||||
noteTool: (event) => singleKey(event, "n"),
|
||||
// Map editor
|
||||
gridNudgeUp: ({ key }) => key === "ArrowUp",
|
||||
gridNudgeLeft: ({ key }) => key === "ArrowLeft",
|
||||
gridNudgeRight: ({ key }) => key === "ArrowRight",
|
||||
gridNudgeDown: ({ key }) => key === "ArrowDown",
|
||||
// Drawing tool
|
||||
drawBrush: (event) => singleKey(event, "b"),
|
||||
drawPaint: (event) => singleKey(event, "p"),
|
||||
drawLine: (event) => singleKey(event, "l"),
|
||||
drawRect: (event) => singleKey(event, "r"),
|
||||
drawCircle: (event) => singleKey(event, "c"),
|
||||
drawTriangle: (event) => singleKey(event, "t"),
|
||||
drawErase: (event) => singleKey(event, "e"),
|
||||
drawBlend: (event) => singleKey(event, "o"),
|
||||
// Fog tool
|
||||
fogPolygon: (event) => singleKey(event, "p"),
|
||||
fogRectangle: (event) => singleKey(event, "r"),
|
||||
fogBrush: (event) => singleKey(event, "b"),
|
||||
fogToggle: (event) => singleKey(event, "t"),
|
||||
fogErase: (event) => singleKey(event, "e"),
|
||||
fogLayer: (event) => singleKey(event, "l"),
|
||||
fogPreview: (event) => singleKey(event, "f"),
|
||||
fogCut: (event) => singleKey(event, "c"),
|
||||
fogFinishPolygon: ({ key }) => key === "Enter",
|
||||
fogCancelPolygon: ({ key }) => key === "Escape",
|
||||
// Stage interaction
|
||||
stageZoomIn: zoomIn,
|
||||
stageZoomOut: zoomOut,
|
||||
stagePrecisionZoom: ({ key }) => key === "Shift",
|
||||
// Select
|
||||
selectRange: ({ key }) => key === "Shift",
|
||||
selectMultiple: ({ key }) => key === "Control" || key === "Meta",
|
||||
// Common
|
||||
undo,
|
||||
redo,
|
||||
delete: ({ key }) => key === "Backspace" || key === "Delete",
|
||||
};
|
||||
|
||||
export default shortcuts;
|
114
src/shortcuts.ts
Normal file
114
src/shortcuts.ts
Normal file
@ -0,0 +1,114 @@
|
||||
/**
|
||||
* @param {KeyboardEvent} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasModifier(event: KeyboardEvent): boolean {
|
||||
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key press without any modifiers and ignoring capitals
|
||||
* @param {KeyboardEvent} event
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function singleKey(event: KeyboardEvent, key: string): boolean {
|
||||
return (
|
||||
!hasModifier(event) &&
|
||||
(event.key === key || event.key === key.toUpperCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Keyboard} event
|
||||
* @returns {string | boolean}
|
||||
*/
|
||||
function undo(event: KeyboardEvent): string | boolean {
|
||||
const { key, ctrlKey, metaKey, shiftKey } = event;
|
||||
return (key === "z" || key === "Z") && (ctrlKey || metaKey) && !shiftKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Keyboard} event
|
||||
* @returns {string | boolean}
|
||||
*/
|
||||
function redo(event: KeyboardEvent): string | boolean {
|
||||
const { key, ctrlKey, metaKey, shiftKey } = event;
|
||||
return (key === "z" || key === "Z") && (ctrlKey || metaKey) && shiftKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Keyboard} event
|
||||
* @returns {string | boolean}
|
||||
*/
|
||||
function zoomIn(event: KeyboardEvent): string | boolean {
|
||||
const { key, ctrlKey, metaKey } = event;
|
||||
return (key === "=" || key === "+") && !ctrlKey && !metaKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Keyboard} event
|
||||
* @returns {string | boolean}
|
||||
*/
|
||||
function zoomOut(event: KeyboardEvent): string | boolean {
|
||||
const { key, ctrlKey, metaKey } = event;
|
||||
return (key === "-" || key === "_") && !ctrlKey && !metaKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @callback shortcut
|
||||
* @param {KeyboardEvent} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Object.<string, shortcut>}
|
||||
*/
|
||||
const shortcuts = {
|
||||
// Tools
|
||||
move: (event: KeyboardEvent) => singleKey(event, " "),
|
||||
moveTool: (event: KeyboardEvent) => singleKey(event, "w"),
|
||||
drawingTool: (event: KeyboardEvent) => singleKey(event, "d"),
|
||||
fogTool: (event: KeyboardEvent) => singleKey(event, "f"),
|
||||
measureTool: (event: KeyboardEvent) => singleKey(event, "m"),
|
||||
pointerTool: (event: KeyboardEvent) => singleKey(event, "q"),
|
||||
noteTool: (event: KeyboardEvent) => singleKey(event, "n"),
|
||||
// Map editor
|
||||
gridNudgeUp: ({ key }: { key: string}) => key === "ArrowUp",
|
||||
gridNudgeLeft: ({ key }: { key: string }) => key === "ArrowLeft",
|
||||
gridNudgeRight: ({ key }: { key: string }) => key === "ArrowRight",
|
||||
gridNudgeDown: ({ key }: { key: string }) => key === "ArrowDown",
|
||||
// Drawing tool
|
||||
drawBrush: (event: KeyboardEvent) => singleKey(event, "b"),
|
||||
drawPaint: (event: KeyboardEvent) => singleKey(event, "p"),
|
||||
drawLine: (event: KeyboardEvent) => singleKey(event, "l"),
|
||||
drawRect: (event: KeyboardEvent) => singleKey(event, "r"),
|
||||
drawCircle: (event: KeyboardEvent) => singleKey(event, "c"),
|
||||
drawTriangle: (event: KeyboardEvent) => singleKey(event, "t"),
|
||||
drawErase: (event: KeyboardEvent) => singleKey(event, "e"),
|
||||
drawBlend: (event: KeyboardEvent) => singleKey(event, "o"),
|
||||
// Fog tool
|
||||
fogPolygon: (event: KeyboardEvent) => singleKey(event, "p"),
|
||||
fogRectangle: (event: KeyboardEvent) => singleKey(event, "r"),
|
||||
fogBrush: (event: KeyboardEvent) => singleKey(event, "b"),
|
||||
fogToggle: (event: KeyboardEvent) => singleKey(event, "t"),
|
||||
fogErase: (event: KeyboardEvent) => singleKey(event, "e"),
|
||||
fogLayer: (event: KeyboardEvent) => singleKey(event, "l"),
|
||||
fogPreview: (event: KeyboardEvent) => singleKey(event, "f"),
|
||||
fogCut: (event: KeyboardEvent) => singleKey(event, "c"),
|
||||
fogFinishPolygon: ({ key }: { key: string }) => key === "Enter",
|
||||
fogCancelPolygon: ({ key }: { key: string }) => key === "Escape",
|
||||
// Stage interaction
|
||||
stageZoomIn: zoomIn,
|
||||
stageZoomOut: zoomOut,
|
||||
stagePrecisionZoom: ({ key }: { key: string }) => key === "Shift",
|
||||
// Select
|
||||
selectRange: ({ key }: { key: string }) => key === "Shift",
|
||||
selectMultiple: ({ key }: { key: string }) => key === "Control" || key === "Meta",
|
||||
// Common
|
||||
undo,
|
||||
redo,
|
||||
delete: ({ key }: { key: string }) => key === "Backspace" || key === "Delete",
|
||||
};
|
||||
|
||||
export default shortcuts;
|
Loading…
Reference in New Issue
Block a user