Merge branch 'master' into typescript
This commit is contained in:
+72
-67
@@ -19,14 +19,14 @@ import {
|
||||
useDebouncedStageScale,
|
||||
} from "../contexts/MapInteractionContext";
|
||||
import { MapStageProvider, useMapStage } from "../contexts/MapStageContext";
|
||||
import AuthContext, { useAuth } from "../contexts/AuthContext";
|
||||
import UserIdContext, { useUserId } from "../contexts/UserIdContext";
|
||||
import SettingsContext, { useSettings } from "../contexts/SettingsContext";
|
||||
import KeyboardContext from "../contexts/KeyboardContext";
|
||||
import TokenDataContext, { useTokenData } from "../contexts/TokenDataContext";
|
||||
import {
|
||||
ImageSourcesStateContext,
|
||||
ImageSourcesUpdaterContext,
|
||||
} from "../contexts/ImageSourceContext";
|
||||
import AssetsContext, {
|
||||
AssetURLsStateContext,
|
||||
AssetURLsUpdaterContext,
|
||||
useAssets,
|
||||
} from "../contexts/AssetsContext";
|
||||
import {
|
||||
useGrid,
|
||||
useGridCellPixelSize,
|
||||
@@ -43,17 +43,18 @@ import {
|
||||
GridStrokeWidthContext,
|
||||
GridCellPixelOffsetContext,
|
||||
} from "../contexts/GridContext";
|
||||
import DatabaseContext, { useDatabase } from "../contexts/DatabaseContext";
|
||||
|
||||
/**
|
||||
* Provide a bridge for konva that forwards our contexts
|
||||
*/
|
||||
function KonvaBridge({ stageRender, children }: { stageRender: any, children: any}) {
|
||||
const mapStageRef = useMapStage();
|
||||
const auth = useAuth();
|
||||
const userId = useUserId();
|
||||
const settings = useSettings();
|
||||
const tokenData = useTokenData();
|
||||
const imageSources = useContext(ImageSourcesStateContext);
|
||||
const setImageSources = useContext(ImageSourcesUpdaterContext);
|
||||
const assets = useAssets();
|
||||
const assetURLs = useContext(AssetURLsStateContext);
|
||||
const setAssetURLs = useContext(AssetURLsUpdaterContext);
|
||||
const keyboardValue = useContext(KeyboardContext);
|
||||
|
||||
const stageScale = useStageScale();
|
||||
@@ -73,70 +74,74 @@ function KonvaBridge({ stageRender, children }: { stageRender: any, children: an
|
||||
const gridCellPixelOffset = useGridCellPixelOffset();
|
||||
const gridOffset = useGridOffset();
|
||||
|
||||
const database = useDatabase();
|
||||
|
||||
return stageRender(
|
||||
<AuthContext.Provider value={auth}>
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<KeyboardContext.Provider value={keyboardValue}>
|
||||
<MapStageProvider value={mapStageRef}>
|
||||
<TokenDataContext.Provider value={tokenData}>
|
||||
<ImageSourcesStateContext.Provider value={imageSources}>
|
||||
<ImageSourcesUpdaterContext.Provider value={setImageSources}>
|
||||
<InteractionEmitterContext.Provider
|
||||
value={interactionEmitter}
|
||||
>
|
||||
<SetPreventMapInteractionContext.Provider
|
||||
value={setPreventMapInteraction}
|
||||
<DatabaseContext.Provider value={database}>
|
||||
<UserIdContext.Provider value={userId}>
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<KeyboardContext.Provider value={keyboardValue}>
|
||||
<MapStageProvider value={mapStageRef}>
|
||||
<AssetsContext.Provider value={assets}>
|
||||
<AssetURLsStateContext.Provider value={assetURLs}>
|
||||
<AssetURLsUpdaterContext.Provider value={setAssetURLs}>
|
||||
<InteractionEmitterContext.Provider
|
||||
value={interactionEmitter}
|
||||
>
|
||||
<StageWidthContext.Provider value={stageWidth}>
|
||||
<StageHeightContext.Provider value={stageHeight}>
|
||||
<MapWidthContext.Provider value={mapWidth}>
|
||||
<MapHeightContext.Provider value={mapHeight}>
|
||||
<StageScaleContext.Provider value={stageScale}>
|
||||
<DebouncedStageScaleContext.Provider
|
||||
value={debouncedStageScale}
|
||||
>
|
||||
<GridContext.Provider value={grid}>
|
||||
<GridPixelSizeContext.Provider
|
||||
value={gridPixelSize}
|
||||
>
|
||||
<GridCellPixelSizeContext.Provider
|
||||
value={gridCellPixelSize}
|
||||
<SetPreventMapInteractionContext.Provider
|
||||
value={setPreventMapInteraction}
|
||||
>
|
||||
<StageWidthContext.Provider value={stageWidth}>
|
||||
<StageHeightContext.Provider value={stageHeight}>
|
||||
<MapWidthContext.Provider value={mapWidth}>
|
||||
<MapHeightContext.Provider value={mapHeight}>
|
||||
<StageScaleContext.Provider value={stageScale}>
|
||||
<DebouncedStageScaleContext.Provider
|
||||
value={debouncedStageScale}
|
||||
>
|
||||
<GridContext.Provider value={grid}>
|
||||
<GridPixelSizeContext.Provider
|
||||
value={gridPixelSize}
|
||||
>
|
||||
<GridCellNormalizedSizeContext.Provider
|
||||
value={gridCellNormalizedSize}
|
||||
<GridCellPixelSizeContext.Provider
|
||||
value={gridCellPixelSize}
|
||||
>
|
||||
<GridOffsetContext.Provider
|
||||
value={gridOffset}
|
||||
<GridCellNormalizedSizeContext.Provider
|
||||
value={gridCellNormalizedSize}
|
||||
>
|
||||
<GridStrokeWidthContext.Provider
|
||||
value={gridStrokeWidth}
|
||||
<GridOffsetContext.Provider
|
||||
value={gridOffset}
|
||||
>
|
||||
<GridCellPixelOffsetContext.Provider
|
||||
value={gridCellPixelOffset}
|
||||
<GridStrokeWidthContext.Provider
|
||||
value={gridStrokeWidth}
|
||||
>
|
||||
{children}
|
||||
</GridCellPixelOffsetContext.Provider>
|
||||
</GridStrokeWidthContext.Provider>
|
||||
</GridOffsetContext.Provider>
|
||||
</GridCellNormalizedSizeContext.Provider>
|
||||
</GridCellPixelSizeContext.Provider>
|
||||
</GridPixelSizeContext.Provider>
|
||||
</GridContext.Provider>
|
||||
</DebouncedStageScaleContext.Provider>
|
||||
</StageScaleContext.Provider>
|
||||
</MapHeightContext.Provider>
|
||||
</MapWidthContext.Provider>
|
||||
</StageHeightContext.Provider>
|
||||
</StageWidthContext.Provider>
|
||||
</SetPreventMapInteractionContext.Provider>
|
||||
</InteractionEmitterContext.Provider>
|
||||
</ImageSourcesUpdaterContext.Provider>
|
||||
</ImageSourcesStateContext.Provider>
|
||||
</TokenDataContext.Provider>
|
||||
</MapStageProvider>
|
||||
</KeyboardContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
</AuthContext.Provider>
|
||||
<GridCellPixelOffsetContext.Provider
|
||||
value={gridCellPixelOffset}
|
||||
>
|
||||
{children}
|
||||
</GridCellPixelOffsetContext.Provider>
|
||||
</GridStrokeWidthContext.Provider>
|
||||
</GridOffsetContext.Provider>
|
||||
</GridCellNormalizedSizeContext.Provider>
|
||||
</GridCellPixelSizeContext.Provider>
|
||||
</GridPixelSizeContext.Provider>
|
||||
</GridContext.Provider>
|
||||
</DebouncedStageScaleContext.Provider>
|
||||
</StageScaleContext.Provider>
|
||||
</MapHeightContext.Provider>
|
||||
</MapWidthContext.Provider>
|
||||
</StageHeightContext.Provider>
|
||||
</StageWidthContext.Provider>
|
||||
</SetPreventMapInteractionContext.Provider>
|
||||
</InteractionEmitterContext.Provider>
|
||||
</AssetURLsUpdaterContext.Provider>
|
||||
</AssetURLsStateContext.Provider>
|
||||
</AssetsContext.Provider>
|
||||
</MapStageProvider>
|
||||
</KeyboardContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
</UserIdContext.Provider>
|
||||
</DatabaseContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -520,6 +520,66 @@ class Vector2 {
|
||||
return { x: -p.y, y: p.x };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the centroid of the given points
|
||||
* @param {Vector2[]} points
|
||||
* @returns {Vector2}
|
||||
*/
|
||||
static centroid(points) {
|
||||
let center = { x: 0, y: 0 };
|
||||
for (let point of points) {
|
||||
center.x += point.x;
|
||||
center.y += point.y;
|
||||
}
|
||||
if (points.length > 0) {
|
||||
center = { x: center.x / points.length, y: center.y / points.length };
|
||||
}
|
||||
return center;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether given points are rectangular
|
||||
* @param {Vector2[]} points
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static rectangular(points) {
|
||||
if (points.length !== 4) {
|
||||
return false;
|
||||
}
|
||||
// Check whether distance to the center is the same for all four points
|
||||
const centroid = this.centroid(points);
|
||||
let prevDist;
|
||||
for (let point of points) {
|
||||
const dist = this.distance(point, centroid);
|
||||
if (prevDist && dist !== prevDist) {
|
||||
return false;
|
||||
} else {
|
||||
prevDist = dist;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether given points are circular
|
||||
* @param {Vector2[]} points
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static circular(points, threshold = 0.1) {
|
||||
const centroid = this.centroid(points);
|
||||
let distances = [];
|
||||
for (let point of points) {
|
||||
distances.push(this.distance(point, centroid));
|
||||
}
|
||||
if (distances.length > 0) {
|
||||
const maxDistance = Math.max(...distances);
|
||||
const minDistance = Math.min(...distances);
|
||||
return maxDistance - minDistance < threshold;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Vector2;
|
||||
|
||||
@@ -42,3 +42,11 @@ export function addPolygonIntersectionToShapes(shape: any, intersection: any, sh
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function shapeToGeometry(shape) {
|
||||
const shapePoints = shape.data.points.map(({ x, y }) => [x, y]);
|
||||
const shapeHoles = shape.data.holes.map((hole) =>
|
||||
hole.map(({ x, y }) => [x, y])
|
||||
);
|
||||
return [[shapePoints, ...shapeHoles]];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import set from "lodash.set";
|
||||
import unset from "lodash.unset";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
|
||||
/**
|
||||
* Remove all empty values from an object recursively
|
||||
* @param {Object} obj
|
||||
*/
|
||||
function trimArraysInObject(obj) {
|
||||
for (let key in obj) {
|
||||
const value = obj[key];
|
||||
if (Array.isArray(value)) {
|
||||
let arr = [];
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const el = value[i];
|
||||
if (typeof el === "object") {
|
||||
arr.push(trimArraysInObject(el));
|
||||
} else if (el !== undefined) {
|
||||
arr.push(el);
|
||||
}
|
||||
}
|
||||
obj[key] = arr;
|
||||
} else if (typeof obj[key] === "object") {
|
||||
obj[key] = trimArraysInObject(obj[key]);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function applyObservableChange(change) {
|
||||
// Custom application of dexie change to fix issue with array indices being wrong
|
||||
// https://github.com/dfahlander/Dexie.js/issues/1176
|
||||
// TODO: Fix dexie observable source
|
||||
let obj = cloneDeep(change.oldObj);
|
||||
const changes = Object.entries(change.mods).reverse();
|
||||
for (let [key, value] of changes) {
|
||||
if (value === null) {
|
||||
unset(obj, key);
|
||||
} else {
|
||||
obj = set(obj, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Trim empty values from calling unset on arrays
|
||||
obj = trimArraysInObject(obj);
|
||||
|
||||
return obj;
|
||||
}
|
||||
+5
-10
@@ -212,7 +212,7 @@ export function getUpdatedShapeData(
|
||||
y: data.y,
|
||||
});
|
||||
const scaled = Vector2.multiply(dif, gridRatio);
|
||||
const distance = Vector2.setLength(scaled);
|
||||
const distance = Vector2.magnitude(scaled);
|
||||
return {
|
||||
...data,
|
||||
radius: distance,
|
||||
@@ -234,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.setLength(dif);
|
||||
const length = Vector2.magnitude(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));
|
||||
@@ -257,18 +257,13 @@ export function getUpdatedShapeData(
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSimplifySize = 1 / 100;
|
||||
/**
|
||||
* Simplify points to a grid size
|
||||
* @param {Vector2[]} points
|
||||
* @param {Vector2} gridCellSize
|
||||
* @param {number} scale
|
||||
* @param {number} tolerance
|
||||
*/
|
||||
export function simplifyPoints(points: Vector2[], gridCellSize: Vector2, scale: number): any {
|
||||
return simplify(
|
||||
points,
|
||||
(Vector2.min(gridCellSize) as number * defaultSimplifySize) / scale
|
||||
);
|
||||
export function simplifyPoints(points: Vector2[], tolerance: number): Vector2[] {
|
||||
return simplify(points, tolerance);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
-7
@@ -65,6 +65,9 @@ export function getGridPixelSize(grid: Required<Grid>, baseWidth: number, baseHe
|
||||
* @returns {Size}
|
||||
*/
|
||||
export function getCellPixelSize(grid: Grid, gridWidth: number, gridHeight: number): Size {
|
||||
if (grid.size.x === 0 || grid.size.y === 0) {
|
||||
return new Size(0, 0);
|
||||
}
|
||||
switch (grid.type) {
|
||||
case "square":
|
||||
return new Size(gridWidth / grid.size.x, gridHeight / grid.size.y);
|
||||
@@ -226,7 +229,10 @@ export function getGridDefaultInset(grid: Grid, mapWidth: number, mapHeight: num
|
||||
* @returns {GridInset}
|
||||
*/
|
||||
export function getGridUpdatedInset(grid: Required<Grid>, mapWidth: number, mapHeight: number): GridInset {
|
||||
let inset = grid.inset;
|
||||
let inset = {
|
||||
topLeft: { ...grid.inset.topLeft },
|
||||
bottomRight: { ...grid.inset.bottomRight },
|
||||
};
|
||||
// Take current inset width and use it to calculate the new height
|
||||
if (grid.size.x > 0 && grid.size.x > 0) {
|
||||
// Convert to px relative to map size
|
||||
@@ -301,10 +307,7 @@ export function gridDistance(grid: Required<Grid>, a: Vector2, b: Vector2, cellS
|
||||
const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize);
|
||||
if (grid.type === "square") {
|
||||
if (grid.measurement.type === "chebyshev") {
|
||||
return Math.max(
|
||||
Math.abs(aCoord.x - bCoord.x),
|
||||
Math.abs(aCoord.y - bCoord.y)
|
||||
);
|
||||
return Vector2.max(Vector2.abs(Vector2.subtract(aCoord, bCoord)));
|
||||
} else if (grid.measurement.type === "alternating") {
|
||||
// Alternating diagonal distance like D&D 3.5 and Pathfinder
|
||||
const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord));
|
||||
@@ -312,7 +315,7 @@ export function gridDistance(grid: Required<Grid>, a: Vector2, b: Vector2, cellS
|
||||
const min: any = Vector2.min(delta);
|
||||
return max - min + Math.floor(1.5 * min);
|
||||
} else if (grid.measurement.type === "euclidean") {
|
||||
return Vector2.distance(aCoord, bCoord);
|
||||
return Vector2.magnitude(Vector2.divide(Vector2.subtract(a, b), cellSize));
|
||||
} else if (grid.measurement.type === "manhattan") {
|
||||
return Math.abs(aCoord.x - bCoord.x) + Math.abs(aCoord.y - bCoord.y);
|
||||
}
|
||||
@@ -328,7 +331,7 @@ export function gridDistance(grid: Required<Grid>, a: Vector2, b: Vector2, cellS
|
||||
2
|
||||
);
|
||||
} else if (grid.measurement.type === "euclidean") {
|
||||
return Vector2.distance(aCoord, bCoord);
|
||||
return Vector2.magnitude(Vector2.divide(Vector2.subtract(a, b), cellSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
|
||||
import { keyBy } from "./shared";
|
||||
|
||||
/**
|
||||
* @typedef GroupItem
|
||||
* @property {string} id
|
||||
* @property {"item"} type
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef GroupContainer
|
||||
* @property {string} id
|
||||
* @property {"group"} type
|
||||
* @property {GroupItem[]} items
|
||||
* @property {string} name
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {GroupItem|GroupContainer} Group
|
||||
*/
|
||||
|
||||
/**
|
||||
* Transform an array of group ids to their groups
|
||||
* @param {string[]} groupIds
|
||||
* @param {Group[]} groups
|
||||
* @return {Group[[]}
|
||||
*/
|
||||
export function groupsFromIds(groupIds, groups) {
|
||||
const groupsByIds = keyBy(groups, "id");
|
||||
const filteredGroups = [];
|
||||
for (let groupId of groupIds) {
|
||||
if (groupId in groupsByIds) {
|
||||
filteredGroups.push(groupsByIds[groupId]);
|
||||
}
|
||||
}
|
||||
return filteredGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all items from a group including all sub groups
|
||||
* @param {Group} group
|
||||
* @return {GroupItem[]}
|
||||
*/
|
||||
export function getGroupItems(group) {
|
||||
if (group.type === "group") {
|
||||
let groups = [];
|
||||
for (let item of group.items) {
|
||||
groups.push(...getGroupItems(item));
|
||||
}
|
||||
return groups;
|
||||
} else {
|
||||
return [group];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an array of groups into their assosiated items
|
||||
* @param {Group[]} groups
|
||||
* @param {any[]} allItems
|
||||
* @param {string} itemKey
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export function itemsFromGroups(groups, allItems, itemKey = "id") {
|
||||
const allItemsById = keyBy(allItems, itemKey);
|
||||
const groupedItems = [];
|
||||
|
||||
for (let group of groups) {
|
||||
const groupItems = getGroupItems(group);
|
||||
const items = groupItems.map((item) => allItemsById[item.id]);
|
||||
groupedItems.push(...items);
|
||||
}
|
||||
|
||||
return groupedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine two groups
|
||||
* @param {Group} a
|
||||
* @param {Group} b
|
||||
* @returns {GroupContainer}
|
||||
*/
|
||||
export function combineGroups(a, b) {
|
||||
if (a.type === "item") {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: "group",
|
||||
items: [a, b],
|
||||
name: "",
|
||||
};
|
||||
}
|
||||
if (a.type === "group") {
|
||||
return {
|
||||
id: a.id,
|
||||
type: "group",
|
||||
items: [...a.items, b],
|
||||
name: a.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutably move group at indices `indices` into group at index `into`
|
||||
* @param {Group[]} groups
|
||||
* @param {number} into
|
||||
* @param {number[]} indices
|
||||
* @returns {Group[]}
|
||||
*/
|
||||
export function moveGroupsInto(groups, into, indices) {
|
||||
const newGroups = cloneDeep(groups);
|
||||
|
||||
const intoGroup = newGroups[into];
|
||||
let fromGroups = [];
|
||||
for (let i of indices) {
|
||||
fromGroups.push(newGroups[i]);
|
||||
}
|
||||
|
||||
let combined = intoGroup;
|
||||
for (let fromGroup of fromGroups) {
|
||||
combined = combineGroups(combined, fromGroup);
|
||||
}
|
||||
|
||||
// Replace and remove old groups
|
||||
newGroups[into] = combined;
|
||||
for (let fromGroup of fromGroups) {
|
||||
const i = newGroups.findIndex((group) => group.id === fromGroup.id);
|
||||
newGroups.splice(i, 1);
|
||||
}
|
||||
|
||||
return newGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutably move group at indices `indices` to index `to`
|
||||
* @param {Group[]} groups
|
||||
* @param {number} into
|
||||
* @param {number[]} indices
|
||||
* @returns {Group[]}
|
||||
*/
|
||||
export function moveGroups(groups, to, indices) {
|
||||
const newGroups = cloneDeep(groups);
|
||||
|
||||
let fromGroups = [];
|
||||
for (let i of indices) {
|
||||
fromGroups.push(newGroups[i]);
|
||||
}
|
||||
|
||||
// Remove old groups
|
||||
for (let fromGroup of fromGroups) {
|
||||
const i = newGroups.findIndex((group) => group.id === fromGroup.id);
|
||||
newGroups.splice(i, 1);
|
||||
}
|
||||
|
||||
// Add back at new index
|
||||
newGroups.splice(to, 0, ...fromGroups);
|
||||
|
||||
return newGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move items from a sub group to the start of the base group
|
||||
* @param {Group[]} groups
|
||||
* @param {string} fromId The id of the group to move from
|
||||
* @param {number[]} indices The indices of the items in the group
|
||||
*/
|
||||
export function ungroup(groups, fromId, indices) {
|
||||
const newGroups = cloneDeep(groups);
|
||||
|
||||
let fromIndex = newGroups.findIndex((group) => group.id === fromId);
|
||||
|
||||
let items = [];
|
||||
for (let i of indices) {
|
||||
items.push(newGroups[fromIndex].items[i]);
|
||||
}
|
||||
|
||||
// Remove items from previous group
|
||||
for (let item of items) {
|
||||
const i = newGroups[fromIndex].items.findIndex((el) => el.id === item.id);
|
||||
newGroups[fromIndex].items.splice(i, 1);
|
||||
}
|
||||
|
||||
// If we have no more items in the group delete it
|
||||
if (newGroups[fromIndex].items.length === 0) {
|
||||
newGroups.splice(fromIndex, 1);
|
||||
}
|
||||
|
||||
// Add to base group
|
||||
newGroups.splice(0, 0, ...items);
|
||||
|
||||
return newGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find a group within a group array
|
||||
* @param {Group[]} groups
|
||||
* @param {string} groupId
|
||||
* @returns {Group}
|
||||
*/
|
||||
export function findGroup(groups, groupId) {
|
||||
for (let group of groups) {
|
||||
if (group.id === groupId) {
|
||||
return group;
|
||||
}
|
||||
const items = getGroupItems(group);
|
||||
for (let item of items) {
|
||||
if (item.id === groupId) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform and item array to a record of item ids to item names
|
||||
* @param {any[]} items
|
||||
* @param {string=} itemKey
|
||||
*/
|
||||
export function getItemNames(items, itemKey = "id") {
|
||||
let names = {};
|
||||
for (let item of items) {
|
||||
names[item[itemKey]] = item.name;
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutably rename a group
|
||||
* @param {Group[]} groups
|
||||
* @param {string} groupId
|
||||
* @param {string} newName
|
||||
*/
|
||||
export function renameGroup(groups, groupId, newName) {
|
||||
let newGroups = cloneDeep(groups);
|
||||
const groupIndex = newGroups.findIndex((group) => group.id === groupId);
|
||||
if (groupIndex >= 0) {
|
||||
newGroups[groupIndex].name = newName;
|
||||
}
|
||||
return newGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove items from groups including sub groups
|
||||
* @param {Group[]} groups
|
||||
* @param {string[]} itemIds
|
||||
*/
|
||||
export function removeGroupsItems(groups, itemIds) {
|
||||
let newGroups = cloneDeep(groups);
|
||||
|
||||
for (let i = newGroups.length - 1; i >= 0; i--) {
|
||||
const group = newGroups[i];
|
||||
if (group.type === "item") {
|
||||
if (itemIds.includes(group.id)) {
|
||||
newGroups.splice(i, 1);
|
||||
}
|
||||
} else {
|
||||
const items = group.items;
|
||||
for (let j = items.length - 1; j >= 0; j--) {
|
||||
const item = items[j];
|
||||
if (itemIds.includes(item.id)) {
|
||||
newGroups[i].items.splice(j, 1);
|
||||
}
|
||||
}
|
||||
// Remove group if no items are left
|
||||
if (newGroups[i].items.length === 0) {
|
||||
newGroups.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newGroups;
|
||||
}
|
||||
+103
-7
@@ -1,4 +1,7 @@
|
||||
import imageOutline from "image-outline";
|
||||
|
||||
import blobToBuffer from "./blobToBuffer";
|
||||
import Vector2 from "./Vector2";
|
||||
|
||||
const lightnessDetectionOffset = 0.1;
|
||||
|
||||
@@ -101,12 +104,11 @@ export async function resizeImage(image: HTMLImageElement, size: number, type: s
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef ImageFile
|
||||
* @property {Uint8Array|null} file
|
||||
* @typedef ImageAsset
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
* @property {"file"} type
|
||||
* @property {string} id
|
||||
* @property {Uint8Array} file
|
||||
* @property {string} mime
|
||||
*/
|
||||
|
||||
export type ImageFile = {
|
||||
@@ -122,7 +124,7 @@ export type ImageFile = {
|
||||
* @param {string} type the mime type of the image
|
||||
* @param {number} size the width and height of the thumbnail
|
||||
* @param {number} quality if image is a jpeg or webp this is the quality setting
|
||||
* @returns {Promise<ImageFile>}
|
||||
* @returns {Promise<ImageAsset>}
|
||||
*/
|
||||
export async function createThumbnail(image: HTMLImageElement, type: string, size = 300, quality = 0.5): Promise<ImageFile> {
|
||||
let canvas = document.createElement("canvas");
|
||||
@@ -174,7 +176,101 @@ export async function createThumbnail(image: HTMLImageElement, type: string, siz
|
||||
file: thumbnailBuffer,
|
||||
width: thumbnailImage.width,
|
||||
height: thumbnailImage.height,
|
||||
type: "file",
|
||||
id: "thumbnail",
|
||||
mime: type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef CircleOutline
|
||||
* @property {"circle"} type
|
||||
* @property {number} x - Center X of the circle
|
||||
* @property {number} y - Center Y of the circle
|
||||
* @property {number} radius
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef RectOutline
|
||||
* @property {"rect"} type
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
* @property {number} x - Leftmost X position of the rect
|
||||
* @property {number} y - Topmost Y position of the rect
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef PathOutline
|
||||
* @property {"path"} type
|
||||
* @property {number[]} points - Alternating x, y coordinates zipped together
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {CircleOutline|RectOutline|PathOutline} Outline
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the outline of an image
|
||||
* @param {HTMLImageElement} image
|
||||
* @returns {Outline}
|
||||
*/
|
||||
export function getImageOutline(image, maxPoints = 100) {
|
||||
// Basic rect outline for fail conditions
|
||||
const defaultOutline = {
|
||||
type: "rect",
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
try {
|
||||
let outlinePoints = imageOutline(image, {
|
||||
opacityThreshold: 1, // Allow everything except full transparency
|
||||
});
|
||||
|
||||
if (outlinePoints) {
|
||||
if (outlinePoints.length > maxPoints) {
|
||||
outlinePoints = Vector2.resample(outlinePoints, maxPoints);
|
||||
}
|
||||
const bounds = Vector2.getBoundingBox(outlinePoints);
|
||||
|
||||
// Reject outline if it's area is less than 5% of the image
|
||||
const imageArea = image.width * image.height;
|
||||
const area = bounds.width * bounds.height;
|
||||
if (area < imageArea * 0.05) {
|
||||
return defaultOutline;
|
||||
}
|
||||
|
||||
// Detect if the outline is a rectangle or circle
|
||||
if (Vector2.rectangular(outlinePoints)) {
|
||||
return {
|
||||
type: "rect",
|
||||
x: Math.round(bounds.min.x),
|
||||
y: Math.round(bounds.min.y),
|
||||
width: Math.round(bounds.width),
|
||||
height: Math.round(bounds.height),
|
||||
};
|
||||
} else if (
|
||||
Vector2.circular(
|
||||
outlinePoints,
|
||||
Math.max(bounds.width / 10, bounds.height / 10)
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: "circle",
|
||||
x: Math.round(bounds.center.x),
|
||||
y: Math.round(bounds.center.y),
|
||||
radius: Math.round(Math.min(bounds.width, bounds.height) / 2),
|
||||
};
|
||||
} else {
|
||||
// Flatten and round outline to save on storage size
|
||||
const points = outlinePoints
|
||||
.map(({ x, y }) => [Math.round(x), Math.round(y)])
|
||||
.flat();
|
||||
return { type: "path", points };
|
||||
}
|
||||
} else {
|
||||
return defaultOutline;
|
||||
}
|
||||
} catch {
|
||||
return defaultOutline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
import Case from "case";
|
||||
|
||||
import blobToBuffer from "./blobToBuffer";
|
||||
import { resizeImage, createThumbnail } from "./image";
|
||||
import {
|
||||
getGridDefaultInset,
|
||||
getGridSizeFromImage,
|
||||
gridSizeVaild,
|
||||
} from "./grid";
|
||||
import Vector2 from "./Vector2";
|
||||
|
||||
const defaultMapProps = {
|
||||
showGrid: false,
|
||||
snapToGrid: true,
|
||||
quality: "original",
|
||||
group: "",
|
||||
};
|
||||
|
||||
const mapResolutions = [
|
||||
{
|
||||
size: 30, // Pixels per grid
|
||||
quality: 0.5, // JPEG compression quality
|
||||
id: "low",
|
||||
},
|
||||
{ size: 70, quality: 0.6, id: "medium" },
|
||||
{ size: 140, quality: 0.7, id: "high" },
|
||||
{ size: 300, quality: 0.8, id: "ultra" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the asset id of the preview file to send for a map
|
||||
* @param {any} map
|
||||
* @returns {undefined|string}
|
||||
*/
|
||||
export function getMapPreviewAsset(map) {
|
||||
const res = map.resolutions;
|
||||
switch (map.quality) {
|
||||
case "low":
|
||||
return;
|
||||
case "medium":
|
||||
return res.low;
|
||||
case "high":
|
||||
return res.medium;
|
||||
case "ultra":
|
||||
return res.medium;
|
||||
case "original":
|
||||
if (res.medium) {
|
||||
return res.medium;
|
||||
} else if (res.low) {
|
||||
return res.low;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createMapFromFile(file, userId) {
|
||||
let image = new Image();
|
||||
|
||||
const buffer = await blobToBuffer(file);
|
||||
// Copy file to avoid permissions issues
|
||||
const blob = new Blob([buffer]);
|
||||
// Create and load the image temporarily to get its dimensions
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = async function () {
|
||||
// Find name and grid size
|
||||
let gridSize;
|
||||
let name = "Unknown Map";
|
||||
if (file.name) {
|
||||
if (file.name.matchAll) {
|
||||
// Match against a regex to find the grid size in the file name
|
||||
// e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]]
|
||||
const gridMatches = [...file.name.matchAll(/(\d+) ?(x|X) ?(\d+)/g)];
|
||||
for (let match of gridMatches) {
|
||||
const matchX = parseInt(match[1]);
|
||||
const matchY = parseInt(match[3]);
|
||||
if (
|
||||
!isNaN(matchX) &&
|
||||
!isNaN(matchY) &&
|
||||
gridSizeVaild(matchX, matchY)
|
||||
) {
|
||||
gridSize = { x: matchX, y: matchY };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!gridSize) {
|
||||
gridSize = await getGridSizeFromImage(image);
|
||||
}
|
||||
|
||||
// Remove file extension
|
||||
name = file.name.replace(/\.[^/.]+$/, "");
|
||||
// Removed grid size expression
|
||||
name = name.replace(/(\[ ?|\( ?)?\d+ ?(x|X) ?\d+( ?\]| ?\))?/, "");
|
||||
// Clean string
|
||||
name = name.replace(/ +/g, " ");
|
||||
name = name.trim();
|
||||
// Capitalize and remove underscores
|
||||
name = Case.capital(name);
|
||||
}
|
||||
|
||||
if (!gridSize) {
|
||||
gridSize = { x: 22, y: 22 };
|
||||
}
|
||||
|
||||
let assets = [];
|
||||
|
||||
// Create resolutions
|
||||
const resolutions = {};
|
||||
for (let resolution of mapResolutions) {
|
||||
const resolutionPixelSize = Vector2.multiply(gridSize, resolution.size);
|
||||
if (
|
||||
image.width >= resolutionPixelSize.x &&
|
||||
image.height >= resolutionPixelSize.y
|
||||
) {
|
||||
const resized = await resizeImage(
|
||||
image,
|
||||
Vector2.max(resolutionPixelSize),
|
||||
file.type,
|
||||
resolution.quality
|
||||
);
|
||||
if (resized.blob) {
|
||||
const assetId = uuid();
|
||||
resolutions[resolution.id] = assetId;
|
||||
const resizedBuffer = await blobToBuffer(resized.blob);
|
||||
const asset = {
|
||||
file: resizedBuffer,
|
||||
width: resized.width,
|
||||
height: resized.height,
|
||||
id: assetId,
|
||||
mime: file.type,
|
||||
owner: userId,
|
||||
};
|
||||
assets.push(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create thumbnail
|
||||
const thumbnailImage = await createThumbnail(image, file.type);
|
||||
const thumbnail = {
|
||||
...thumbnailImage,
|
||||
id: uuid(),
|
||||
owner: userId,
|
||||
};
|
||||
assets.push(thumbnail);
|
||||
|
||||
const fileAsset = {
|
||||
id: uuid(),
|
||||
file: buffer,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
mime: file.type,
|
||||
owner: userId,
|
||||
};
|
||||
assets.push(fileAsset);
|
||||
|
||||
const map = {
|
||||
name,
|
||||
resolutions,
|
||||
file: fileAsset.id,
|
||||
thumbnail: thumbnail.id,
|
||||
type: "file",
|
||||
grid: {
|
||||
size: gridSize,
|
||||
inset: getGridDefaultInset(
|
||||
{ size: gridSize, type: "square" },
|
||||
image.width,
|
||||
image.height
|
||||
),
|
||||
type: "square",
|
||||
measurement: {
|
||||
type: "chebyshev",
|
||||
scale: "5ft",
|
||||
},
|
||||
},
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
id: uuid(),
|
||||
created: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
owner: userId,
|
||||
...defaultMapProps,
|
||||
};
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
resolve({ map, assets });
|
||||
};
|
||||
image.onerror = reject;
|
||||
image.src = url;
|
||||
});
|
||||
}
|
||||
@@ -75,3 +75,24 @@ export function groupBy(array: any, key: string) {
|
||||
}
|
||||
|
||||
export const isMacLike = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
||||
|
||||
export function shuffle(array) {
|
||||
let temp = [...array];
|
||||
var currentIndex = temp.length,
|
||||
randomIndex;
|
||||
|
||||
// While there remain elements to shuffle...
|
||||
while (0 !== currentIndex) {
|
||||
// Pick a remaining element...
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
|
||||
// And swap it with the current element.
|
||||
[temp[currentIndex], temp[randomIndex]] = [
|
||||
temp[randomIndex],
|
||||
temp[currentIndex],
|
||||
];
|
||||
}
|
||||
|
||||
return temp;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
import Case from "case";
|
||||
|
||||
import blobToBuffer from "./blobToBuffer";
|
||||
import { createThumbnail, getImageOutline } from "./image";
|
||||
import Vector2 from "./Vector2";
|
||||
|
||||
export function createTokenState(token, position, userId) {
|
||||
let tokenState = {
|
||||
id: uuid(),
|
||||
tokenId: token.id,
|
||||
owner: userId,
|
||||
size: token.defaultSize,
|
||||
category: token.defaultCategory,
|
||||
label: token.defaultLabel,
|
||||
statuses: [],
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
lastModifiedBy: userId,
|
||||
lastModified: Date.now(),
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
visible: true,
|
||||
type: token.type,
|
||||
outline: token.outline,
|
||||
width: token.width,
|
||||
height: token.height,
|
||||
};
|
||||
if (token.type === "file") {
|
||||
tokenState.file = token.file;
|
||||
} else if (token.type === "default") {
|
||||
tokenState.key = token.key;
|
||||
}
|
||||
return tokenState;
|
||||
}
|
||||
|
||||
export async function createTokenFromFile(file, userId) {
|
||||
if (!file) {
|
||||
return Promise.reject();
|
||||
}
|
||||
let name = "Unknown Token";
|
||||
let defaultSize = 1;
|
||||
if (file.name) {
|
||||
if (file.name.matchAll) {
|
||||
// Match against a regex to find the grid size in the file name
|
||||
// e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]]
|
||||
const sizeMatches = [...file.name.matchAll(/(\d+) ?(x|X) ?(\d+)/g)];
|
||||
for (let match of sizeMatches) {
|
||||
const matchX = parseInt(match[1]);
|
||||
const matchY = parseInt(match[3]);
|
||||
if (
|
||||
!isNaN(matchX) &&
|
||||
!isNaN(matchY) &&
|
||||
matchX < 256 // Add check to test match isn't resolution
|
||||
) {
|
||||
defaultSize = matchX;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove file extension
|
||||
name = file.name.replace(/\.[^/.]+$/, "");
|
||||
// Removed grid size expression
|
||||
name = name.replace(/(\[ ?|\( ?)?\d+ ?(x|X) ?\d+( ?\]| ?\))?/, "");
|
||||
// Clean string
|
||||
name = name.replace(/ +/g, " ");
|
||||
name = name.trim();
|
||||
// Capitalize and remove underscores
|
||||
name = Case.capital(name);
|
||||
}
|
||||
let image = new Image();
|
||||
const buffer = await blobToBuffer(file);
|
||||
|
||||
// Copy file to avoid permissions issues
|
||||
const blob = new Blob([buffer]);
|
||||
// Create and load the image temporarily to get its dimensions
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = async function () {
|
||||
let assets = [];
|
||||
const thumbnailImage = await createThumbnail(image, file.type);
|
||||
const thumbnail = { ...thumbnailImage, id: uuid(), owner: userId };
|
||||
assets.push(thumbnail);
|
||||
|
||||
const fileAsset = {
|
||||
id: uuid(),
|
||||
file: buffer,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
mime: file.type,
|
||||
owner: userId,
|
||||
};
|
||||
assets.push(fileAsset);
|
||||
|
||||
const outline = getImageOutline(image);
|
||||
|
||||
const token = {
|
||||
name,
|
||||
defaultSize,
|
||||
thumbnail: thumbnail.id,
|
||||
file: fileAsset.id,
|
||||
id: uuid(),
|
||||
type: "file",
|
||||
created: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
owner: userId,
|
||||
defaultCategory: "character",
|
||||
defaultLabel: "",
|
||||
hideInSidebar: false,
|
||||
group: "",
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
outline,
|
||||
};
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
resolve({ token, assets });
|
||||
};
|
||||
image.onerror = reject;
|
||||
image.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
export function clientPositionToMapPosition(
|
||||
mapStage,
|
||||
clientPosition,
|
||||
checkMapBounds = true
|
||||
) {
|
||||
const mapImage = mapStage.findOne("#mapImage");
|
||||
const map = document.querySelector(".map");
|
||||
const mapRect = map.getBoundingClientRect();
|
||||
|
||||
// Check map bounds
|
||||
if (
|
||||
checkMapBounds &&
|
||||
(clientPosition.x < mapRect.left ||
|
||||
clientPosition.x > mapRect.right ||
|
||||
clientPosition.y < mapRect.top ||
|
||||
clientPosition.y > mapRect.bottom)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert relative to map rect
|
||||
const mapPosition = {
|
||||
x: clientPosition.x - mapRect.left,
|
||||
y: clientPosition.y - mapRect.top,
|
||||
};
|
||||
|
||||
// Convert relative to map image
|
||||
const transform = mapImage.getAbsoluteTransform().copy().invert();
|
||||
const relativePosition = transform.point(mapPosition);
|
||||
const normalizedPosition = {
|
||||
x: relativePosition.x / mapImage.width(),
|
||||
y: relativePosition.y / mapImage.height(),
|
||||
};
|
||||
|
||||
return normalizedPosition;
|
||||
}
|
||||
|
||||
export function getScaledOutline(tokenState, tokenWidth, tokenHeight) {
|
||||
let outline = tokenState.outline;
|
||||
if (outline.type === "rect") {
|
||||
return {
|
||||
...outline,
|
||||
x: (outline.x / tokenState.width) * tokenWidth,
|
||||
y: (outline.y / tokenState.height) * tokenHeight,
|
||||
width: (outline.width / tokenState.width) * tokenWidth,
|
||||
height: (outline.height / tokenState.height) * tokenHeight,
|
||||
};
|
||||
} else if (outline.type === "circle") {
|
||||
return {
|
||||
...outline,
|
||||
x: (outline.x / tokenState.width) * tokenWidth,
|
||||
y: (outline.y / tokenState.height) * tokenHeight,
|
||||
radius: (outline.radius / tokenState.width) * tokenWidth,
|
||||
};
|
||||
} else {
|
||||
let points = [...outline.points]; // Copy array so we can edit it imutably
|
||||
for (let i = 0; i < points.length; i += 2) {
|
||||
// Scale outline to the token
|
||||
points[i] = (points[i] / tokenState.width) * tokenWidth;
|
||||
points[i + 1] = (points[i + 1] / tokenState.height) * tokenHeight;
|
||||
}
|
||||
return { ...outline, points };
|
||||
}
|
||||
}
|
||||
|
||||
export class Intersection {
|
||||
/**
|
||||
*
|
||||
* @param {Outline} outline
|
||||
* @param {Vector2} position - Top left position of the token
|
||||
* @param {Vector2} center - Center position of the token
|
||||
* @param {number} rotation - Rotation of the token in degrees
|
||||
*/
|
||||
constructor(outline, position, center, rotation) {
|
||||
this.outline = outline;
|
||||
this.position = position;
|
||||
this.center = center;
|
||||
this.rotation = rotation;
|
||||
// Save points for intersection
|
||||
if (outline.type === "rect") {
|
||||
this.points = [
|
||||
Vector2.rotate(
|
||||
Vector2.add(new Vector2(outline.x, outline.y), position),
|
||||
center,
|
||||
rotation
|
||||
),
|
||||
Vector2.rotate(
|
||||
Vector2.add(
|
||||
new Vector2(outline.x + outline.width, outline.y),
|
||||
position
|
||||
),
|
||||
center,
|
||||
rotation
|
||||
),
|
||||
Vector2.rotate(
|
||||
Vector2.add(
|
||||
new Vector2(outline.x + outline.width, outline.y + outline.height),
|
||||
position
|
||||
),
|
||||
center,
|
||||
rotation
|
||||
),
|
||||
Vector2.rotate(
|
||||
Vector2.add(
|
||||
new Vector2(outline.x, outline.y + outline.height),
|
||||
position
|
||||
),
|
||||
center,
|
||||
rotation
|
||||
),
|
||||
];
|
||||
} else if (outline.type === "path") {
|
||||
this.points = [];
|
||||
for (let i = 0; i < outline.points.length; i += 2) {
|
||||
this.points.push(
|
||||
Vector2.rotate(
|
||||
Vector2.add(
|
||||
new Vector2(outline.points[i], outline.points[i + 1]),
|
||||
position
|
||||
),
|
||||
center,
|
||||
rotation
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Vector2} point
|
||||
* @returns {boolean}
|
||||
*/
|
||||
intersects(point) {
|
||||
if (this.outline.type === "rect" || this.outline.type === "path") {
|
||||
return Vector2.pointInPolygon(point, this.points);
|
||||
} else if (this.outline.type === "circle") {
|
||||
return Vector2.distance(this.center, point) < this.outline.radius;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user