Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7afb1e11de | |||
| 61c48dff5e | |||
| 3c9a86647e | |||
| c928f46f7c | |||
| 09e8ca79b3 | |||
| 833aced2be | |||
| 17d8db6c34 | |||
| 466e5a36d1 | |||
| 8e29cf186b | |||
| dd9a7ecf68 | |||
| 2bf52e62dd | |||
| c11899fb17 | |||
| e2afe05e4e | |||
| 8eb29a4dce | |||
| db7dd1c1c9 | |||
| 0ccfc6ef2f | |||
| f5b0e145ac | |||
| 78b1e8cc4e | |||
| 4d6543174a | |||
| b55af992ec | |||
| 0f288b65d9 | |||
| 9253f3decd | |||
| 188f6c15bf | |||
| 6273f7f8a7 | |||
| 6ce076fa43 | |||
| 1f434b3f6b | |||
| 1e14ff24b0 | |||
| cd61aa948e | |||
| 8817775201 | |||
| 673fa42a20 | |||
| 1ee406a047 | |||
| 08f41ed2ff | |||
| dc85609a75 | |||
| 472b373f13 | |||
| 57ed4d2cc5 | |||
| 81acdc3e63 | |||
| 64bce387d1 | |||
| 6a5c96e425 | |||
| 43c05745c8 | |||
| efc3eb871c | |||
| 8f134a49ba | |||
| e2c7fd992b | |||
| 2e06a2440c | |||
| 1c27f1a4a8 | |||
| 5e96be458e | |||
| b56d4fd14d | |||
| b822ada782 | |||
| b610a00f0f | |||
| fe3efd7fe4 | |||
| 5f971862e8 | |||
| b8c29de841 | |||
| a5b40382d7 | |||
| ce9bea14fc | |||
| a404353d38 | |||
| a132ac2bf3 | |||
| e3a785d8fe | |||
| 6e0571c565 | |||
| 4c006d34c9 | |||
| 9140ef3acf | |||
| 7e19fb95b0 | |||
| c9696f8725 | |||
| 54cebc3192 | |||
| 310014bb8a | |||
| d53399fa1e | |||
| 369159e6bb |
+1
-3
@@ -1,5 +1,3 @@
|
|||||||
/foundry*.js
|
foundry-*.js
|
||||||
artifact/
|
artifact/
|
||||||
wasm/
|
wasm/
|
||||||
*.lock
|
|
||||||
.vscode/
|
|
||||||
|
|||||||
@@ -1,58 +1,3 @@
|
|||||||
## 1.12.8
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that could cause grid cells to be highlighted in the wrong color
|
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
- Fixed an interaction with the Wall Height module that could allow players to walk through walls
|
|
||||||
|
|
||||||
|
|
||||||
## 1.12.7
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that caused measurement templates to only snap to the corners of the grid (this was a regression introduced in 1.12.5)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.12.6
|
|
||||||
### Bugfixes
|
|
||||||
- Measured templates no longer snap to a virtual grid on gridless scenes (this was a regression introduced in 1.12.5)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.12.5
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler is now compatible with DF Template Enhancements
|
|
||||||
|
|
||||||
|
|
||||||
## 1.12.4
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that could cause Drag Ruler to override the default ruler color on other player's clients
|
|
||||||
|
|
||||||
|
|
||||||
## 1.12.3
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug that could cause foundry to freeze indefinitely when trying to pathfind to an unreachalbe location (thanks to JDCalvert)
|
|
||||||
- Fixed a bug that caused the pathfinder to route through one-directional walls from the wrong direction (thanks to JDCalvert)
|
|
||||||
- Fixed a bug that could cause Drag Ruler to write errors into the JS console during regular usage
|
|
||||||
|
|
||||||
### Compatibility
|
|
||||||
- Drag Ruler's generic speed provider is now aware of good defaults for DnD 4th Edition
|
|
||||||
- Drag Ruler's pathfinder should now be compatible with the Wall Height and Levels modules (thanks to JDCalvert)
|
|
||||||
|
|
||||||
|
|
||||||
## 1.12.2
|
|
||||||
### Bugfixes
|
|
||||||
- Fixed a bug where the pathfinder on gridless scenes sometimes wasn't able to find a way around corners with specific angles
|
|
||||||
- Pathfinding will now be disabled when the hotkey to move tokens without animation is being pressed, to allow GMs to move their tokens through walls
|
|
||||||
|
|
||||||
|
|
||||||
## 1.12.1
|
|
||||||
### Hotfix
|
|
||||||
- Version 1.12.0 was incorrectly packaged, which caused it to fail to load
|
|
||||||
|
|
||||||
|
|
||||||
## 1.12.0
|
|
||||||
### New features
|
|
||||||
- Pathfinding is now supported on gridless scenes
|
|
||||||
|
|
||||||
|
|
||||||
## 1.11.5
|
## 1.11.5
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Fixed a bug that was causing Drag Ruler to spam useless warnings into the console (this was a regression introduced in 1.11.4)
|
- Fixed a bug that was causing Drag Ruler to spam useless warnings into the console (this was a regression introduced in 1.11.4)
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ wasm_pack = Path("~/.cargo/bin/wasm-pack").expanduser()
|
|||||||
root_files = ["module.json", "README.md", "CHANGELOG.md", "LICENSE"]
|
root_files = ["module.json", "README.md", "CHANGELOG.md", "LICENSE"]
|
||||||
wasm_files = ["gridless_pathfinding_bg.wasm", "gridless_pathfinding.js"]
|
wasm_files = ["gridless_pathfinding_bg.wasm", "gridless_pathfinding.js"]
|
||||||
output_dir = Path("artifact")
|
output_dir = Path("artifact")
|
||||||
copy_everything_directories = ["js", "lang", "templates"]
|
copy_everything_directories = ["js", "lang", "templates", "wasm/snippets"]
|
||||||
wasm_dir = Path("wasm")
|
wasm_dir = Path("wasm")
|
||||||
root_dir = Path(".")
|
root_dir = Path(".")
|
||||||
rust_dir = Path("rust")
|
rust_dir = Path("rust")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {measureDistances} from "./compatibility.js";
|
|||||||
import {getMovementHistory} from "./movement_tracking.js";
|
import {getMovementHistory} from "./movement_tracking.js";
|
||||||
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
|
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
|
||||||
import {settingsKey} from "./settings.js"
|
import {settingsKey} from "./settings.js"
|
||||||
import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
|
import {getTokenShape} from "./util.js";
|
||||||
|
|
||||||
export const availableSpeedProviders = {}
|
export const availableSpeedProviders = {}
|
||||||
export let currentSpeedProvider = undefined
|
export let currentSpeedProvider = undefined
|
||||||
@@ -140,10 +140,6 @@ export function getMovedDistanceFromToken(token) {
|
|||||||
return distances.reduce((acc, val) => acc + val, 0);
|
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) {
|
export function registerModule(moduleId, speedProvider) {
|
||||||
// Check if a module with the given id exists and is currently enabled
|
// Check if a module with the given id exists and is currently enabled
|
||||||
const module = game.modules.get(moduleId)
|
const module = game.modules.get(moduleId)
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
import {buildCostFunction} from "./api.js";
|
import {getCostFromSpeedProvider} from "./api.js";
|
||||||
import {settingsKey} from "./settings.js";
|
import {settingsKey} from "./settings.js";
|
||||||
import {highlightTokenShape} from "./util.js";
|
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
|
||||||
|
|
||||||
export function getHexSizeSupportTokenGridCenter(token) {
|
export function getHexSizeSupportTokenGridCenter(token) {
|
||||||
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
|
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
|
||||||
@@ -27,7 +27,7 @@ export function measureDistances(segments, entity, shape, options={}) {
|
|||||||
const newSegments = segments.slice(firstNewSegmentIndex);
|
const newSegments = segments.slice(firstNewSegmentIndex);
|
||||||
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
|
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
|
||||||
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
|
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
|
||||||
opts.costFunction = buildCostFunction(entity, shape);
|
opts.costFunction = (x, y, costOptions={}) => { return getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), costOptions); }
|
||||||
if (previousSegments.length > 0)
|
if (previousSegments.length > 0)
|
||||||
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
||||||
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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
-10
@@ -28,9 +28,6 @@ export async function moveEntities(draggedEntity, selectedEntities) {
|
|||||||
const hasCollision = selectedEntities.some(token => {
|
const hasCollision = selectedEntities.some(token => {
|
||||||
const offset = calculateEntityOffset(token, draggedEntity);
|
const offset = calculateEntityOffset(token, draggedEntity);
|
||||||
const offsetRays = rays.filter(ray => !ray.isPrevious).map(ray => applyOffsetToRay(ray, offset))
|
const offsetRays = rays.filter(ray => !ray.isPrevious).map(ray => applyOffsetToRay(ray, offset))
|
||||||
if (window.WallHeight) {
|
|
||||||
window.WallHeight.addBoundsToRays(offsetRays, draggedEntity);
|
|
||||||
}
|
|
||||||
return offsetRays.some(r => canvas.walls.checkCollision(r));
|
return offsetRays.some(r => canvas.walls.checkCollision(r));
|
||||||
})
|
})
|
||||||
if (hasCollision) {
|
if (hasCollision) {
|
||||||
@@ -175,9 +172,7 @@ export function measure(destination, options={}) {
|
|||||||
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
||||||
const to = getGridPositionFromPixelsObj(destination);
|
const to = getGridPositionFromPixelsObj(destination);
|
||||||
let path = findPath(from, to, this.draggedEntity, this.waypoints);
|
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));
|
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
|
// If the token is snapped to the grid, the first point of the path is already handled by the ruler
|
||||||
@@ -346,8 +341,7 @@ export function highlightMeasurementNative(ray, previousSegments, tokenShape=[{x
|
|||||||
if ( x0 === x1 && y0 === y1 ) continue;
|
if ( x0 === x1 && y0 === y1 ) continue;
|
||||||
|
|
||||||
// Highlight the grid position
|
// Highlight the grid position
|
||||||
let [xgtl, ygtl] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
|
let [xg, yg] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
|
||||||
let [xg, yg] = canvas.grid.grid.getCenter(xgtl, ygtl);
|
|
||||||
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xg, y: yg})}]);
|
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xg, y: yg})}]);
|
||||||
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
||||||
const color = this.dragRulerGetColorForDistance(distance);
|
const color = this.dragRulerGetColorForDistance(distance);
|
||||||
@@ -361,8 +355,7 @@ export function highlightMeasurementNative(ray, previousSegments, tokenShape=[{x
|
|||||||
let th = tMax[i - 1] - (0.5 / nMax);
|
let th = tMax[i - 1] - (0.5 / nMax);
|
||||||
let {x, y} = ray.project(th);
|
let {x, y} = ray.project(th);
|
||||||
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
||||||
let [xghtl, yghtl] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
|
let [xgh, ygh] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
|
||||||
let [xgh, ygh] = canvas.grid.grid.getCenter(xghtl, yghtl);
|
|
||||||
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xgh, y: ygh})}]);
|
const pathUntilSpace = previousSegments.concat([{ray: new Ray(ray.A, {x: xgh, y: ygh})}]);
|
||||||
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
const distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
||||||
const color = this.dragRulerGetColorForDistance(distance);
|
const color = this.dragRulerGetColorForDistance(distance);
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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}
|
|
||||||
}
|
|
||||||
@@ -113,16 +113,6 @@ function handleDisableSnap(event) {
|
|||||||
|
|
||||||
function handleMoveWithoutAnimation(event) {
|
function handleMoveWithoutAnimation(event) {
|
||||||
moveWithoutAnimation = !event.up;
|
moveWithoutAnimation = !event.up;
|
||||||
|
|
||||||
const ruler = canvas.controls.ruler;
|
|
||||||
if (!ruler?.isDragRuler)
|
|
||||||
return false;
|
|
||||||
if (ruler._state !== Ruler.STATES.MEASURING)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
|
||||||
ruler.dragRulerSendState();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTogglePathfinding(event) {
|
function handleTogglePathfinding(event) {
|
||||||
|
|||||||
+2
-23
@@ -7,7 +7,7 @@ import {disableSnap, registerKeybindings} from "./keybindings.js";
|
|||||||
import {libWrapper} from "./libwrapper_shim.js";
|
import {libWrapper} from "./libwrapper_shim.js";
|
||||||
import {performMigrations} from "./migration.js"
|
import {performMigrations} from "./migration.js"
|
||||||
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
|
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
|
||||||
import {wipePathfindingCache, initializePathfinding, startBackgroundCaching, terrainRulerWrapper} from "./pathfinding.js";
|
import {wipePathfindingCache} from "./pathfinding.js";
|
||||||
import {extendRuler} from "./ruler.js";
|
import {extendRuler} from "./ruler.js";
|
||||||
import {registerSettings, RightClickAction, settingsKey} from "./settings.js"
|
import {registerSettings, RightClickAction, settingsKey} from "./settings.js"
|
||||||
import {recalculate} from "./socket.js";
|
import {recalculate} from "./socket.js";
|
||||||
@@ -21,28 +21,10 @@ export let debugGraphics = undefined;
|
|||||||
|
|
||||||
initGridlessPathfinding().then(() => {
|
initGridlessPathfinding().then(() => {
|
||||||
Hooks.on("canvasInit", wipePathfindingCache);
|
Hooks.on("canvasInit", wipePathfindingCache);
|
||||||
Hooks.on("canvasReady", () => {
|
Hooks.on("canvasReady", wipePathfindingCache);
|
||||||
wipePathfindingCache();
|
|
||||||
initializePathfinding();
|
|
||||||
});
|
|
||||||
Hooks.on("createWall", wipePathfindingCache);
|
Hooks.on("createWall", wipePathfindingCache);
|
||||||
Hooks.on("updateWall", wipePathfindingCache);
|
Hooks.on("updateWall", wipePathfindingCache);
|
||||||
Hooks.on("deleteWall", 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", () => {
|
Hooks.once("init", () => {
|
||||||
@@ -62,9 +44,6 @@ Hooks.once("init", () => {
|
|||||||
registerSystem,
|
registerSystem,
|
||||||
recalculate,
|
recalculate,
|
||||||
resetMovementHistory,
|
resetMovementHistory,
|
||||||
private: {
|
|
||||||
terrainRulerWrapper
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+84
-411
@@ -1,249 +1,20 @@
|
|||||||
import {getCenterFromGridPositionObj, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
||||||
import {moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
|
import {togglePathfinding} from "./keybindings.js";
|
||||||
import {debugGraphics} from "./main.js";
|
import {debugGraphics} from "./main.js";
|
||||||
import {settingsKey} from "./settings.js";
|
import {settingsKey} from "./settings.js";
|
||||||
import {buildSnapPointTokenData, getSnapPointForTokenDataObj, getTokenShape, getTokenShapeForTokenData, isModuleActive, iterPairs} from "./util.js";
|
import {getSnapPointForTokenObj, 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 use5105 = false;
|
let use5105 = false;
|
||||||
let gridlessPathfinders = new Map();
|
let gridlessPathfinders = new Map();
|
||||||
let gridWidth, gridHeight;
|
|
||||||
|
|
||||||
export function isPathfindingEnabled() {
|
export function isPathfindingEnabled() {
|
||||||
if (this.user !== game.user)
|
if (this.user !== game.user)
|
||||||
return false;
|
return false;
|
||||||
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding"))
|
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding"))
|
||||||
return false;
|
return false;
|
||||||
if (moveWithoutAnimation)
|
|
||||||
return false;
|
|
||||||
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
|
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,113 +23,74 @@ export function findPath(from, to, token, previousWaypoints) {
|
|||||||
let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size;
|
let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size;
|
||||||
let pathfinder = gridlessPathfinders.get(tokenSize);
|
let pathfinder = gridlessPathfinders.get(tokenSize);
|
||||||
if (!pathfinder) {
|
if (!pathfinder) {
|
||||||
let radiusMultiplier = game.settings.get(settingsKey, "pathfindingRadius");
|
pathfinder = GridlessPathfinding.initialize(canvas.walls.placeables, tokenSize);
|
||||||
// TODO Pass proper options to listAllTerrain
|
|
||||||
// TODO Wipe caches when they become invalid
|
|
||||||
// TODO Multiple caches, analog to gridded pathfinding
|
|
||||||
pathfinder = GridlessPathfinding.initialize(canvas.walls.placeables, canvas.terrain.listAllTerrain(), tokenSize * radiusMultiplier, token.data.elevation, Boolean(game.modules.get("wall-height")?.active));
|
|
||||||
gridlessPathfinders.set(tokenSize, pathfinder);
|
gridlessPathfinders.set(tokenSize, pathfinder);
|
||||||
}
|
}
|
||||||
paintGridlessPathfindingDebug(pathfinder);
|
paintGridlessPathfindingDebug(pathfinder);
|
||||||
return GridlessPathfinding.findPath(pathfinder, from, to);
|
return GridlessPathfinding.findPath(pathfinder, from, to);
|
||||||
} else {
|
|
||||||
const cacheLayer = cache.getCacheLayer(token);
|
|
||||||
const firstNode = calculatePath(from, to, cacheLayer, previousWaypoints);
|
|
||||||
if (!firstNode)
|
|
||||||
return null;
|
|
||||||
paintGriddedPathfindingDebug(firstNode, cacheLayer.tokenData);
|
|
||||||
const path = [];
|
|
||||||
let currentNode = firstNode;
|
|
||||||
while (currentNode) {
|
|
||||||
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 {
|
else {
|
||||||
path.pop();
|
const lastNode = calculatePath(from, to, token, previousWaypoints);
|
||||||
}
|
if (!lastNode)
|
||||||
}
|
return null;
|
||||||
|
paintGriddedPathfindingDebug(lastNode, token);
|
||||||
|
const path = [];
|
||||||
|
let currentNode = lastNode;
|
||||||
|
while (currentNode) {
|
||||||
|
// TODO Check if the distance doesn't change
|
||||||
|
if (path.length >= 2 && !stepCollidesWithWall(currentNode.node, path[path.length - 2], 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});
|
path.push({x: currentNode.node.x, y: currentNode.node.y});
|
||||||
currentNode = currentNode.next;
|
currentNode = currentNode.previous;
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function terrainRulerWrapper(from, to) {
|
export function wipePathfindingCache() {
|
||||||
// TODO Send list of terrain to terrain layer
|
cachedNodes = undefined;
|
||||||
const ray = new Ray(from, to);
|
for (const pathfinder of gridlessPathfinders.values()) {
|
||||||
return terrainRuler.measureDistances([{ray}])[0] / canvas.dimensions.distance * canvas.dimensions.size;
|
GridlessPathfinding.free(pathfinder);
|
||||||
|
}
|
||||||
|
gridlessPathfinders.clear();
|
||||||
|
if (debugGraphics)
|
||||||
|
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTokenData(token) {
|
function getNode(pos, token, initialize=true) {
|
||||||
// Almost all the information we need is for calculating the snap point
|
pos = {layer: 0, ...pos}; // Copy pos and set pos.layer to the default value if it's unset
|
||||||
const tokenData = buildSnapPointTokenData(token);
|
if (!cachedNodes)
|
||||||
|
cachedNodes = new Array(2);
|
||||||
// If Wall Height is enabled, which walls matter depends on the token's elevation.
|
if (!cachedNodes[pos.layer])
|
||||||
// Depending on the settings in Wall Height, the height we care about is either their
|
cachedNodes[pos.layer] = new Array(Math.ceil(canvas.dimensions.height / canvas.grid.h));
|
||||||
// foot height (elevation) or eye height (losHeight).
|
if (!cachedNodes[pos.layer][pos.y])
|
||||||
if (isModuleActive("wall-height")) {
|
cachedNodes[pos.layer][pos.y] = new Array(Math.ceil(canvas.dimensions.width / canvas.grid.w));
|
||||||
const blockSightMovement = game.settings.get("wall-height", "blockSightMovement");
|
if (!cachedNodes[pos.layer][pos.y][pos.x]) {
|
||||||
tokenData.elevation = blockSightMovement ? token.losHeight : token.data.elevation;
|
cachedNodes[pos.layer][pos.y][pos.x] = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokenData;
|
const node = cachedNodes[pos.layer][pos.y][pos.x];
|
||||||
}
|
|
||||||
|
|
||||||
function getNode(pos, cacheLayer, initialize = true) {
|
|
||||||
const node = cacheLayer.nodes[pos.y][pos.x];
|
|
||||||
if (initialize && !node.edges) {
|
if (initialize && !node.edges) {
|
||||||
node.edges = [];
|
node.edges = [];
|
||||||
for (const neighborPos of canvas.grid.grid.getNeighbors(pos.y, pos.x).map(([y, x]) => {return {x, y};})) {
|
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)
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Work with pixels instead of grid locations
|
// TODO Work with pixels instead of grid locations
|
||||||
if (!stepCollidesWithWall(pos, neighborPos, cacheLayer.tokenData)) {
|
if (!stepCollidesWithWall(pos, neighborPos, token)) {
|
||||||
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
|
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
|
||||||
let edgeCost;
|
let targetLayer = pos.layer;
|
||||||
if (window.terrainRuler) {
|
if (use5105 && isDiagonal)
|
||||||
let ray = new Ray(getCenterFromGridPositionObj(pos), getCenterFromGridPositionObj(neighborPos));
|
targetLayer = 1 - targetLayer;
|
||||||
let measuredDistance = terrainRuler.measureDistances([{ray}], {costFunction: buildCostFunction(cacheLayer.tokenData, getTokenShapeForTokenData(cacheLayer.tokenData))})[0];
|
const neighbor = getNode({...neighborPos, layer: targetLayer}, token, false);
|
||||||
edgeCost = Math.round(measuredDistance / canvas.dimensions.distance);
|
|
||||||
if (ray.terrainRulerFinalState?.noDiagonals === 1) {
|
// TODO We currently assume a cost of one or two for all transitions. Change this for difficult terrain support
|
||||||
edgeCost = 1.5;
|
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;
|
||||||
}
|
}
|
||||||
// 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});
|
node.edges.push({target: neighbor, cost: edgeCost});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,67 +98,44 @@ function getNode(pos, cacheLayer, initialize = true) {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculatePath(from, to, cacheLayer, previousWaypoints) {
|
function calculatePath(from, to, token, previousWaypoints) {
|
||||||
use5105 = game.system.id === "pf2e" || canvas.grid.diagonalRule === "5105";
|
if (game.system.id === "pf2e")
|
||||||
let startCost = 0;
|
use5105 = true;
|
||||||
|
if (canvas.grid.diagonalRule === "5105")
|
||||||
|
use5105 = true;
|
||||||
|
let startLayer = 0;
|
||||||
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
|
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
|
||||||
previousWaypoints = previousWaypoints.map(w => getGridPositionFromPixelsObj(w));
|
previousWaypoints = previousWaypoints.map(w => getGridPositionFromPixelsObj(w));
|
||||||
startCost = (calcNoDiagonals(previousWaypoints) % 2) * 0.5;
|
startLayer = calcNoDiagonals(previousWaypoints) % 2;
|
||||||
}
|
}
|
||||||
|
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();
|
const previousNodes = new Set();
|
||||||
|
while (nextNodes.length > 0) {
|
||||||
nextNodes.pushWithPriority(
|
// Sort by estimated cost, high to low
|
||||||
{
|
// TODO Re-sorting every iteration is expensive. Think of something better
|
||||||
node: getNode(from, cacheLayer),
|
nextNodes.sort((a, b) => b.estimated - a.estimated);
|
||||||
cost: startCost,
|
|
||||||
estimated: startCost + estimateCost(from, to),
|
|
||||||
previous: null
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
while (nextNodes.hasNext()) {
|
|
||||||
// Get node with cheapest estimate
|
// Get node with cheapest estimate
|
||||||
const currentNode = nextNodes.pop();
|
const currentNode = nextNodes.pop();
|
||||||
if (currentNode.node.x === to.x && currentNode.node.y === to.y) {
|
if (currentNode.node.x === from.x && currentNode.node.y === from.y)
|
||||||
return buildPathNodes(currentNode);
|
return currentNode;
|
||||||
}
|
|
||||||
previousNodes.add(currentNode.node);
|
previousNodes.add(currentNode.node);
|
||||||
for (const edge of currentNode.node.edges) {
|
for (const edge of currentNode.node.edges) {
|
||||||
const neighborNode = getNode(edge.target, cacheLayer);
|
const neighborNode = getNode(edge.target, token);
|
||||||
if (previousNodes.has(neighborNode)) {
|
if (previousNodes.has(neighborNode))
|
||||||
continue;
|
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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const neighbor = {
|
else {
|
||||||
node: neighborNode,
|
nextNodes.push(neighbor);
|
||||||
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) {
|
function calcNoDiagonals(waypoints) {
|
||||||
@@ -437,66 +146,30 @@ function calcNoDiagonals(waypoints) {
|
|||||||
return diagonals;
|
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) {
|
function estimateCost(pos, target) {
|
||||||
const distX = Math.abs(pos.x - target.x);
|
return Math.max(Math.abs(pos.x - target.x), Math.abs(pos.y - target.y));
|
||||||
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, tokenData) {
|
function stepCollidesWithWall(from, to, token) {
|
||||||
const stepStart = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(from), tokenData);
|
const stepStart = getSnapPointForTokenObj(getPixelsFromGridPositionObj(from), token);
|
||||||
const stepEnd = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(to), tokenData);
|
const stepEnd = getSnapPointForTokenObj(getPixelsFromGridPositionObj(to), token);
|
||||||
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));
|
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function wipePathfindingCache() {
|
function paintGriddedPathfindingDebug(lastNode, token) {
|
||||||
cache.clear();
|
|
||||||
for (const pathfinder of gridlessPathfinders.values()) {
|
|
||||||
GridlessPathfinding.free(pathfinder);
|
|
||||||
}
|
|
||||||
gridlessPathfinders.clear();
|
|
||||||
if (debugGraphics)
|
|
||||||
debugGraphics.removeChildren().forEach(c => c.destroy());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initializePathfinding() {
|
|
||||||
gridWidth = Math.ceil(canvas.dimensions.width / canvas.grid.w);
|
|
||||||
gridHeight = Math.ceil(canvas.dimensions.height / canvas.grid.h);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
if (!CONFIG.debug.dragRuler)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
debugGraphics.removeChildren().forEach(c => c.destroy());
|
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||||
let currentNode = firstNode;
|
let currentNode = lastNode;
|
||||||
while (currentNode) {
|
while (currentNode) {
|
||||||
let text = new PIXI.Text(currentNode.cost.toFixed(1));
|
let text = new PIXI.Text(currentNode.cost.toFixed(0));
|
||||||
let pixels = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(currentNode.node), tokenData);
|
let pixels = getSnapPointForTokenObj(getPixelsFromGridPositionObj(currentNode.node), token);
|
||||||
text.anchor.set(0.5, 1.0);
|
text.anchor.set(0.5, 1.0);
|
||||||
text.x = pixels.x;
|
text.x = pixels.x;
|
||||||
text.y = pixels.y;
|
text.y = pixels.y;
|
||||||
debugGraphics.addChild(text);
|
debugGraphics.addChild(text);
|
||||||
currentNode = currentNode.next;
|
currentNode = currentNode.previous;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ export function extendRuler() {
|
|||||||
else
|
else
|
||||||
this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
this.draggedEntity = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
super.update(data);
|
super.update(data);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-16
@@ -1,6 +1,5 @@
|
|||||||
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
|
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
|
||||||
import {SpeedProvider} from "./speed_provider.js"
|
import {SpeedProvider} from "./speed_provider.js"
|
||||||
import {wipePathfindingCache} from "./pathfinding.js"
|
|
||||||
import { early_isGM } from "./util.js";
|
import { early_isGM } from "./util.js";
|
||||||
|
|
||||||
export const settingsKey = "drag-ruler";
|
export const settingsKey = "drag-ruler";
|
||||||
@@ -11,10 +10,6 @@ export const RightClickAction = Object.freeze({
|
|||||||
ABORT_DRAG:2,
|
ABORT_DRAG:2,
|
||||||
});
|
});
|
||||||
|
|
||||||
function delayedReload() {
|
|
||||||
window.setTimeout(() => location.reload(), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerSettings() {
|
export function registerSettings() {
|
||||||
game.settings.register(settingsKey, "dataVersion", {
|
game.settings.register(settingsKey, "dataVersion", {
|
||||||
scope: "world",
|
scope: "world",
|
||||||
@@ -95,24 +90,16 @@ export function registerSettings() {
|
|||||||
config: true,
|
config: true,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
onChange: delayedReload,
|
onChange: () => location.reload(),
|
||||||
});
|
});
|
||||||
|
|
||||||
game.settings.register(settingsKey, "autoPathfinding", {
|
game.settings.register(settingsKey, "autoPathfinding", {
|
||||||
name: "drag-ruler.settings.autoPathfinding.name",
|
name: "drag-ruler.settings.autoPathfinding.name",
|
||||||
hint: "drag-ruler.settings.autoPathfinding.hint",
|
hint: "drag-ruler.settings.autoPathfinding.hint",
|
||||||
scope: "client",
|
scpoe: "client",
|
||||||
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
|
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
defualt: false,
|
||||||
});
|
|
||||||
|
|
||||||
game.settings.register(settingsKey, "pathfindingRadius", {
|
|
||||||
scope: "world",
|
|
||||||
config: false,
|
|
||||||
type: Number,
|
|
||||||
default: 0.9,
|
|
||||||
onChange: wipePathfindingCache,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
|
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
|
||||||
|
|||||||
+1
-3
@@ -39,7 +39,5 @@ export function recalculate(tokens) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _socketRecalculate(tokenIds) {
|
function _socketRecalculate(tokenIds) {
|
||||||
const ruler = canvas.controls.ruler;
|
return canvas.controls.ruler.dragRulerRecalculate(tokenIds);
|
||||||
if (ruler.isDragRuler)
|
|
||||||
ruler.dragRulerRecalculate(tokenIds);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-8
@@ -4,15 +4,13 @@ export function getDefaultSpeedAttribute() {
|
|||||||
return "actor.data.data.attribs.mov.value";
|
return "actor.data.data.attribs.mov.value";
|
||||||
case "dcc":
|
case "dcc":
|
||||||
return "actor.data.data.attributes.speed.value";
|
return "actor.data.data.attributes.speed.value";
|
||||||
case "dnd4e":
|
|
||||||
return "actor.data.data.movement.walk.value";
|
|
||||||
case "dnd5e":
|
case "dnd5e":
|
||||||
return "actor.data.data.attributes.movement.walk";
|
return "actor.data.data.attributes.movement.walk"
|
||||||
case "lancer":
|
case "lancer":
|
||||||
return "actor.data.data.derived.speed";
|
return "actor.data.data.derived.speed"
|
||||||
case "pf1":
|
case "pf1":
|
||||||
case "D35E":
|
case "D35E":
|
||||||
return "actor.data.data.attributes.speed.land.total";
|
return "actor.data.data.attributes.speed.land.total"
|
||||||
case "sfrpg":
|
case "sfrpg":
|
||||||
return "actor.data.data.attributes.speed.value";
|
return "actor.data.data.attributes.speed.value";
|
||||||
case "shadowrun5e":
|
case "shadowrun5e":
|
||||||
@@ -30,7 +28,6 @@ export function getDefaultDashMultiplier() {
|
|||||||
case "swade":
|
case "swade":
|
||||||
return 0
|
return 0
|
||||||
case "dcc":
|
case "dcc":
|
||||||
case "dnd4e":
|
|
||||||
case "dnd5e":
|
case "dnd5e":
|
||||||
case "lancer":
|
case "lancer":
|
||||||
case "pf1":
|
case "pf1":
|
||||||
@@ -38,9 +35,9 @@ export function getDefaultDashMultiplier() {
|
|||||||
case "sfrpg":
|
case "sfrpg":
|
||||||
case "shadowrun5e":
|
case "shadowrun5e":
|
||||||
case "ds4":
|
case "ds4":
|
||||||
return 2;
|
return 2
|
||||||
case "CoC7":
|
case "CoC7":
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-52
@@ -1,5 +1,4 @@
|
|||||||
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
|
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
|
||||||
import {findVertexSnapPoint} from "./hex_support.js";
|
|
||||||
import { disableSnap } from "./keybindings.js";
|
import { disableSnap } from "./keybindings.js";
|
||||||
|
|
||||||
export function* zip(it1, it2) {
|
export function* zip(it1, it2) {
|
||||||
@@ -26,38 +25,14 @@ export function sum(arr) {
|
|||||||
return arr.reduce((a, b) => a + b, 0);
|
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) {
|
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) {
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
return new PIXI.Point(x, y);
|
return new PIXI.Point(x, y);
|
||||||
}
|
}
|
||||||
if (canvas.grid.isHex) {
|
if (canvas.grid.isHex) {
|
||||||
if (tokenData.hexSizeSupport?.altSnappingFlag) {
|
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
||||||
if (tokenData.hexSizeSupport.borderSize % 2 === 0) {
|
if (token.document.getFlag("hex-size-support", "borderSize") % 2 === 0) {
|
||||||
const snapPoint = findVertexSnapPoint(x, y, tokenData.hexSizeSupport.altOrientationFlag);
|
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
|
||||||
return new PIXI.Point(snapPoint.x, snapPoint.y)
|
return new PIXI.Point(snapPoint.x, snapPoint.y)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -71,38 +46,38 @@ function getSnapPointForTokenData(x, y, tokenData) {
|
|||||||
|
|
||||||
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
||||||
let cellX, cellY;
|
let cellX, cellY;
|
||||||
if (tokenData.width % 2 === 0)
|
if (token.data.width % 2 === 0)
|
||||||
cellX = x - canvas.grid.h / 2;
|
cellX = x - canvas.grid.h / 2;
|
||||||
else
|
else
|
||||||
cellX = x;
|
cellX = x;
|
||||||
if (tokenData.height % 2 === 0)
|
if (token.data.height % 2 === 0)
|
||||||
cellY = y - canvas.grid.h / 2;
|
cellY = y - canvas.grid.h / 2;
|
||||||
else
|
else
|
||||||
cellY = y;
|
cellY = y;
|
||||||
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
|
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
|
||||||
let snapX, snapY;
|
let snapX, snapY;
|
||||||
// Tiny tokens can snap to the cells corners
|
// Tiny tokens can snap to the cells corners
|
||||||
if (tokenData.width <= 0.5) {
|
if (token.data.width <= 0.5) {
|
||||||
const offsetX = x - topLeftX;
|
const offsetX = x - topLeftX;
|
||||||
const subGridWidth = Math.floor(canvas.grid.w / 2);
|
const subGridWidth = Math.floor(canvas.grid.w / 2);
|
||||||
const subGridPosX = Math.floor(offsetX / subGridWidth);
|
const subGridPosX = Math.floor(offsetX / subGridWidth);
|
||||||
snapX = topLeftX + (subGridPosX + 0.5) * 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
|
// 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(tokenData.width) % 2 === 1 || tokenData.width < 1) {
|
else if (Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
|
||||||
snapX = centerX;
|
snapX = centerX;
|
||||||
}
|
}
|
||||||
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
|
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
|
||||||
else {
|
else {
|
||||||
snapX = centerX + canvas.grid.w / 2;
|
snapX = centerX + canvas.grid.w / 2;
|
||||||
}
|
}
|
||||||
if (tokenData.height <= 0.5) {
|
if (token.data.height <= 0.5) {
|
||||||
const offsetY = y - topLeftY;
|
const offsetY = y - topLeftY;
|
||||||
const subGridHeight = Math.floor(canvas.grid.h / 2);
|
const subGridHeight = Math.floor(canvas.grid.h / 2);
|
||||||
const subGridPosY = Math.floor(offsetY / subGridHeight);
|
const subGridPosY = Math.floor(offsetY / subGridHeight);
|
||||||
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
|
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
|
||||||
}
|
}
|
||||||
else if (Math.round(tokenData.height) % 2 === 1 || tokenData.height < 1) {
|
else if (Math.round(token.data.height) % 2 === 1 || token.data.height < 1) {
|
||||||
snapY = centerY;
|
snapY = centerY;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -119,7 +94,23 @@ export function getSnapPointForMeasuredTemplate(x, y) {
|
|||||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
return new PIXI.Point(x, y);
|
return new PIXI.Point(x, y);
|
||||||
}
|
}
|
||||||
return canvas.grid.grid.getSnappedPosition(x, y, canvas.templates.gridPrecision);
|
let subgridWidth, subgridHeight;
|
||||||
|
if (canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
|
||||||
|
subgridWidth = subgridHeight = canvas.dimensions.size / 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (canvas.grid.grid.columns) {
|
||||||
|
subgridWidth = canvas.grid.w / 4;
|
||||||
|
subgridHeight = canvas.grid.h / 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
subgridWidth = canvas.grid.w / 2;
|
||||||
|
subgridHeight = canvas.grid.h / 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const snappedX = Math.round(x / subgridWidth) * subgridWidth;
|
||||||
|
const snappedY = Math.round(y / subgridHeight) * subgridHeight;
|
||||||
|
return new PIXI.Point(snappedX, snappedY);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSnapPointForEntity(x, y, entity) {
|
export function getSnapPointForEntity(x, y, entity) {
|
||||||
@@ -167,19 +158,15 @@ export function getAreaFromPositionAndShape(position, shape) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getTokenShape(token) {
|
export function getTokenShape(token) {
|
||||||
return getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
|
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
||||||
}
|
|
||||||
|
|
||||||
export function getTokenShapeForTokenData(tokenData, scene=canvas.scene) {
|
|
||||||
if (scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
|
||||||
return [{x: 0, y: 0}]
|
return [{x: 0, y: 0}]
|
||||||
}
|
}
|
||||||
else if (scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
|
else if (token.scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
|
||||||
const topOffset = -Math.floor(tokenData.height / 2)
|
const topOffset = -Math.floor(token.data.height / 2)
|
||||||
const leftOffset = -Math.floor(tokenData.width / 2)
|
const leftOffset = -Math.floor(token.data.width / 2)
|
||||||
const shape = []
|
const shape = []
|
||||||
for (let y = 0;y < tokenData.height;y++) {
|
for (let y = 0;y < token.data.height;y++) {
|
||||||
for (let x = 0;x < tokenData.width;x++) {
|
for (let x = 0;x < token.data.width;x++) {
|
||||||
shape.push({x: x + leftOffset, y: y + topOffset})
|
shape.push({x: x + leftOffset, y: y + topOffset})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,8 +174,8 @@ export function getTokenShapeForTokenData(tokenData, scene=canvas.scene) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Hex grids
|
// Hex grids
|
||||||
if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
|
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
||||||
const borderSize = tokenData.hexSizeSupport.borderSize;
|
const borderSize = token.data.flags["hex-size-support"].borderSize;
|
||||||
let shape = [{x: 0, y: 0}];
|
let shape = [{x: 0, y: 0}];
|
||||||
if (borderSize >= 2)
|
if (borderSize >= 2)
|
||||||
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
|
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
|
||||||
@@ -197,7 +184,7 @@ export function getTokenShapeForTokenData(tokenData, scene=canvas.scene) {
|
|||||||
if (borderSize >= 4)
|
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}])
|
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(tokenData.hexSizeSupport.altOrientationFlag) !== canvas.grid.grid.options.columns)
|
if (Boolean(CONFIG.hexSizeSupport.getAltOrientationFlag(token)) !== canvas.grid.grid.options.columns)
|
||||||
shape.forEach(space => space.y *= -1);
|
shape.forEach(space => space.y *= -1);
|
||||||
if (canvas.grid.grid.options.columns)
|
if (canvas.grid.grid.options.columns)
|
||||||
shape = shape.map(space => {return {x: space.y, y: space.x}});
|
shape = shape.map(space => {return {x: space.y, y: space.x}});
|
||||||
@@ -292,7 +279,3 @@ export function early_isGM() {
|
|||||||
const gmLevel = CONST.USER_ROLES.ASSISTANT;
|
const gmLevel = CONST.USER_ROLES.ASSISTANT;
|
||||||
return level >= gmLevel;
|
return level >= gmLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isModuleActive(moduleName) {
|
|
||||||
return game.modules.get(moduleName)?.active
|
|
||||||
}
|
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
"name": "drag-ruler",
|
"name": "drag-ruler",
|
||||||
"title": "Drag Ruler",
|
"title": "Drag Ruler",
|
||||||
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
||||||
"version": "1.12.8",
|
"version": "1.11.5",
|
||||||
"minimumCoreVersion" : "9.245",
|
"minimumCoreVersion" : "9.245",
|
||||||
"compatibleCoreVersion" : "9",
|
"compatibleCoreVersion" : "9",
|
||||||
"authors": [
|
"authors": [
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
],
|
],
|
||||||
"socket": true,
|
"socket": true,
|
||||||
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
||||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.8/drag-ruler-1.12.8.zip",
|
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.11.5.zip",
|
||||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
||||||
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
||||||
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
||||||
|
|||||||
Generated
+1
-79
@@ -2,15 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
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]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.9.1"
|
version = "3.9.1"
|
||||||
@@ -33,53 +24,13 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"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]]
|
[[package]]
|
||||||
name = "gridless-pathfinding"
|
name = "gridless-pathfinding"
|
||||||
version = "1.12.8"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"sha1",
|
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -98,12 +49,6 @@ version = "1.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.125"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -137,17 +82,6 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
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]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.86"
|
version = "1.0.86"
|
||||||
@@ -159,24 +93,12 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typenum"
|
|
||||||
version = "1.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.79"
|
version = "0.2.79"
|
||||||
|
|||||||
+1
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "gridless-pathfinding"
|
name = "gridless-pathfinding"
|
||||||
version = "1.12.8"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
@@ -16,5 +16,4 @@ lto = true
|
|||||||
console_error_panic_hook = "0.1.7"
|
console_error_panic_hook = "0.1.7"
|
||||||
js-sys = "0.3.56"
|
js-sys = "0.3.56"
|
||||||
rustc-hash = "1.1.0"
|
rustc-hash = "1.1.0"
|
||||||
sha1 = "0.10.1"
|
|
||||||
wasm-bindgen = "0.2.79"
|
wasm-bindgen = "0.2.79"
|
||||||
|
|||||||
@@ -38,11 +38,6 @@ impl Point {
|
|||||||
let e = 0.000001;
|
let e = 0.000001;
|
||||||
(self.x - other.x).abs() < e && (self.y - other.y).abs() < e
|
(self.x - other.x).abs() < e && (self.y - other.y).abs() < e
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_in_rectangle(&self, rect: &Rectangle) -> bool {
|
|
||||||
between(self.x, rect.left.p1.x, rect.right.p1.x)
|
|
||||||
&& between(self.y, rect.top.p1.y, rect.bottom.p1.y)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for Point {}
|
impl Eq for Point {}
|
||||||
@@ -184,70 +179,9 @@ impl LineSegment {
|
|||||||
}
|
}
|
||||||
between(intersection.x, self.p1.x, self.p2.x)
|
between(intersection.x, self.p1.x, self.p2.x)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn intersects_rect(&self, rect: &Rectangle) -> bool {
|
|
||||||
if self.p1.is_in_rectangle(rect) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
[rect.left, rect.top, rect.right, rect.bottom]
|
|
||||||
.iter()
|
|
||||||
.any(|edge| self.intersection(edge).is_some())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn between<T: Copy + PartialOrd>(num: T, a: T, b: T) -> bool {
|
pub fn between<T: Copy + PartialOrd>(num: T, a: T, b: T) -> bool {
|
||||||
let (min, max) = if a < b { (a, b) } else { (b, a) };
|
let (min, max) = if a < b { (a, b) } else { (b, a) };
|
||||||
num >= min && num <= max
|
num >= min && num <= max
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
extern "C" {
|
|
||||||
pub type JsRectangle;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn left(this: &JsRectangle) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn top(this: &JsRectangle) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn right(this: &JsRectangle) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn bottom(this: &JsRectangle) -> f64;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Rectangle {
|
|
||||||
pub left: LineSegment,
|
|
||||||
pub top: LineSegment,
|
|
||||||
pub right: LineSegment,
|
|
||||||
pub bottom: LineSegment,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rectangle {
|
|
||||||
pub fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
|
|
||||||
Self {
|
|
||||||
left: LineSegment::new(Point::new(left, top), Point::new(left, bottom)),
|
|
||||||
top: LineSegment::new(Point::new(left, top), Point::new(right, top)),
|
|
||||||
right: LineSegment::new(Point::new(right, top), Point::new(right, bottom)),
|
|
||||||
bottom: LineSegment::new(Point::new(left, bottom), Point::new(right, bottom)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsRectangle> for Rectangle {
|
|
||||||
fn from(rect: &JsRectangle) -> Self {
|
|
||||||
let left = rect.left();
|
|
||||||
let top = rect.top();
|
|
||||||
let right = rect.right();
|
|
||||||
let bottom = rect.bottom();
|
|
||||||
Self::new(left, top, right, bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct Circle {
|
|
||||||
pub center: Point,
|
|
||||||
pub radius: f64,
|
|
||||||
}
|
|
||||||
|
|||||||
+5
-186
@@ -1,11 +1,9 @@
|
|||||||
use js_sys::Array;
|
use js_sys::Array;
|
||||||
use sha1::{Digest, Sha1};
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::{Circle, LineSegment, Point, Rectangle},
|
geometry::Point,
|
||||||
pathfinder::{DiscoveredNodePtr, Pathfinder},
|
pathfinder::{DiscoveredNodePtr, Pathfinder},
|
||||||
util::Windows,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
@@ -19,17 +17,12 @@ macro_rules! log {
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#[wasm_bindgen(js_namespace = console, js_name=warn)]
|
#[wasm_bindgen(js_namespace = console, js_name=warn)]
|
||||||
pub fn log(s: &str);
|
pub fn log(s: &str);
|
||||||
|
|
||||||
#[wasm_bindgen(js_namespace = ["dragRuler", "private"], js_name=terrainRulerWrapper)]
|
|
||||||
pub fn distance_with_terrain(a: Point, b: Point) -> f64;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
extern "C" {
|
extern "C" {
|
||||||
pub type JsWall;
|
pub type JsWall;
|
||||||
pub type JsWallData;
|
pub type JsWallData;
|
||||||
pub type JsWallFlags;
|
|
||||||
pub type JsWallHeight;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
#[wasm_bindgen(method, getter)]
|
||||||
fn data(this: &JsWall) -> JsWallData;
|
fn data(this: &JsWall) -> JsWallData;
|
||||||
@@ -45,18 +38,6 @@ extern "C" {
|
|||||||
|
|
||||||
#[wasm_bindgen(method, getter, js_name = "move")]
|
#[wasm_bindgen(method, getter, js_name = "move")]
|
||||||
fn move_type(this: &JsWallData) -> WallSenseType;
|
fn move_type(this: &JsWallData) -> WallSenseType;
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn flags(this: &JsWallData) -> JsWallFlags;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter, js_name = "wallHeight")]
|
|
||||||
fn wall_height(this: &JsWallFlags) -> Option<JsWallHeight>;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter, js_name = "wallHeightTop")]
|
|
||||||
fn top(this: &JsWallHeight) -> Option<f64>;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter, js_name = "wallHeightBottom")]
|
|
||||||
fn bottom(this: &JsWallHeight) -> Option<f64>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -79,112 +60,6 @@ impl From<JsPoint> for Point {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
extern "C" {
|
|
||||||
pub type JsTerrainInfo;
|
|
||||||
pub type JsTerrainObject;
|
|
||||||
pub type JsTerrainShape;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn object(this: &JsTerrainInfo) -> JsTerrainObject;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn shape(this: &JsTerrainInfo) -> JsTerrainShape;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn x(this: &JsTerrainObject) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn y(this: &JsTerrainObject) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn width(this: &JsTerrainObject) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn height(this: &JsTerrainObject) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter, js_name = "type")]
|
|
||||||
fn shape_type(this: &JsTerrainShape) -> u32;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn x(this: &JsTerrainShape) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn y(this: &JsTerrainShape) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn radius(this: &JsTerrainShape) -> f64;
|
|
||||||
|
|
||||||
#[wasm_bindgen(method, getter)]
|
|
||||||
fn points(this: &JsTerrainShape) -> Vec<f64>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JsTerrainObject {
|
|
||||||
fn to_bounding_rect(&self) -> Rectangle {
|
|
||||||
let left = self.x();
|
|
||||||
let top = self.y();
|
|
||||||
let right = left + self.width();
|
|
||||||
let bottom = top + self.height();
|
|
||||||
Rectangle::new(left, top, right, bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JsTerrainShape {
|
|
||||||
fn to_segments(&self, x: f64, y: f64) -> Vec<LineSegment> {
|
|
||||||
let points = self.points();
|
|
||||||
assert!(points.len() % 2 == 0);
|
|
||||||
points
|
|
||||||
.chunks(2)
|
|
||||||
.map(|coordinates| Point::new(coordinates[0] + x, coordinates[1] + y))
|
|
||||||
.windows()
|
|
||||||
.map(|(p1, p2)| LineSegment::new(p1, p2))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_circle(&self, x: f64, y: f64) -> Circle {
|
|
||||||
let center = Point::new(self.x() + x, self.y() + y);
|
|
||||||
let radius = self.radius();
|
|
||||||
Circle { center, radius }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsTerrainInfo> for TerrainShape {
|
|
||||||
fn from(terrain: &JsTerrainInfo) -> Self {
|
|
||||||
let shape = terrain.shape();
|
|
||||||
let object = terrain.object();
|
|
||||||
let x = object.x();
|
|
||||||
let y = object.y();
|
|
||||||
match shape.shape_type() {
|
|
||||||
0 => TerrainShape::Polygon(shape.to_segments(x, y)),
|
|
||||||
2 => TerrainShape::Circle(shape.to_circle(x, y)),
|
|
||||||
_ => unimplemented!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum TerrainShape {
|
|
||||||
Polygon(Vec<LineSegment>),
|
|
||||||
Circle(Circle),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Terrain {
|
|
||||||
pub shape: TerrainShape,
|
|
||||||
pub bounding_box: Rectangle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&JsTerrainInfo> for Terrain {
|
|
||||||
fn from(terrain: &JsTerrainInfo) -> Self {
|
|
||||||
let bounding_box = terrain.object().to_bounding_rect();
|
|
||||||
let shape = terrain.into();
|
|
||||||
Self {
|
|
||||||
bounding_box,
|
|
||||||
shape,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum DoorState {
|
pub enum DoorState {
|
||||||
@@ -245,38 +120,6 @@ impl TryFrom<usize> for WallSenseType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct WallHeight {
|
|
||||||
pub top: f64,
|
|
||||||
pub bottom: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for WallHeight {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
top: f64::INFINITY,
|
|
||||||
bottom: f64::NEG_INFINITY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Option<JsWallHeight>> for WallHeight {
|
|
||||||
fn from(height: Option<JsWallHeight>) -> Self {
|
|
||||||
let height = height
|
|
||||||
.map(|height| (height.top(), height.bottom()))
|
|
||||||
.unwrap_or((None, None));
|
|
||||||
let top = height.0.unwrap_or(WallHeight::default().top);
|
|
||||||
let bottom = height.1.unwrap_or(WallHeight::default().bottom);
|
|
||||||
Self { top, bottom }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WallHeight {
|
|
||||||
pub fn contains(&self, height: f64) -> bool {
|
|
||||||
self.top >= height && self.bottom <= height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct Wall {
|
pub struct Wall {
|
||||||
pub p1: Point,
|
pub p1: Point,
|
||||||
@@ -284,7 +127,6 @@ pub struct Wall {
|
|||||||
pub door_type: DoorType,
|
pub door_type: DoorType,
|
||||||
pub door_state: DoorState,
|
pub door_state: DoorState,
|
||||||
pub move_type: WallSenseType,
|
pub move_type: WallSenseType,
|
||||||
pub height: WallHeight,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Wall {
|
impl Wall {
|
||||||
@@ -294,7 +136,6 @@ impl Wall {
|
|||||||
door_type: DoorType,
|
door_type: DoorType,
|
||||||
door_state: DoorState,
|
door_state: DoorState,
|
||||||
move_type: WallSenseType,
|
move_type: WallSenseType,
|
||||||
height: WallHeight,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
p1,
|
p1,
|
||||||
@@ -302,7 +143,6 @@ impl Wall {
|
|||||||
door_type,
|
door_type,
|
||||||
door_state,
|
door_state,
|
||||||
move_type,
|
move_type,
|
||||||
height,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,42 +156,29 @@ impl Wall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Wall {
|
impl Wall {
|
||||||
fn from_js(wall: &JsWall, enable_height: bool) -> Self {
|
fn from_js(wall: &JsWall) -> Self {
|
||||||
let data = wall.data();
|
let data = wall.data();
|
||||||
let mut c = data.c();
|
let mut c = data.c();
|
||||||
c.iter_mut().for_each(|val| *val = val.round());
|
c.iter_mut().for_each(|val| *val = val.round());
|
||||||
let height = if enable_height {
|
|
||||||
data.flags().wall_height().into()
|
|
||||||
} else {
|
|
||||||
WallHeight::default()
|
|
||||||
};
|
|
||||||
Self::new(
|
Self::new(
|
||||||
Point::new(c[0], c[1]),
|
Point::new(c[0], c[1]),
|
||||||
Point::new(c[2], c[3]),
|
Point::new(c[2], c[3]),
|
||||||
data.door_type(),
|
data.door_type(),
|
||||||
data.door_state(),
|
data.door_state(),
|
||||||
data.move_type(),
|
data.move_type(),
|
||||||
height,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn initialize(
|
pub fn initialize(js_walls: Vec<JsValue>, token_size: f64) -> Pathfinder {
|
||||||
js_walls: Vec<JsValue>,
|
|
||||||
js_terrain: Vec<JsTerrainInfo>,
|
|
||||||
token_size: f64,
|
|
||||||
token_elevation: f64,
|
|
||||||
enable_height: bool,
|
|
||||||
) -> Pathfinder {
|
|
||||||
let mut walls = Vec::with_capacity(js_walls.len());
|
let mut walls = Vec::with_capacity(js_walls.len());
|
||||||
for wall in js_walls {
|
for wall in js_walls {
|
||||||
let wall = JsWall::from(wall);
|
let wall = JsWall::from(wall);
|
||||||
walls.push(Wall::from_js(&wall, enable_height));
|
walls.push(Wall::from_js(&wall));
|
||||||
}
|
}
|
||||||
let terrain = js_terrain.iter().map(|terrain| terrain.into()).collect();
|
Pathfinder::initialize(walls, token_size)
|
||||||
Pathfinder::initialize(walls, terrain, token_size, token_elevation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -379,14 +206,6 @@ pub fn debug_get_pathfinding_points(pathfinder: &Pathfinder) -> Array {
|
|||||||
.collect()
|
.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 {
|
trait IteratePath {
|
||||||
fn iter_path(&self) -> PathIterator;
|
fn iter_path(&self) -> PathIterator;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ mod geometry;
|
|||||||
mod js_api;
|
mod js_api;
|
||||||
mod pathfinder;
|
mod pathfinder;
|
||||||
mod ptr_indexed_hash_set;
|
mod ptr_indexed_hash_set;
|
||||||
mod util;
|
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
|||||||
+41
-88
@@ -5,8 +5,8 @@ use wasm_bindgen::prelude::*;
|
|||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::{LineSegment, Point, Rectangle},
|
geometry::{LineSegment, Point},
|
||||||
js_api::{distance_with_terrain, Terrain, TerrainShape, Wall, WallSenseType, log},
|
js_api::{Wall, WallSenseType},
|
||||||
ptr_indexed_hash_set::PtrIndexedHashSet,
|
ptr_indexed_hash_set::PtrIndexedHashSet,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ impl NodeStorage {
|
|||||||
self.regular_nodes.push(node);
|
self.regular_nodes.push(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialize_edges(&mut self, node: &NodePtr, walls: &[LineSegment], terrain: &[Rectangle]) {
|
fn initialize_edges(&mut self, node: &NodePtr, walls: &[LineSegment]) {
|
||||||
if node.borrow().final_edge.is_none() {
|
if node.borrow().final_edge.is_none() {
|
||||||
let final_edge = self
|
let final_edge = self
|
||||||
.final_node
|
.final_node
|
||||||
@@ -97,11 +97,7 @@ impl NodeStorage {
|
|||||||
})
|
})
|
||||||
.map(|neighbor| Edge {
|
.map(|neighbor| Edge {
|
||||||
target: neighbor.clone(),
|
target: neighbor.clone(),
|
||||||
cost: Self::measure_distance(
|
cost: node.borrow().point.distance_to(neighbor.borrow().point),
|
||||||
node.borrow().point,
|
|
||||||
neighbor.borrow().point,
|
|
||||||
terrain,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
node.borrow_mut().final_edge = Some(final_edge);
|
node.borrow_mut().final_edge = Some(final_edge);
|
||||||
}
|
}
|
||||||
@@ -118,8 +114,7 @@ impl NodeStorage {
|
|||||||
}
|
}
|
||||||
let neighbor_point = neighbor.borrow().point;
|
let neighbor_point = neighbor.borrow().point;
|
||||||
if !self.collides_with_wall(&LineSegment::new(point, neighbor_point), walls) {
|
if !self.collides_with_wall(&LineSegment::new(point, neighbor_point), walls) {
|
||||||
let cost =
|
let cost = point.distance_to(neighbor_point);
|
||||||
Self::measure_distance(node.borrow().point, neighbor.borrow().point, terrain);
|
|
||||||
edges.push(Edge {
|
edges.push(Edge {
|
||||||
target: neighbor.clone(),
|
target: neighbor.clone(),
|
||||||
cost,
|
cost,
|
||||||
@@ -133,15 +128,6 @@ impl NodeStorage {
|
|||||||
walls.iter().any(|wall| line.intersection(wall).is_some())
|
walls.iter().any(|wall| line.intersection(wall).is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure_distance(a: Point, b: Point, terrain: &[Rectangle]) -> f64 {
|
|
||||||
let segment = LineSegment::new(a, b);
|
|
||||||
if terrain.iter().any(|rect| segment.intersects_rect(rect)) {
|
|
||||||
distance_with_terrain(a, b)
|
|
||||||
} else {
|
|
||||||
a.distance_to(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cleanup_final_edges(&mut self) {
|
pub fn cleanup_final_edges(&mut self) {
|
||||||
for node in &self.regular_nodes {
|
for node in &self.regular_nodes {
|
||||||
node.borrow_mut().final_edge = None;
|
node.borrow_mut().final_edge = None;
|
||||||
@@ -159,51 +145,32 @@ pub struct Pathfinder {
|
|||||||
pub nodes: NodeStorage,
|
pub nodes: NodeStorage,
|
||||||
#[wasm_bindgen(skip)]
|
#[wasm_bindgen(skip)]
|
||||||
pub walls: Vec<LineSegment>,
|
pub walls: Vec<LineSegment>,
|
||||||
#[wasm_bindgen(skip)]
|
|
||||||
pub terrain: Vec<Rectangle>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pathfinder {
|
impl Pathfinder {
|
||||||
pub fn initialize(
|
pub fn initialize<I>(walls: I, token_size: f64) -> Self
|
||||||
walls: Vec<Wall>,
|
where
|
||||||
terrain: Vec<Terrain>,
|
I: IntoIterator<Item = Wall>,
|
||||||
token_size: f64,
|
{
|
||||||
token_elevation: f64,
|
|
||||||
) -> Self {
|
|
||||||
log!("{:#?}", terrain);
|
|
||||||
let distance_from_walls = token_size / 2.0;
|
let distance_from_walls = token_size / 2.0;
|
||||||
|
|
||||||
// TODO Place pathfinding nodes around terrain edges
|
|
||||||
let mut polygon_terrain = Vec::new();
|
|
||||||
let mut circle_terrain = Vec::new();
|
|
||||||
|
|
||||||
for terrain in &terrain {
|
|
||||||
match &terrain.shape {
|
|
||||||
TerrainShape::Polygon(polygon) => polygon_terrain.append(&mut polygon.clone()),
|
|
||||||
TerrainShape::Circle(circle) => circle_terrain.push(circle),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut walls = walls
|
|
||||||
.into_iter()
|
|
||||||
.filter(|wall| wall.move_type != WallSenseType::NONE)
|
|
||||||
.filter(|wall| !(wall.is_door() && wall.is_open()))
|
|
||||||
.filter(|wall| wall.height.contains(token_elevation))
|
|
||||||
.map(|wall| LineSegment::new(wall.p1, wall.p2))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// TODO Generate points around difficult terrain
|
|
||||||
let mut endpoints = FxHashMap::<Point, Vec<f64>>::default();
|
let mut endpoints = FxHashMap::<Point, Vec<f64>>::default();
|
||||||
|
let mut line_segments = Vec::new();
|
||||||
for segments in walls.iter().chain(polygon_terrain.iter()) {
|
for wall in walls {
|
||||||
let x_diff = segments.p2.x - segments.p1.x;
|
if wall.move_type == WallSenseType::NONE {
|
||||||
let y_diff = segments.p2.y - segments.p1.y;
|
continue;
|
||||||
|
}
|
||||||
|
if wall.is_door() && wall.is_open() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let x_diff = wall.p2.x - wall.p1.x;
|
||||||
|
let y_diff = wall.p2.y - wall.p1.y;
|
||||||
let p1_angle = y_diff.atan2(x_diff).rem_euclid(2.0 * PI);
|
let p1_angle = y_diff.atan2(x_diff).rem_euclid(2.0 * PI);
|
||||||
let p2_angle = (p1_angle + PI).rem_euclid(2.0 * PI);
|
let p2_angle = (p1_angle + PI).rem_euclid(2.0 * PI);
|
||||||
for (point, angle) in [(segments.p1, p1_angle), (segments.p2, p2_angle)] {
|
for (point, angle) in [(wall.p1, p1_angle), (wall.p2, p2_angle)] {
|
||||||
let angles = endpoints.entry(point).or_insert_with(Vec::new);
|
let angles = endpoints.entry(point).or_insert_with(Vec::new);
|
||||||
angles.push(angle);
|
angles.push(angle);
|
||||||
}
|
}
|
||||||
|
line_segments.push(LineSegment::new(wall.p1, wall.p2));
|
||||||
}
|
}
|
||||||
endpoints
|
endpoints
|
||||||
.values_mut()
|
.values_mut()
|
||||||
@@ -221,24 +188,29 @@ impl Pathfinder {
|
|||||||
if angle_diff <= PI {
|
if angle_diff <= PI {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
let angle_between = angle_diff / 2.0 + angle1;
|
let angle_between = angle_diff / 2.0 + angle1;
|
||||||
nodes.push(calc_pathfinding_node(
|
let pathfinding_node = calc_pathfinding_node(
|
||||||
point,
|
point,
|
||||||
angle_between,
|
angle_between,
|
||||||
distance_from_walls,
|
distance_from_walls,
|
||||||
&mut walls,
|
&mut line_segments,
|
||||||
));
|
);
|
||||||
|
if angle_diff > 1.5 * PI {
|
||||||
|
nodes.push(pathfinding_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
nodes.push(calc_pathfinding_node(
|
nodes.push(calc_pathfinding_node(
|
||||||
point,
|
point,
|
||||||
angle1 + 0.5 * PI,
|
angle1 + 0.5 * PI,
|
||||||
distance_from_walls,
|
distance_from_walls,
|
||||||
&mut walls,
|
&mut line_segments,
|
||||||
));
|
));
|
||||||
nodes.push(calc_pathfinding_node(
|
nodes.push(calc_pathfinding_node(
|
||||||
point,
|
point,
|
||||||
angle2 - 0.5 * PI,
|
angle2 - 0.5 * PI,
|
||||||
distance_from_walls,
|
distance_from_walls,
|
||||||
&mut walls,
|
&mut line_segments,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let angle1 = angles.last().unwrap();
|
let angle1 = angles.last().unwrap();
|
||||||
@@ -247,51 +219,32 @@ impl Pathfinder {
|
|||||||
if angle_diff <= PI {
|
if angle_diff <= PI {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if angle_diff > 1.5 * PI {
|
||||||
let angle_between = angle_diff / 2.0 + angle1;
|
let angle_between = angle_diff / 2.0 + angle1;
|
||||||
nodes.push(calc_pathfinding_node(
|
nodes.push(calc_pathfinding_node(
|
||||||
point,
|
point,
|
||||||
angle_between,
|
angle_between,
|
||||||
distance_from_walls,
|
distance_from_walls,
|
||||||
&mut walls,
|
&mut line_segments,
|
||||||
));
|
));
|
||||||
|
}
|
||||||
nodes.push(calc_pathfinding_node(
|
nodes.push(calc_pathfinding_node(
|
||||||
point,
|
point,
|
||||||
angle1 + 0.5 * PI,
|
angle1 + 0.5 * PI,
|
||||||
distance_from_walls,
|
distance_from_walls,
|
||||||
&mut walls,
|
&mut line_segments,
|
||||||
));
|
));
|
||||||
nodes.push(calc_pathfinding_node(
|
nodes.push(calc_pathfinding_node(
|
||||||
point,
|
point,
|
||||||
angle2 - 0.5 * PI,
|
angle2 - 0.5 * PI,
|
||||||
distance_from_walls,
|
distance_from_walls,
|
||||||
&mut walls,
|
&mut line_segments,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Check bounding box of circle terrain
|
|
||||||
for circle in circle_terrain {
|
|
||||||
let angle_step = f64::asin(token_size / 2.0 / circle.radius);
|
|
||||||
let mut angle = 0.0;
|
|
||||||
while angle < 2.0 * PI {
|
|
||||||
let point = Point {
|
|
||||||
x: circle.center.x + angle.cos() * circle.radius,
|
|
||||||
y: circle.center.y + angle.sin() * circle.radius,
|
|
||||||
};
|
|
||||||
nodes.push(calc_pathfinding_node(
|
|
||||||
point,
|
|
||||||
angle,
|
|
||||||
distance_from_walls,
|
|
||||||
&mut walls,
|
|
||||||
));
|
|
||||||
angle += angle_step;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Eliminating nodes close to each other may improve performance
|
// TODO Eliminating nodes close to each other may improve performance
|
||||||
Self {
|
Self {
|
||||||
nodes,
|
nodes,
|
||||||
walls,
|
walls: line_segments,
|
||||||
terrain: terrain.iter().map(|terrain| terrain.bounding_box).collect(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +253,7 @@ impl Pathfinder {
|
|||||||
let mut nodes = self.nodes.clone();
|
let mut nodes = self.nodes.clone();
|
||||||
nodes.final_node = Some(NodePtr::from(Node::new(from)));
|
nodes.final_node = Some(NodePtr::from(Node::new(from)));
|
||||||
let to_node = NodePtr::from(Node::new(to));
|
let to_node = NodePtr::from(Node::new(to));
|
||||||
nodes.initialize_edges(&to_node, &self.walls, &self.terrain);
|
nodes.initialize_edges(&to_node, &self.walls);
|
||||||
let to = DiscoveredNode {
|
let to = DiscoveredNode {
|
||||||
node: to_node,
|
node: to_node,
|
||||||
cost: 0.0,
|
cost: 0.0,
|
||||||
@@ -333,7 +286,7 @@ impl Pathfinder {
|
|||||||
if previous_nodes.contains(neighbor) {
|
if previous_nodes.contains(neighbor) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
nodes.initialize_edges(neighbor, &self.walls, &self.terrain);
|
nodes.initialize_edges(neighbor, &self.walls);
|
||||||
// Add a flat 0.00001 cost per node to discurage creation of unnecessary waypoints
|
// Add a flat 0.00001 cost per node to discurage creation of unnecessary waypoints
|
||||||
let cost = current_node.borrow().cost + edge.cost + 0.00001;
|
let cost = current_node.borrow().cost + edge.cost + 0.00001;
|
||||||
let discovered_neighbor = DiscoveredNode {
|
let discovered_neighbor = DiscoveredNode {
|
||||||
@@ -363,11 +316,11 @@ fn calc_pathfinding_node(
|
|||||||
p: Point,
|
p: Point,
|
||||||
angle: f64,
|
angle: f64,
|
||||||
distance_from_walls: f64,
|
distance_from_walls: f64,
|
||||||
walls: &mut Vec<LineSegment>,
|
line_segments: &mut Vec<LineSegment>,
|
||||||
) -> NodePtr {
|
) -> NodePtr {
|
||||||
let offset_x = angle.cos() * distance_from_walls;
|
let offset_x = angle.cos() * distance_from_walls;
|
||||||
let offset_y = angle.sin() * distance_from_walls;
|
let offset_y = angle.sin() * distance_from_walls;
|
||||||
walls.push(LineSegment::new(
|
line_segments.push(LineSegment::new(
|
||||||
p,
|
p,
|
||||||
Point {
|
Point {
|
||||||
x: p.x + offset_x * 0.99,
|
x: p.x + offset_x * 0.99,
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
pub struct WindowIterator<I: Iterator> {
|
|
||||||
iterator: I,
|
|
||||||
previous: Option<I::Item>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I: Iterator> Iterator for WindowIterator<I>
|
|
||||||
where
|
|
||||||
I::Item: Copy,
|
|
||||||
{
|
|
||||||
type Item = (I::Item, I::Item);
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
if self.previous.is_none() {
|
|
||||||
self.previous = self.iterator.next();
|
|
||||||
}
|
|
||||||
let current = self.iterator.next();
|
|
||||||
if current.is_none() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let current = current.unwrap();
|
|
||||||
let previous = self.previous.unwrap();
|
|
||||||
let result = (current, previous);
|
|
||||||
self.previous = Some(current);
|
|
||||||
Some(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Windows<T> {
|
|
||||||
fn windows(self) -> WindowIterator<Self>
|
|
||||||
where
|
|
||||||
Self: Sized + Iterator;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<I: Iterator> Windows<I> for I {
|
|
||||||
fn windows(self) -> WindowIterator<Self>
|
|
||||||
where
|
|
||||||
Self: Sized + Iterator,
|
|
||||||
{
|
|
||||||
WindowIterator {
|
|
||||||
iterator: self,
|
|
||||||
previous: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user