Compare commits

...

41 Commits

Author SHA1 Message Date
Manuel Vögele fe2c8dd456 Merge branch 'develop' into pathfinding-difficult-terrain 2022-05-10 08:37:10 +02:00
Manuel Vögele b7354c2d9f Adjust to updated enhanced terrain layer api 2022-05-07 09:39:00 +02:00
Manuel Vögele 5904efbdd5 Remove redundant first waypoint from path generated by pathfinder
This fixes a bug where gridless snapping would snap slightly below the target range
2022-05-07 08:12:30 +02:00
Manuel Vögele d325f8acee Don't attempt to start background caching on gridless scenes, as it's currently unsupported (fixes #196) 2022-05-06 20:22:42 +02:00
Jonathan Calvert 1577435a33 Fix issue caused by client setting moving from Levels to Wall Height (#195) 2022-05-06 20:16:52 +02:00
Manuel Vögele e2ee00b937 Use a new cache if the difficult terrain changes 2022-05-04 22:58:45 +02:00
Manuel Vögele d9407ad7d8 The pathfinding code has changed to move forward 2022-05-03 23:02:05 +02:00
Manuel Vögele 4334bc4672 Merge branch 'develop' into pathfinding-difficult-terrain 2022-05-03 22:47:52 +02:00
Cole Schultz 1b8b2328c6 Add the pathfinding radius setting (#194) 2022-04-29 10:41:05 +02:00
Manuel Vögele 0dba39b145 Merge branch 'master' into develop 2022-04-12 00:22:20 +02:00
Manuel Vögele bdbd4fde18 Merge branch 'master' into develop 2022-04-01 18:07:25 +02:00
Manuel Vögele f10be469a2 Fix remainging errors from merge 2022-03-09 20:57:18 +01:00
Manuel Vögele 154dd1c009 Merge branch 'develop' into pathfinding-difficult-terrain 2022-03-09 19:21:36 +01:00
Jonathan Calvert 2979e86201 Improve Pathfinding: Add background caching (#175) 2022-03-09 18:40:43 +01:00
Manuel Vögele 8f81fd707e Merge branch 'master' into develop 2022-03-08 23:13:36 +01:00
Manuel Vögele 04c75f2e22 Merge branch 'develop' into pathfinding-difficult-terrain 2022-03-08 22:09:32 +01:00
Manuel Vögele d536ae8dfb Merge branch 'master' into develop 2022-03-08 22:07:43 +01:00
Manuel Vögele 5bb13ec590 Merge branch 'develop' into pathfinding-difficult-terrain 2022-03-08 12:52:28 +01:00
Manuel Vögele 44efabc656 Merge branch 'master' into develop 2022-03-08 12:50:16 +01:00
Manuel Vögele cb3475915e Remove unnecessary import 2022-03-08 11:58:07 +01:00
Manuel Vögele 68960b1530 Merge branch 'develop' into pathfinding-difficult-terrain 2022-03-08 11:49:47 +01:00
Manuel Vögele 6604e322bd Show diagnoal count in debug print 2022-03-08 11:49:27 +01:00
Manuel Vögele b68f3bbe29 Elevation also needs to be respected if only "wall-height" is enabled 2022-03-08 11:45:58 +01:00
Manuel Vögele 0cd91b06bb Merge branch 'develop' into pathfinding-difficult-terrain 2022-03-08 11:10:34 +01:00
Manuel Vögele a0aa51df78 Use the correct token shape 2022-03-08 10:21:18 +01:00
Manuel Vögele d1db57451a 5-10-5 support 2022-03-08 10:21:18 +01:00
Manuel Vögele 1783d26362 Fix several overculling artifacts 2022-03-08 10:21:18 +01:00
Manuel Vögele 4cdbd61259 Don't cull waypoints if the path cost would increase by doing so 2022-03-08 10:21:14 +01:00
Manuel Vögele 8687777fc1 Discourage diagonals 2022-03-08 10:18:35 +01:00
Manuel Vögele aec11f9f79 Initial difficult terrain support 2022-03-08 10:18:35 +01:00
Manuel Vögele 793e87bc2f Merge branch 'master' into develop 2022-03-08 09:56:03 +01:00
Manuel Vögele 65911c11f0 Only reclaculate the ruler if it's a Drag Ruler (fixes #161) 2022-03-07 14:37:15 +01:00
Jonathan Calvert 846e39d946 Pathfinding: Keep Multiple Caches (#177) 2022-03-05 21:36:56 +01:00
Manuel Vögele 26748ee234 Revert ebd0ee6ccf, which introduced a bug instead of fixing one 2022-03-04 08:40:04 +01:00
Manuel Vögele ebd0ee6ccf Fix the order of colission checks during pathfinding waypoint culling to make one-way-walls work properly 2022-03-02 22:33:42 +01:00
Jonathan Calvert 1d477094b8 Fix bug with diagonal movement (#174) 2022-03-01 14:28:01 +01:00
Jonathan Calvert bbfce2bb77 Pathfinding Improvements: Improve 5-10-5 Diagonal Handling (#170) 2022-02-28 23:08:56 +01:00
Manuel Vögele 54cee52eb2 Add support for the wall height module in the gridless pathfinder 2022-02-28 22:48:15 +01:00
Jonathan Calvert a967a03197 Levels Compatibility: Clear pathfinding cache after changing elevation (#173) 2022-02-28 21:17:54 +01:00
Jonathan Calvert 8d243ce919 Fix one-way wall collision detection (fixes #167) (#172) 2022-02-28 19:13:12 +01:00
Jonathan Calvert b227d5073b Pathfinding Improvements: Replace nextNodes array with PriorityQueueSet (#169) 2022-02-28 18:23:46 +01:00
13 changed files with 796 additions and 122 deletions
+1
View File
@@ -2,3 +2,4 @@
artifact/
wasm/
*.lock
.vscode/
+5 -1
View File
@@ -2,7 +2,7 @@ import {measureDistances} from "./compatibility.js";
import {getMovementHistory} from "./movement_tracking.js";
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
import {settingsKey} from "./settings.js"
import {getTokenShape} from "./util.js";
import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
export const availableSpeedProviders = {}
export let currentSpeedProvider = undefined
@@ -140,6 +140,10 @@ export function getMovedDistanceFromToken(token) {
return distances.reduce((acc, val) => acc + val, 0);
}
export function buildCostFunction(token, shape) {
return (x, y, costOptions={}) => getCostFromSpeedProvider(token, getAreaFromPositionAndShape({x, y}, shape), costOptions);
}
export function registerModule(moduleId, speedProvider) {
// Check if a module with the given id exists and is currently enabled
const module = game.modules.get(moduleId)
+3 -3
View File
@@ -1,6 +1,6 @@
import {getCostFromSpeedProvider} from "./api.js";
import {buildCostFunction} from "./api.js";
import {settingsKey} from "./settings.js";
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
import {highlightTokenShape} from "./util.js";
export function getHexSizeSupportTokenGridCenter(token) {
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
@@ -27,7 +27,7 @@ export function measureDistances(segments, entity, shape, options={}) {
const newSegments = segments.slice(firstNewSegmentIndex);
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
opts.costFunction = (x, y, costOptions={}) => { return getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), costOptions); }
opts.costFunction = buildCostFunction(entity, shape);
if (previousSegments.length > 0)
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
+138
View File
@@ -0,0 +1,138 @@
/**
* A combination queue/set where the elements are ordered (in ascending order, according to the given priority function)
* and unique (according to the given elementMatcher).
*
* If an element is added to the set and an equivalent element already exists, the lower-priority one is discarded.
*/
export class PriorityQueueSet {
constructor(elementMatcher, priorityFunction) {
this.first = null;
this.elementMatcher = elementMatcher;
this.priorityFunction = priorityFunction;
}
pushWithPriority(value) {
const newNode = {value, priority: this.priorityFunction(value), next: null};
// If the queue is currently empty, we can just set this new node as the first and we're done
if (!this.first) {
this.first = newNode;
return;
}
let inserted = false;
let previous;
let current = this.first;
// Loop through the existing elements
while (current) {
if (this.elementMatcher(current.value, value)) {
// We've found an equivalent element before one with a lower priority. This one has at least
// the same priority as the new one, so don't bother inserting
return;
} else if (newNode.priority <= current.priority) {
// We've found some element with lower priority than the new one, so insert the new one just before it
newNode.next = current;
if (previous) {
previous.next = newNode;
} else {
this.first = newNode;
}
inserted = true;
previous = current;
current = current.next;
break;
}
previous = current;
current = current.next;
}
if (inserted) {
// Go through the rest of the list and try to find an equivalent element to the new one.
// We know it has higher priority than the new one, so remove it.
while (current) {
if (this.elementMatcher(current.value, value)) {
if (previous) {
previous.next = current.next;
} else {
this.first = current.next;
}
return;
}
previous = current;
current = current.next;
}
} else {
// We reached the end of the queue without finding a lower-priority or existing element, so
// insert the new one at the end
previous.next = newNode;
}
}
hasNext() {
return !!this.first;
}
pop() {
const first = this.first;
this.first = first?.next;
return first?.value;
}
}
/**
* Queue that will only ever accept elements with a given value once. Elements must have a "value" field, the
* JSON representation of which will be used as the key to match
*/
export class ProcessOnceQueue {
constructor() {
this.first = null;
this.last = null;
this.previouslyQueued = new Set();
}
/**
* Remove everything from the queue and forget all the previously-queued items
*/
reset() {
this.first = null;
this.last = null;
this.previouslyQueued.clear();
}
push(element) {
if (this.previouslyQueued.has(element)) {
return;
}
this.previouslyQueued.add(element);
const newNode = {
value: element,
next: null,
previous: null
}
if (!this.first) {
this.first = newNode;
this.last = newNode;
} else {
this.last.next = newNode;
newNode.previous = this.last;
this.last = newNode;
}
}
pop() {
const node = this.first;
this.first = node?.next;
if (!node?.next) {
this.last = null;
}
return node?.value;
}
hasNext() {
return !!this.first;
}
}
+3 -1
View File
@@ -175,7 +175,9 @@ export function measure(destination, options={}) {
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
const to = getGridPositionFromPixelsObj(destination);
let path = findPath(from, to, this.draggedEntity, this.waypoints);
if (path) {
if (path)
path.shift();
if (path && path.length > 0) {
path = path.map(point => getSnapPointForTokenObj(getPixelsFromGridPositionObj(point), this.draggedEntity));
// If the token is snapped to the grid, the first point of the path is already handled by the ruler
+93
View File
@@ -0,0 +1,93 @@
/**
* Functions taken directly from the hex-size-support module (https://github.com/Ourobor/Hex-Size-Support/releases/tag/1.1.0).
* Unless otherwise stated, these functions are taken as-is.
*/
/**
* Altered version of this function.
* - Instead of taking a token as a parameter to retrieve the altOrientationFlag, receive the flag value directly
* - Instead of taking a grid parameter, get the grid value from the globas canvas
*/
export function findVertexSnapPoint(x, y, altOrientationFlag) {
const grid = canvas.grid.grid;
if (grid.columns) {
return findSnapPointCols(x, y, grid.h, grid.w, altOrientationFlag);
} else {
return findSnapPointRows(x, y, grid.h, grid.w, altOrientationFlag);
}
}
function findSnapPointRows(x, y, h, w, alt) {
let xOffset = 0.0
if (canvas.grid.grid.even) {
xOffset = -0.5
}
let yOffset1 = 0.75
let yOffset2 = 0.00
if (alt) {
yOffset1 = 0.25
yOffset2 = 1.00
}
let row1 = calculateSnapPointsRows(x, y, h, w, 0.5 + xOffset, yOffset1);
let row2 = calculateSnapPointsRows(x, y, h, w, 1.0 + xOffset, yOffset2);
let dist1 = Math.pow((row1.x - x), 2) + Math.pow((row1.y - y), 2)
let dist2 = Math.pow((row2.x - x), 2) + Math.pow((row2.y - y), 2)
if (dist1 < dist2) {
return row1
}
else {
return row2
}
}
function calculateSnapPointsRows(x, y, h, w, xOff, yOff) {
let c = Math.floor(((x + ((0.5 - xOff) * w)) / w) + 1)
let r = Math.floor(((y + ((0.75 - yOff) * h)) / (1.5 * h)) + 1)
let snapX = (c * w) - ((1 - xOff) * w)
let snapY = (r * h * 1.5) - ((1.5 - yOff) * h)
return {x: snapX, y: snapY}
}
function findSnapPointCols(x, y, h, w, alt) {
let yOffset = 0.0
if (canvas.grid.grid.even) {
yOffset = -0.5
}
let xOffset1 = 0.25
let xOffset2 = 1.00
if (alt) {
xOffset1 = 0.75
xOffset2 = 0.00
}
let row1 = calculateSnapPointsCols(x, y, h, w, xOffset1, 0.5 + yOffset);
let row2 = calculateSnapPointsCols(x, y, h, w, xOffset2, 1.0 + yOffset);
let dist1 = Math.pow((row1.x - x), 2) + Math.pow((row1.y - y), 2)
let dist2 = Math.pow((row2.x - x), 2) + Math.pow((row2.y - y), 2)
if (dist1 < dist2) {
return row1
}
else {
return row2
}
}
function calculateSnapPointsCols(x, y, h, w, xOff, yOff) {
let c = Math.floor(((x + ((0.75 - xOff) * w)) / (1.5 * w)) + 1)
let r = Math.floor(((y + ((0.5 - yOff) * h)) / h) + 1)
let snapX = (c * w * 1.5) - ((1.5 - xOff) * w)
let snapY = (r * h) - ((1 - yOff) * h)
return {x: snapX, y: snapY}
}
+16 -1
View File
@@ -7,7 +7,7 @@ import {disableSnap, registerKeybindings} from "./keybindings.js";
import {libWrapper} from "./libwrapper_shim.js";
import {performMigrations} from "./migration.js"
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
import {wipePathfindingCache, initializePathfinding} from "./pathfinding.js";
import {wipePathfindingCache, initializePathfinding, startBackgroundCaching} from "./pathfinding.js";
import {extendRuler} from "./ruler.js";
import {registerSettings, RightClickAction, settingsKey} from "./settings.js"
import {recalculate} from "./socket.js";
@@ -28,6 +28,21 @@ initGridlessPathfinding().then(() => {
Hooks.on("createWall", wipePathfindingCache);
Hooks.on("updateWall", wipePathfindingCache);
Hooks.on("deleteWall", wipePathfindingCache);
// Whenever the current user selects a token, start caching
Hooks.on("controlToken", (token, controlled) => {
if (controlled) {
startBackgroundCaching(token);
}
});
// Whenever a token the current user controls updates, start caching
Hooks.on("updateToken", (document) => {
const token = document.object;
if (token._controlled) {
startBackgroundCaching(token);
}
});
});
Hooks.once("init", () => {
+386 -95
View File
@@ -1,13 +1,238 @@
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
import {getCenterFromGridPositionObj, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
import {moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
import {debugGraphics} from "./main.js";
import {settingsKey} from "./settings.js";
import {getSnapPointForTokenObj, iterPairs} from "./util.js";
import {buildSnapPointTokenData, getSnapPointForTokenDataObj, getTokenShape, getTokenShapeForTokenData, isModuleActive, iterPairs} from "./util.js";
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js"
import * as GridlessPathfinding from "../wasm/gridless_pathfinding.js";
import {PriorityQueueSet, ProcessOnceQueue} from "./data_structures.js";
import {buildCostFunction} from "./api.js";
class CacheLayer {
constructor(tokenData, cacheId) {
this.tokenData = tokenData;
this.cacheId = cacheId;
this.queue = new ProcessOnceQueue();
this.buildNodes();
this.registerUse();
}
buildNodes() {
this.nodes = new Array(gridHeight);
for (let y = 0; y < gridHeight; y++) {
this.nodes[y] = new Array(gridWidth);
for (let x = 0; x < gridWidth; x++) {
this.nodes[y][x] = {x, y};
}
}
}
registerUse() {
this.lastUsed = Date.now();
}
}
/**
* Class to hold all the cached node data, and functions to deal with caching.
*
* Since pathfinding can depend on several factors, e.g. the token's size, we keep
* several caches, keyed by all the data relevant to pathfinding. If we already have
* the maximum number of caches and we need to create another one, we discard the
* one not used for the longest.
*
* When we select a token, or a token we have selected updates, we start caching
* in the background so, when we do start pathfinding, it's very performant.
*
* Background caching starts by trying to run an idle process (when the browser is
* otherwise not busy), but if it can't do that after an amount of time (e.g. the
* CPU is very slow and is busy) then we instead start caching a few nodes each
* frame.
*/
class Cache {
static maxCacheLayers = 5;
static maxBackgroundCachingMillis = 10;
static maxAnimationCachingMillis = 5;
static backgroundCachingTimeoutMillis = 200;
constructor() {
this.layers = new Map();
this.background = {
nextJobId: null,
nextTimeoutId: null,
nextAnimationFrameId: null
}
}
clear() {
this.layers.clear();
if (this.background.nextJobId) {
window.cancelIdleCallback(this.background.nextJobId);
this.background.nextJobId = null;
}
this.cancelTimeout();
this.cancelAnimationFrame();
}
/**
* Retrieve the cache layer for this token, using information that can make a difference to the pathfinding algorithm
* If a layer that suits this token doesn't exist, create one
*/
getCacheLayer(token) {
const tokenData = buildTokenData(token);
// TODO Request this from the speed providers so they can set their own options
let terrainData = canvas.terrain.listAllTerrain({token});
terrainData = terrainData.map(data => {
return {
x: data.object.x,
y: data.object.y,
cost: data.cost,
shape: data.shape,
};
});
const cacheIdData = {tokenData, terrainData};
const cacheId = GridlessPathfinding.sha1(JSON.stringify(cacheIdData));
let cacheLayer = this.layers.get(cacheId);
// If we don't already have a cache layer for this cache ID, create one now
if (!cacheLayer) {
// Check if we already have the max number of layers. If we do,
// get rid of the one that hasn't been used for the longest
if (this.layers.size >= Cache.maxCacheLayers) {
const oldestCache = Array.from(this.layers.values())
.reduce((layer1, layer2) => (layer1?.lastUsed < layer2.lastUsed) ? layer1 : layer2, null);
this.layers.delete(oldestCache.cacheId);
}
// Create the new cache
cacheLayer = new CacheLayer(tokenData, cacheId);
this.layers.set(cacheId, cacheLayer);
} else {
// Register that we're using this cache right now
cacheLayer.registerUse();
}
return cacheLayer;
}
/**
* Start background caching from the token's current position
*/
startBackgroundCaching(token) {
const cacheLayer = this.getCacheLayer(token);
const tokenPosition = getGridPositionFromPixelsObj(token.position)
cacheLayer.queue.push(cacheLayer.nodes[tokenPosition.y][tokenPosition.x]);
this.scheduleBackgroundCache();
}
/**
* Find if any of the caches have more nodes to background cache. If there is, then schedule a background
* caching job for that queue
*/
scheduleBackgroundCache() {
// If we already have a nextJobId, then don't start another one
if (this.background.nextJobId) return;
// Find the latest-used cache that has nodes left to cache
const latestCache = this.getLatestCacheWithNonEmptyQueue();
if (latestCache) {
this.background.nextJobId = window.requestIdleCallback(
() => this.runBackgroundCache(latestCache)
);
this.resetAnimationFrameTimeout();
}
}
/**
* Start a timeout which, if we reach the timeout time, will schedule a small amount of caching
* to be performed every frame. This timeout will be reset every time we perform background caching.
*/
resetAnimationFrameTimeout() {
this.cancelTimeout();
this.cancelAnimationFrame();
this.background.nextTimeoutId = window.setTimeout(
() => {
this.scheduleAnimationFrameCache();
this.background.nextTimeoutId = null;
},
Cache.backgroundCachingTimeoutMillis
);
}
/**
* Schedule a small amount of caching to be done just before the next frame renders
*/
scheduleAnimationFrameCache() {
const latestCache = this.getLatestCacheWithNonEmptyQueue();
if (latestCache) {
this.background.nextAnimationFrameId = window.requestAnimationFrame(
() => this.runAnimationCache(latestCache)
);
}
}
/**
* Find which cache was last used and get its cache ID
*/
getLatestCacheWithNonEmptyQueue() {
return Array.from(this.layers.values())
.filter(layer => layer.queue.hasNext())
.reduce((layer1, layer2) => (layer1?.lastUsed > layer2.lastUsed) ? layer1 : layer2, null);
}
/**
* Cache nodes for a short time, and then schedule another idle job to cache more nodes
*/
runBackgroundCache(cacheLayer) {
const endTime = performance.now() + Cache.maxBackgroundCachingMillis;
while (cacheLayer.queue.hasNext() && performance.now() < endTime) {
this.cacheNextNode(cacheLayer);
}
this.background.nextJobId = null;
this.scheduleBackgroundCache();
}
/**
* Cache nodes for a very short time, then schedule to cache more nodes next frame
*/
runAnimationCache(cacheLayer) {
const endTime = performance.now() + Cache.maxAnimationCachingMillis;
while (cacheLayer.queue.hasNext() && performance.now() < endTime) {
this.cacheNextNode(cacheLayer);
}
this.background.nextAnimationFrameId = null;
this.scheduleAnimationFrameCache();
}
cacheNextNode(cacheLayer) {
let node = cacheLayer.queue.pop();
getNode(node, cacheLayer);
for (let edge of node.edges) {
cacheLayer.queue.push(edge.target);
}
}
cancelTimeout() {
if (this.background.nextTimeoutId) {
window.clearTimeout(this.background.nextTimeoutId);
this.background.nextTimeoutId = null;
}
}
cancelAnimationFrame() {
if (this.background.nextAnimationFrameId) {
window.cancelAnimationFrame(this.background.nextAnimationFrameId);
this.background.nextAnimationFrameId = null;
}
}
}
const cache = new Cache();
let cachedNodes = undefined;
let cacheElevation;
let use5105 = false;
let gridlessPathfinders = new Map();
let gridWidth, gridHeight;
@@ -23,72 +248,108 @@ export function isPathfindingEnabled() {
}
export function findPath(from, to, token, previousWaypoints) {
checkCacheValid(token);
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size;
let pathfinder = gridlessPathfinders.get(tokenSize);
if (!pathfinder) {
pathfinder = GridlessPathfinding.initialize(canvas.walls.placeables, tokenSize, token.data.elevation, Boolean(game.modules.get("levels")?.active));
let radiusMultiplier = game.settings.get(settingsKey, "pathfindingRadius");
pathfinder = GridlessPathfinding.initialize(canvas.walls.placeables, tokenSize * radiusMultiplier, token.data.elevation, Boolean(game.modules.get("wall-height")?.active));
gridlessPathfinders.set(tokenSize, pathfinder);
}
paintGridlessPathfindingDebug(pathfinder);
return GridlessPathfinding.findPath(pathfinder, from, to);
}
else {
const lastNode = calculatePath(from, to, token, previousWaypoints);
if (!lastNode)
} else {
const cacheLayer = cache.getCacheLayer(token);
const firstNode = calculatePath(from, to, cacheLayer, previousWaypoints);
if (!firstNode)
return null;
paintGriddedPathfindingDebug(lastNode, token);
paintGriddedPathfindingDebug(firstNode, cacheLayer.tokenData);
const path = [];
let currentNode = lastNode;
let currentNode = firstNode;
while (currentNode) {
// TODO Check if the distance doesn't change
if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, token))
// Replace last waypoint if the current waypoint leads to a valid path
path[path.length - 1] = {x: currentNode.node.x, y: currentNode.node.y};
else
path.push({x: currentNode.node.x, y: currentNode.node.y});
currentNode = currentNode.previous;
if (path.length >= 2 && !stepCollidesWithWall(path[path.length - 2], currentNode.node, cacheLayer.tokenData)) {
// Replace last waypoint if the current waypoint leads to a valid path that isn't longer than the old path
if (window.terrainRuler) {
let startNode = getCenterFromGridPositionObj(path[path.length - 2]);
let middleNode = getCenterFromGridPositionObj(path[path.length - 1]);
let endNode = getCenterFromGridPositionObj(currentNode.node);
let oldPath = [{ray: new Ray(startNode, middleNode)}, {ray: new Ray(middleNode, endNode)}];
let newPath = [{ray: new Ray(startNode, endNode)}];
let costFunction = buildCostFunction(token, getTokenShape(token));
// TODO Cache the used measurement for use in the next loop to improve performance
let oldDistance = terrainRuler.measureDistances(oldPath, {costFunction}).reduce((a, b) => a + b);
let newDistance = terrainRuler.measureDistances(newPath, {costFunction})[0];
// TODO We might need to check if the diagonal count has increased on 5-10-5
if (newDistance < oldDistance) {
path.pop();
}
else if (newDistance === oldDistance) {
let oldNoDiagonals = oldPath[1].ray.terrainRulerFinalState?.noDiagonals;
let newNoDiagonals = newPath[0].ray.terrainRulerFinalState?.noDiagonals;
// This uses === && < instead of <= because the variables might be undefined (which shall lead to a true result)
if (oldNoDiagonals === newNoDiagonals || newNoDiagonals < oldNoDiagonals) {
path.pop();
}
}
}
else {
path.pop();
}
}
path.push({x: currentNode.node.x, y: currentNode.node.y});
currentNode = currentNode.next;
}
return path;
}
}
function getNode(pos, token, initialize=true) {
pos = {layer: 0, ...pos}; // Copy pos and set pos.layer to the default value if it's unset
if (!cachedNodes)
cachedNodes = new Array(2);
if (!cachedNodes[pos.layer])
cachedNodes[pos.layer] = new Array(Math.ceil(canvas.dimensions.height / canvas.grid.h));
if (!cachedNodes[pos.layer][pos.y])
cachedNodes[pos.layer][pos.y] = new Array(Math.ceil(canvas.dimensions.width / canvas.grid.w));
if (!cachedNodes[pos.layer][pos.y][pos.x]) {
cachedNodes[pos.layer][pos.y][pos.x] = pos;
function buildTokenData(token) {
// Almost all the information we need is for calculating the snap point
const tokenData = buildSnapPointTokenData(token);
// If Wall Height is enabled, which walls matter depends on the token's elevation.
// Depending on the settings in Wall Height, the height we care about is either their
// foot height (elevation) or eye height (losHeight).
if (isModuleActive("wall-height")) {
const blockSightMovement = game.settings.get("wall-height", "blockSightMovement");
tokenData.elevation = blockSightMovement ? token.losHeight : token.data.elevation;
}
const node = cachedNodes[pos.layer][pos.y][pos.x];
return tokenData;
}
function getNode(pos, cacheLayer, initialize = true) {
const node = cacheLayer.nodes[pos.y][pos.x];
if (initialize && !node.edges) {
node.edges = [];
for (const neighborPos of canvas.grid.grid.getNeighbors(pos.y, pos.x).map(([y, x]) => {return {x, y};})) {
if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x > gridWidth || neighborPos.y > gridHeight) {
if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x >= gridWidth || neighborPos.y >= gridHeight) {
continue;
}
// TODO Work with pixels instead of grid locations
if (!stepCollidesWithWall(neighborPos, pos, token)) {
if (!stepCollidesWithWall(pos, neighborPos, cacheLayer.tokenData)) {
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
let targetLayer = pos.layer;
if (use5105 && isDiagonal)
targetLayer = 1 - targetLayer;
const neighbor = getNode({...neighborPos, layer: targetLayer}, token, false);
// TODO We currently assume a cost of one or two for all transitions. Change this for difficult terrain support
let edgeCost = 1;
if (isDiagonal) {
// We charge 0.0001 more for edges to avoid unnecessary diagonal steps
edgeCost = pos.layer === 1 && targetLayer === 0 ? 2 : 1.0001;
let edgeCost;
if (window.terrainRuler) {
let ray = new Ray(getCenterFromGridPositionObj(pos), getCenterFromGridPositionObj(neighborPos));
let measuredDistance = terrainRuler.measureDistances([{ray}], {costFunction: buildCostFunction(cacheLayer.tokenData, getTokenShapeForTokenData(cacheLayer.tokenData))})[0];
edgeCost = Math.round(measuredDistance / canvas.dimensions.distance);
if (ray.terrainRulerFinalState?.noDiagonals === 1) {
edgeCost = 1.5;
}
// Charge 1.0001 instead of 1 for diagonals to discourage unnecessary diagonals
if (isDiagonal && edgeCost == 1) {
edgeCost = 1.0001;
}
}
else {
// Count 5-10-5 diagonals as 1.5 (so two add up to 3) and 5-5-5 diagonals as 1.0001 (to discourage unnecessary diagonals)
// TODO Account for difficult terrain
edgeCost = isDiagonal ? (use5105 ? 1.5 : 1.0001) : 1;
}
const neighbor = getNode(neighborPos, cacheLayer, false);
node.edges.push({target: neighbor, cost: edgeCost});
}
}
@@ -96,46 +357,69 @@ function getNode(pos, token, initialize=true) {
return node;
}
function calculatePath(from, to, token, previousWaypoints) {
if (game.system.id === "pf2e")
use5105 = true;
if (canvas.grid.diagonalRule === "5105")
use5105 = true;
let startLayer = 0;
function calculatePath(from, to, cacheLayer, previousWaypoints) {
use5105 = game.system.id === "pf2e" || canvas.grid.diagonalRule === "5105";
let startCost = 0;
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
previousWaypoints = previousWaypoints.map(w => getGridPositionFromPixelsObj(w));
startLayer = calcNoDiagonals(previousWaypoints) % 2;
startCost = (calcNoDiagonals(previousWaypoints) % 2) * 0.5;
}
const nextNodes = [{node: getNode({...to, layer: startLayer}, token), cost: 0, estimated: estimateCost(to, from), previous: null}];
const nextNodes = new PriorityQueueSet((node1, node2) => node1.node === node2.node, node => node.estimated);
const previousNodes = new Set();
while (nextNodes.length > 0) {
// Sort by estimated cost, high to low
// TODO Re-sorting every iteration is expensive. Think of something better
nextNodes.sort((a, b) => b.estimated - a.estimated);
nextNodes.pushWithPriority(
{
node: getNode(from, cacheLayer),
cost: startCost,
estimated: startCost + estimateCost(from, to),
previous: null
}
);
while (nextNodes.hasNext()) {
// Get node with cheapest estimate
const currentNode = nextNodes.pop();
if (currentNode.node.x === from.x && currentNode.node.y === from.y)
return currentNode;
if (currentNode.node.x === to.x && currentNode.node.y === to.y) {
return buildPathNodes(currentNode);
}
previousNodes.add(currentNode.node);
for (const edge of currentNode.node.edges) {
const neighborNode = getNode(edge.target, token);
if (previousNodes.has(neighborNode))
const neighborNode = getNode(edge.target, cacheLayer);
if (previousNodes.has(neighborNode)) {
continue;
const neighbor = {node: neighborNode, cost: currentNode.cost + edge.cost, estimated: currentNode.cost + edge.cost + estimateCost(neighborNode, from), previous: currentNode};
const neighborIndex = nextNodes.findIndex(node => node.node === neighbor.node);
if (neighborIndex >= 0) {
// If the neighbor is cheaper to reach via the current route than through previously discovered routes, replace it
if (nextNodes[neighborIndex].cost > neighbor.cost) {
nextNodes[neighborIndex] = neighbor;
}
}
else {
nextNodes.push(neighbor);
}
const neighbor = {
node: neighborNode,
cost: currentNode.cost + edge.cost,
estimated: currentNode.cost + edge.cost + estimateCost(neighborNode, to),
previous: currentNode
};
nextNodes.pushWithPriority(neighbor);
}
}
}
/**
* Now we've found the path, we know the final node, and each node links to the previous one.
* Reverse this list and return the first node in the path, with each node linking to the next
*/
function buildPathNodes(lastNode) {
let currentNode = lastNode;
let previousNode = null;
while (currentNode) {
const pathNode = {
node: currentNode.node,
cost: currentNode.cost,
next: previousNode
}
previousNode = pathNode;
currentNode = currentNode.previous;
}
return previousNode;
}
function calcNoDiagonals(waypoints) {
let diagonals = 0;
for (const [p1, p2] of iterPairs(waypoints)) {
@@ -144,18 +428,30 @@ function calcNoDiagonals(waypoints) {
return diagonals;
}
/**
* Estimate the travel distance between two points, as the crow flies. Most of the time, this is 1
* per space, but for a square grid using 5-10-5 diagonals, count each diagonal as an extra 0.5
*/
function estimateCost(pos, target) {
return Math.max(Math.abs(pos.x - target.x), Math.abs(pos.y - target.y));
const distX = Math.abs(pos.x - target.x);
const distY = Math.abs(pos.y - target.y);
return Math.max(distX, distY) + (use5105 ? Math.min(distX, distY) * 0.5 : 0);
}
function stepCollidesWithWall(from, to, token) {
const stepStart = getSnapPointForTokenObj(getPixelsFromGridPositionObj(from), token);
const stepEnd = getSnapPointForTokenObj(getPixelsFromGridPositionObj(to), token);
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
function stepCollidesWithWall(from, to, tokenData) {
const stepStart = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(from), tokenData);
const stepEnd = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(to), tokenData);
if (isModuleActive("levels")) {
stepStart.z = tokenData.elevation;
stepEnd.z = tokenData.elevation;
return _levels.testCollision(stepStart, stepEnd, "collision")
} else {
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
}
}
export function wipePathfindingCache() {
cachedNodes = undefined;
cache.clear();
for (const pathfinder of gridlessPathfinders.values()) {
GridlessPathfinding.free(pathfinder);
}
@@ -164,39 +460,34 @@ export function wipePathfindingCache() {
debugGraphics.removeChildren().forEach(c => c.destroy());
}
/**
* Check if the current cache is still suitable for the path we're about to find. If not, clear the cache
*/
function checkCacheValid(token) {
// If levels is enabled, the cache is invalid if it was made for a
if (game.modules.get("levels")?.active) {
const tokenElevation = token.data.elevation;
if (tokenElevation !== cacheElevation) {
cacheElevation = tokenElevation;
wipePathfindingCache();
}
}
}
export function initializePathfinding() {
gridWidth = Math.ceil(canvas.dimensions.width / canvas.grid.w);
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);
}
function paintGriddedPathfindingDebug(lastNode, token) {
export function startBackgroundCaching(token) {
// Background caching isn't yet supported for gridless scenes
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS)
return;
if (game.user.isGM || game.settings.get(settingsKey, "allowPathfinding")) {
cache.startBackgroundCaching(token);
}
}
function paintGriddedPathfindingDebug(firstNode, tokenData) {
if (!CONFIG.debug.dragRuler)
return;
debugGraphics.removeChildren().forEach(c => c.destroy());
let currentNode = lastNode;
let currentNode = firstNode;
while (currentNode) {
let text = new PIXI.Text(currentNode.cost.toFixed(0));
let pixels = getSnapPointForTokenObj(getPixelsFromGridPositionObj(currentNode.node), token);
let text = new PIXI.Text(currentNode.cost.toFixed(1));
let pixels = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(currentNode.node), tokenData);
text.anchor.set(0.5, 1.0);
text.x = pixels.x;
text.y = pixels.y;
debugGraphics.addChild(text);
currentNode = currentNode.previous;
currentNode = currentNode.next;
}
}
+11 -2
View File
@@ -1,5 +1,6 @@
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
import {SpeedProvider} from "./speed_provider.js"
import {wipePathfindingCache} from "./pathfinding.js"
import { early_isGM } from "./util.js";
export const settingsKey = "drag-ruler";
@@ -100,10 +101,18 @@ export function registerSettings() {
game.settings.register(settingsKey, "autoPathfinding", {
name: "drag-ruler.settings.autoPathfinding.name",
hint: "drag-ruler.settings.autoPathfinding.hint",
scpoe: "client",
scope: "client",
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
type: Boolean,
defualt: false,
default: false,
});
game.settings.register(settingsKey, "pathfindingRadius", {
scope: "world",
config: false,
type: Number,
default: 0.9,
onChange: wipePathfindingCache,
});
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
+52 -19
View File
@@ -1,5 +1,6 @@
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
import { disableSnap } from "./keybindings.js";
import {findVertexSnapPoint} from "./hex_support.js";
import {disableSnap} from "./keybindings.js";
export function* zip(it1, it2) {
for (let i = 0;i < Math.min(it1.length, it2.length);i++) {
@@ -25,14 +26,38 @@ export function sum(arr) {
return arr.reduce((a, b) => a + b, 0);
}
export function buildSnapPointTokenData(token) {
const tokenData = {
width: token.data.width,
height: token.data.height
};
if (isModuleActive("hex-size-support")) {
tokenData.hexSizeSupport = {};
tokenData.hexSizeSupport.altSnappingFlag = CONFIG.hexSizeSupport.getAltSnappingFlag(token);
tokenData.hexSizeSupport.altOrientationFlag = CONFIG.hexSizeSupport.getAltOrientationFlag(token);
tokenData.hexSizeSupport.borderSize = token.document.getFlag("hex-size-support", "borderSize");
}
return tokenData;
}
export function getSnapPointForToken(x, y, token) {
return getSnapPointForTokenData(x, y, buildSnapPointTokenData(token));
}
export function getSnapPointForTokenDataObj(pos, tokenData) {
return getSnapPointForTokenData(pos.x, pos.y, tokenData);
}
function getSnapPointForTokenData(x, y, tokenData) {
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
return new PIXI.Point(x, y);
}
if (canvas.grid.isHex) {
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
if (token.document.getFlag("hex-size-support", "borderSize") % 2 === 0) {
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
if (tokenData.hexSizeSupport?.altSnappingFlag) {
if (tokenData.hexSizeSupport.borderSize % 2 === 0) {
const snapPoint = findVertexSnapPoint(x, y, tokenData.hexSizeSupport.altOrientationFlag);
return new PIXI.Point(snapPoint.x, snapPoint.y)
}
else {
@@ -46,38 +71,38 @@ export function getSnapPointForToken(x, y, token) {
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
let cellX, cellY;
if (token.data.width % 2 === 0)
if (tokenData.width % 2 === 0)
cellX = x - canvas.grid.h / 2;
else
cellX = x;
if (token.data.height % 2 === 0)
if (tokenData.height % 2 === 0)
cellY = y - canvas.grid.h / 2;
else
cellY = y;
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
let snapX, snapY;
// Tiny tokens can snap to the cells corners
if (token.data.width <= 0.5) {
if (tokenData.width <= 0.5) {
const offsetX = x - topLeftX;
const subGridWidth = Math.floor(canvas.grid.w / 2);
const subGridPosX = Math.floor(offsetX / subGridWidth);
snapX = topLeftX + (subGridPosX + 0.5) * subGridWidth;
}
// Tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
else if (Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
else if (Math.round(tokenData.width) % 2 === 1 || tokenData.width < 1) {
snapX = centerX;
}
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
else {
snapX = centerX + canvas.grid.w / 2;
}
if (token.data.height <= 0.5) {
if (tokenData.height <= 0.5) {
const offsetY = y - topLeftY;
const subGridHeight = Math.floor(canvas.grid.h / 2);
const subGridPosY = Math.floor(offsetY / subGridHeight);
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
}
else if (Math.round(token.data.height) % 2 === 1 || token.data.height < 1) {
else if (Math.round(tokenData.height) % 2 === 1 || tokenData.height < 1) {
snapY = centerY;
}
else {
@@ -142,15 +167,19 @@ export function getAreaFromPositionAndShape(position, shape) {
}
export function getTokenShape(token) {
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
return getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
}
export function getTokenShapeForTokenData(tokenData, scene=canvas.scene) {
if (scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
return [{x: 0, y: 0}]
}
else if (token.scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
const topOffset = -Math.floor(token.data.height / 2)
const leftOffset = -Math.floor(token.data.width / 2)
else if (scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
const topOffset = -Math.floor(tokenData.height / 2)
const leftOffset = -Math.floor(tokenData.width / 2)
const shape = []
for (let y = 0;y < token.data.height;y++) {
for (let x = 0;x < token.data.width;x++) {
for (let y = 0;y < tokenData.height;y++) {
for (let x = 0;x < tokenData.width;x++) {
shape.push({x: x + leftOffset, y: y + topOffset})
}
}
@@ -158,8 +187,8 @@ export function getTokenShape(token) {
}
else {
// Hex grids
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
const borderSize = token.data.flags["hex-size-support"].borderSize;
if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
const borderSize = tokenData.hexSizeSupport.borderSize;
let shape = [{x: 0, y: 0}];
if (borderSize >= 2)
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
@@ -168,7 +197,7 @@ export function getTokenShape(token) {
if (borderSize >= 4)
shape = shape.concat([{x: -2, y: -1}, {x: 1, y: -1}, {x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}])
if (Boolean(CONFIG.hexSizeSupport.getAltOrientationFlag(token)) !== canvas.grid.grid.options.columns)
if (Boolean(tokenData.hexSizeSupport.altOrientationFlag) !== canvas.grid.grid.options.columns)
shape.forEach(space => space.y *= -1);
if (canvas.grid.grid.options.columns)
shape = shape.map(space => {return {x: space.y, y: space.x}});
@@ -263,3 +292,7 @@ export function early_isGM() {
const gmLevel = CONST.USER_ROLES.ASSISTANT;
return level >= gmLevel;
}
export function isModuleActive(moduleName) {
return game.modules.get(moduleName)?.active
}
+78
View File
@@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.9.1"
@@ -24,6 +33,45 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "cpufeatures"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "gridless-pathfinding"
version = "1.12.8"
@@ -31,6 +79,7 @@ dependencies = [
"console_error_panic_hook",
"js-sys",
"rustc-hash",
"sha1",
"wasm-bindgen",
]
@@ -49,6 +98,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
[[package]]
name = "log"
version = "0.4.14"
@@ -82,6 +137,17 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "sha1"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "syn"
version = "1.0.86"
@@ -93,12 +159,24 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasm-bindgen"
version = "0.2.79"
+1
View File
@@ -16,4 +16,5 @@ lto = true
console_error_panic_hook = "0.1.7"
js-sys = "0.3.56"
rustc-hash = "1.1.0"
sha1 = "0.10.1"
wasm-bindgen = "0.2.79"
+9
View File
@@ -1,4 +1,5 @@
use js_sys::Array;
use sha1::{Sha1, Digest};
use wasm_bindgen::prelude::*;
use crate::{
@@ -262,6 +263,14 @@ pub fn debug_get_pathfinding_points(pathfinder: &Pathfinder) -> Array {
.collect()
}
#[allow(dead_code)]
#[wasm_bindgen]
pub fn sha1(input: &str) -> String {
let mut hasher = Sha1::new();
hasher.update(input);
format!("{:x}", hasher.finalize())
}
trait IteratePath {
fn iter_path(&self) -> PathIterator;
}