Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c08d65ffad | |||
| 445c03d29a | |||
| 5904efbdd5 | |||
| d325f8acee | |||
| 1577435a33 | |||
| 1b8b2328c6 | |||
| 0dba39b145 | |||
| c1b0e9d7fb | |||
| 9d227efcd1 | |||
| a3d942c287 | |||
| bdbd4fde18 | |||
| 3afb1f30b7 | |||
| 654f8e3111 | |||
| 2979e86201 | |||
| 8f81fd707e | |||
| 5f6e348bf0 | |||
| 82f2a885fb | |||
| d536ae8dfb | |||
| 5add0be393 | |||
| f77990420a | |||
| 44efabc656 | |||
| e84632c0d7 | |||
| 30bd3d33e0 | |||
| 6604e322bd | |||
| b68f3bbe29 | |||
| 793e87bc2f | |||
| df4c515fcd | |||
| 02dfd8db0d | |||
| 65911c11f0 | |||
| ddf89f9499 | |||
| 0dfdb23bfb | |||
| 3f2e9e1a3e | |||
| 846e39d946 | |||
| 26748ee234 | |||
| ebd0ee6ccf | |||
| 1d477094b8 | |||
| bbfce2bb77 | |||
| 54cee52eb2 | |||
| a967a03197 | |||
| 8d243ce919 | |||
| b227d5073b | |||
| 576db2dc5a | |||
| b031acaa6e | |||
| ee956abd78 | |||
| 1334703877 | |||
| d6cccf4466 | |||
| 074d2f5052 | |||
| e5b8f896f8 | |||
| 3de350d3e9 | |||
| 8f97081236 | |||
| 18253dc42b | |||
| fb702cd850 | |||
| ad3fdf4d18 | |||
| 58946096a6 | |||
| 22112ddfd5 | |||
| f59b0f9d2d | |||
| 443cfd4317 | |||
| fb3665d758 | |||
| 96cb690431 | |||
| fb7202d5f0 | |||
| fa581ddfc3 | |||
| 56e15be4ca | |||
| 1509307313 | |||
| 68885385ea | |||
| 6748617f6a | |||
| 9d5dccb504 | |||
| a252da620a | |||
| 41c8979925 | |||
| 39f9204fa7 | |||
| 10633e4e2a | |||
| f3e1492edd | |||
| 7ac1c828b6 | |||
| 1972498b05 | |||
| 4a5b9bbb11 | |||
| d467fe5bcf | |||
| 1f770f7597 | |||
| fa62e1d9c4 | |||
| f18ce33779 | |||
| 34cead5cba | |||
| eeb6db76a6 | |||
| 9c44994fd2 | |||
| 23c1a903a0 | |||
| 1b3f563c4e | |||
| c4595f408f |
@@ -0,0 +1,5 @@
|
||||
/foundry*.js
|
||||
artifact/
|
||||
wasm/
|
||||
*.lock
|
||||
.vscode/
|
||||
+120
@@ -1,3 +1,123 @@
|
||||
## 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
|
||||
### 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)
|
||||
|
||||
|
||||
## 1.11.4
|
||||
### Bugfixes
|
||||
- When changing the measurement mode via a keybinding (toggle snaping or toggle pathfinding) the updated ruler will now be sent to other players immediately
|
||||
- Fixed a bug that incorrectly showed a ruler to be snapped to other players despite the ruler not being snapped
|
||||
- Fixed a bug that could cause a token to move to an incorrect location if the token was being dragged and dropped very rapidly
|
||||
- Drag Ruler's token movement animations can now be properly waited for (this improves the interaction with modules like sequencer)
|
||||
|
||||
### Translation
|
||||
- Updated Spanish translation (thanks to Viriato139ac#342)
|
||||
|
||||
|
||||
## 1.11.3
|
||||
### Bugfixes
|
||||
- The setting to automatically start pathfinding is now visible to players again (this was a regression introduced in 1.11.2)
|
||||
- Fixed a bug that would show the measurements of other players as if they were using the pathfinder, even if they were not using it.
|
||||
|
||||
|
||||
## 1.11.2
|
||||
### Bugfixes
|
||||
- Fixed a memory leak that could cause the rule to slow down after using the pathfinding functionality for a while
|
||||
|
||||
### Misc
|
||||
- GMs are now always allowed to use the pathfinding tool. The setting now only prevents players from using it.
|
||||
|
||||
### Compatibility
|
||||
- Drag Ruler's generic speed provider is now aware of good defaults for Dungeonslayers 4
|
||||
|
||||
|
||||
## 1.11.1
|
||||
### Bugfixes
|
||||
- Fixed a bug that would cause the pathfinding algorithm to make tokens of size 2 and 4 to take an unnecessary step
|
||||
|
||||
|
||||
## 1.11.0
|
||||
### New features
|
||||
- Drag Ruler now supports pathfinding. Pressing the assigned button will automatically calculate the shortest route to the point you're dragging your token to and add the necessary waypoints to the ruler.
|
||||
- This feature is only available for gridded maps
|
||||
- This feature can only be used if it's enabled by the GM in the module settings
|
||||
- The routing algorithm *does not* take difficult terrain into account
|
||||
|
||||
|
||||
## 1.10.3
|
||||
### Compatibility
|
||||
- This release contains changes required to be compatible with Foundry 9.245
|
||||
|
||||
|
||||
## 1.10.2
|
||||
### Bugfixes
|
||||
- Fixed a bug that could cause squares to be highlighted in the wrong color when using waypoints on a 5/10/5 gird
|
||||
- When using Token Drag Vision, the temporary vision is now correctly cleaned up when dropping the token (resolves a conflict with the "Perfect Vision" module)
|
||||
|
||||
|
||||
## 1.10.1
|
||||
### Bugfixes
|
||||
- Fixed a bug that caused keybindings to break if no scene is active
|
||||
|
||||
### Translation
|
||||
- Updated Spanish translation (thanks to Viriato139ac#342)
|
||||
|
||||
|
||||
## 1.10.0
|
||||
**BREAKING** Drag Ruler 1.10.0 and onward cannot update directly from Drag Ruler versions older than 1.3.0. If you've been using Drag Ruler 1.2.2 or earlier in your world make sure to update to any Version between (inclusive) Drag Ruler 1.3.0 and 1.9.1, launch your world and log in as GM at least once. After doing so you can safely update to v1.10.0 or newer versions. Updating directly from 1.2.2 or older to 1.10.0 or newer will cause Drag Ruler to forget your Speed Prover Settings.
|
||||
|
||||
|
||||
@@ -37,12 +37,15 @@ Drag Ruler will work with all Foundry VTT game systems. However, some game syste
|
||||
|
||||
The game systems that offer Drag Ruler integration are:
|
||||
- Cypher System (starting with version 1.13.0)
|
||||
- DnD5e (via the module [DnD5e Drag Ruler Integration](https://foundryvtt.com/packages/elevation-drag-ruler))
|
||||
- GURPS 4th Edition Game Aid (Unofficial) (starting with version 0.9.1)
|
||||
- Ironclaw Second Edition (starting with version 0.2.2)
|
||||
- Lancer (via the module [Lancer Speed Provider](https://foundryvtt.com/packages/lancer-speed-provider))
|
||||
- Level Up: Advanced 5th Edition (Official) (via the module [A5E Drag Ruler Integration](https://foundryvtt.com/packages/a5edragruler))
|
||||
- Pathfinder 1 (starting with version 0.77.3)
|
||||
- Pathfinder 2e (via the module [PF2E Drag Ruler Integration](https://foundryvtt.com/packages/pf2e-dragruler/))
|
||||
- Shadowrun 5th Edition (via the module [Drag Ruler Integration for Shadowrun 5E](https://foundryvtt.com/packages/drag-ruler-integration-for-shadowrun-5e))
|
||||
- Shadow of the Demon Lord (starting with version 1.7.15)
|
||||
- Starfinder (via the module [Starfinder Drag Ruler Integration](https://foundryvtt.com/packages/starfinder-drag-ruler))
|
||||
- Stargate RPG (starting with version 1.6.0)
|
||||
- Symbaroum (via the module [Symbaroum drag ruler integration](https://foundryvtt.com/packages/symbaroum-drag-ruler-integration))
|
||||
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
from pathlib import PurePath, Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
wasm_pack = Path("~/.cargo/bin/wasm-pack").expanduser()
|
||||
|
||||
root_files = ["module.json", "README.md", "CHANGELOG.md", "LICENSE"]
|
||||
wasm_files = ["gridless_pathfinding_bg.wasm", "gridless_pathfinding.js"]
|
||||
output_dir = Path("artifact")
|
||||
copy_everything_directories = ["js", "lang", "templates"]
|
||||
wasm_dir = Path("wasm")
|
||||
root_dir = Path(".")
|
||||
rust_dir = Path("rust")
|
||||
build_dir_tmp = tempfile.TemporaryDirectory()
|
||||
build_dir = Path(build_dir_tmp.name)
|
||||
|
||||
with open("module.json", "r") as file:
|
||||
manifest = json.load(file)
|
||||
|
||||
zip_root = PurePath(f'{manifest["name"]}')
|
||||
|
||||
filename = f'{manifest["name"]}-{manifest["version"]}.zip'
|
||||
|
||||
result = subprocess.run([wasm_pack, "build", "--target", "web", "--out-dir", build_dir, root_dir / rust_dir])
|
||||
if result.returncode != 0:
|
||||
raise Exception("Wasm build failed")
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def write_directory(archive, d):
|
||||
for f in (root_dir / d).iterdir():
|
||||
if f.is_dir():
|
||||
write_directory(archive, f)
|
||||
else:
|
||||
assert(f.is_file())
|
||||
archive.write(f, arcname=zip_root / d / f.name)
|
||||
|
||||
with zipfile.ZipFile(output_dir / filename, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
|
||||
for f in root_files:
|
||||
archive.write(root_dir / f, arcname=zip_root / f)
|
||||
for d in copy_everything_directories:
|
||||
write_directory(archive, d)
|
||||
for f in wasm_files:
|
||||
archive.write(build_dir / f, arcname=zip_root / wasm_dir / f)
|
||||
|
||||
print(f"Successfully built {output_dir / filename}")
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
root_dir = Path(".")
|
||||
wasm_dir = root_dir / Path("wasm")
|
||||
rust_dir = root_dir / Path("rust")
|
||||
|
||||
debug = " --debug" if len(sys.argv) >= 2 and sys.argv[1] == "--debug" else ""
|
||||
|
||||
result = subprocess.run(["cargo", "watch", "-C" , rust_dir, "-s", f"wasm-pack build --target web --out-dir {wasm_dir.resolve()}{debug}"])
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
cargo install cargo-watch
|
||||
cargo install wasm-pack
|
||||
@@ -2,7 +2,7 @@ import {measureDistances} from "./compatibility.js";
|
||||
import {getMovementHistory} from "./movement_tracking.js";
|
||||
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
|
||||
import {settingsKey} from "./settings.js"
|
||||
import {getTokenShape} from "./util.js";
|
||||
import {getAreaFromPositionAndShape, getTokenShape} from "./util.js";
|
||||
|
||||
export const availableSpeedProviders = {}
|
||||
export let currentSpeedProvider = undefined
|
||||
@@ -140,6 +140,10 @@ export function getMovedDistanceFromToken(token) {
|
||||
return distances.reduce((acc, val) => acc + val, 0);
|
||||
}
|
||||
|
||||
export function buildCostFunction(token, shape) {
|
||||
return (x, y, costOptions={}) => getCostFromSpeedProvider(token, getAreaFromPositionAndShape({x, y}, shape), costOptions);
|
||||
}
|
||||
|
||||
export function registerModule(moduleId, speedProvider) {
|
||||
// Check if a module with the given id exists and is currently enabled
|
||||
const module = game.modules.get(moduleId)
|
||||
@@ -1,6 +1,6 @@
|
||||
import {getCostFromSpeedProvider} from "./api.js";
|
||||
import {buildCostFunction} from "./api.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {getAreaFromPositionAndShape, highlightTokenShape} from "./util.js";
|
||||
import {highlightTokenShape} from "./util.js";
|
||||
|
||||
export function getHexSizeSupportTokenGridCenter(token) {
|
||||
const tokenCenterOffset = CONFIG.hexSizeSupport.getCenterOffset(token)
|
||||
@@ -27,7 +27,7 @@ export function measureDistances(segments, entity, shape, options={}) {
|
||||
const newSegments = segments.slice(firstNewSegmentIndex);
|
||||
const distances = previousSegments.map(segment => segment.ray.dragRulerVisitedSpaces[segment.ray.dragRulerVisitedSpaces.length - 1].distance);
|
||||
previousSegments.forEach(segment => segment.ray.terrainRulerVisitedSpaces = duplicate(segment.ray.dragRulerVisitedSpaces));
|
||||
opts.costFunction = (x, y, costOptions={}) => { return getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), costOptions); }
|
||||
opts.costFunction = buildCostFunction(entity, shape);
|
||||
if (previousSegments.length > 0)
|
||||
opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState;
|
||||
return distances.concat(terrainRuler.measureDistances(newSegments, opts));
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* A combination queue/set where the elements are ordered (in ascending order, according to the given priority function)
|
||||
* and unique (according to the given elementMatcher).
|
||||
*
|
||||
* If an element is added to the set and an equivalent element already exists, the lower-priority one is discarded.
|
||||
*/
|
||||
export class PriorityQueueSet {
|
||||
constructor(elementMatcher, priorityFunction) {
|
||||
this.first = null;
|
||||
this.elementMatcher = elementMatcher;
|
||||
this.priorityFunction = priorityFunction;
|
||||
}
|
||||
|
||||
pushWithPriority(value) {
|
||||
const newNode = {value, priority: this.priorityFunction(value), next: null};
|
||||
|
||||
// If the queue is currently empty, we can just set this new node as the first and we're done
|
||||
if (!this.first) {
|
||||
this.first = newNode;
|
||||
return;
|
||||
}
|
||||
|
||||
let inserted = false;
|
||||
let previous;
|
||||
let current = this.first;
|
||||
|
||||
// Loop through the existing elements
|
||||
while (current) {
|
||||
if (this.elementMatcher(current.value, value)) {
|
||||
// We've found an equivalent element before one with a lower priority. This one has at least
|
||||
// the same priority as the new one, so don't bother inserting
|
||||
return;
|
||||
} else if (newNode.priority <= current.priority) {
|
||||
// We've found some element with lower priority than the new one, so insert the new one just before it
|
||||
newNode.next = current;
|
||||
if (previous) {
|
||||
previous.next = newNode;
|
||||
} else {
|
||||
this.first = newNode;
|
||||
}
|
||||
inserted = true;
|
||||
|
||||
previous = current;
|
||||
current = current.next;
|
||||
break;
|
||||
}
|
||||
previous = current;
|
||||
current = current.next;
|
||||
}
|
||||
|
||||
if (inserted) {
|
||||
// Go through the rest of the list and try to find an equivalent element to the new one.
|
||||
// We know it has higher priority than the new one, so remove it.
|
||||
while (current) {
|
||||
if (this.elementMatcher(current.value, value)) {
|
||||
if (previous) {
|
||||
previous.next = current.next;
|
||||
} else {
|
||||
this.first = current.next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
previous = current;
|
||||
current = current.next;
|
||||
}
|
||||
} else {
|
||||
// We reached the end of the queue without finding a lower-priority or existing element, so
|
||||
// insert the new one at the end
|
||||
previous.next = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return !!this.first;
|
||||
}
|
||||
|
||||
pop() {
|
||||
const first = this.first;
|
||||
this.first = first?.next;
|
||||
return first?.value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue that will only ever accept elements with a given value once. Elements must have a "value" field, the
|
||||
* JSON representation of which will be used as the key to match
|
||||
*/
|
||||
export class ProcessOnceQueue {
|
||||
constructor() {
|
||||
this.first = null;
|
||||
this.last = null;
|
||||
this.previouslyQueued = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove everything from the queue and forget all the previously-queued items
|
||||
*/
|
||||
reset() {
|
||||
this.first = null;
|
||||
this.last = null;
|
||||
this.previouslyQueued.clear();
|
||||
}
|
||||
|
||||
push(element) {
|
||||
if (this.previouslyQueued.has(element)) {
|
||||
return;
|
||||
}
|
||||
this.previouslyQueued.add(element);
|
||||
|
||||
const newNode = {
|
||||
value: element,
|
||||
next: null,
|
||||
previous: null
|
||||
}
|
||||
|
||||
if (!this.first) {
|
||||
this.first = newNode;
|
||||
this.last = newNode;
|
||||
} else {
|
||||
this.last.next = newNode;
|
||||
newNode.previous = this.last;
|
||||
this.last = newNode;
|
||||
}
|
||||
}
|
||||
|
||||
pop() {
|
||||
const node = this.first;
|
||||
this.first = node?.next;
|
||||
if (!node?.next) {
|
||||
this.last = null;
|
||||
}
|
||||
return node?.value;
|
||||
}
|
||||
|
||||
hasNext() {
|
||||
return !!this.first;
|
||||
}
|
||||
}
|
||||
@@ -16,3 +16,21 @@ export function getGridPositionFromPixels(xPixel, yPixel) {
|
||||
return [y, x]
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
export function getGridPositionFromPixelsObj(o) {
|
||||
const r = {};
|
||||
[r.x, r.y] = getGridPositionFromPixels(o.x, o.y);
|
||||
return r;
|
||||
}
|
||||
|
||||
export function getPixelsFromGridPositionObj(o) {
|
||||
const r = {};
|
||||
[r.x, r.y] = getPixelsFromGridPosition(o.x, o.y);
|
||||
return r;
|
||||
}
|
||||
|
||||
export function getCenterFromGridPositionObj(o) {
|
||||
const r = getPixelsFromGridPositionObj(o);
|
||||
[r.x, r.y] = canvas.grid.getCenter(r.x, r.y);
|
||||
return r;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
|
||||
import {getGridPositionFromPixels} from "./foundry_fixes.js";
|
||||
import {getGridPositionFromPixels, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
||||
import {Line} from "./geometry.js";
|
||||
import {disableSnap, moveWithoutAnimation} from "./keybindings.js";
|
||||
import {trackRays} from "./movement_tracking.js"
|
||||
import {findPath, isPathfindingEnabled} from "./pathfinding.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {recalculate} from "./socket.js";
|
||||
import {applyTokenSizeOffset, getSnapPointForEntity, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js";
|
||||
import {applyTokenSizeOffset, enumeratedZip, getSnapPointForEntity, getSnapPointForToken, getSnapPointForTokenObj, getTokenShape, highlightTokenShape, sum} from "./util.js";
|
||||
|
||||
// This is a modified version of Ruler.moveToken from foundry 0.7.9
|
||||
export async function moveEntities(draggedEntity, selectedEntities) {
|
||||
@@ -26,6 +28,9 @@ export async function moveEntities(draggedEntity, selectedEntities) {
|
||||
const hasCollision = selectedEntities.some(token => {
|
||||
const offset = calculateEntityOffset(token, draggedEntity);
|
||||
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));
|
||||
})
|
||||
if (hasCollision) {
|
||||
@@ -89,7 +94,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
|
||||
});
|
||||
await draggedEntity.scene.updateEmbeddedDocuments(draggedEntity.constructor.embeddedName, updates, {animate});
|
||||
if (animate)
|
||||
await Promise.all(entityPaths.map(({entity, path}) => entity.animateMovement(path)));
|
||||
await Promise.all(entityPaths.map(({entity}) => CanvasAnimation.getAnimation(entity.movementAnimationName)?.promise));
|
||||
|
||||
// This is a flag of the "Monk's Active Tile Triggers" module that signals that the movement should be cancelled early
|
||||
if (this.cancelMovement) {
|
||||
@@ -153,14 +158,48 @@ export function cancelScheduledMeasurement() {
|
||||
|
||||
// This is a modified version of Ruler.measure form foundry 0.7.9
|
||||
export function measure(destination, options={}) {
|
||||
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
if (isToken && !this.draggedEntity.isVisible)
|
||||
return []
|
||||
|
||||
options.snap = options.snap ?? !disableSnap;
|
||||
|
||||
if (options.snap) {
|
||||
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
||||
}
|
||||
|
||||
this.dragRulerRemovePathfindingWaypoints();
|
||||
|
||||
if (isToken && isPathfindingEnabled.call(this)) {
|
||||
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
||||
const to = getGridPositionFromPixelsObj(destination);
|
||||
let path = findPath(from, to, this.draggedEntity, this.waypoints);
|
||||
if (path)
|
||||
path.shift();
|
||||
if (path && path.length > 0) {
|
||||
path = path.map(point => getSnapPointForTokenObj(getPixelsFromGridPositionObj(point), this.draggedEntity));
|
||||
|
||||
// If the token is snapped to the grid, the first point of the path is already handled by the ruler
|
||||
if (path[0].x === this.waypoints[this.waypoints.length - 1].x && path[0].y === this.waypoints[this.waypoints.length - 1].y)
|
||||
path = path.slice(1);
|
||||
|
||||
// If snapping is enabled, the last point of the path is already handled by the ruler
|
||||
if (options.snap)
|
||||
path = path.slice(0, path.length - 1);
|
||||
|
||||
for (const point of path) {
|
||||
point.isPathfinding = true;
|
||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||
}
|
||||
this.waypoints = this.waypoints.concat(path);
|
||||
}
|
||||
else {
|
||||
// Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable
|
||||
destination = this.waypoints[this.waypoints.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
if(options.gridSpaces === undefined) {
|
||||
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
||||
}
|
||||
@@ -237,7 +276,7 @@ export function measure(destination, options={}) {
|
||||
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
|
||||
else
|
||||
rulerColor = this.color
|
||||
for (const [s, cs] of zip(segments.reverse(), centeredSegments.reverse())) {
|
||||
for (const [i, s, cs] of enumeratedZip([...segments].reverse(), [...centeredSegments].reverse())) {
|
||||
const { label, text, last } = cs;
|
||||
|
||||
// Draw line segment
|
||||
@@ -270,10 +309,13 @@ export function measure(destination, options={}) {
|
||||
|
||||
// Highlight grid positions
|
||||
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && options.gridSpaces) {
|
||||
if (options.enableTerrainRuler)
|
||||
if (options.enableTerrainRuler) {
|
||||
highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier)
|
||||
else
|
||||
highlightMeasurementNative.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier);
|
||||
}
|
||||
else {
|
||||
const previousSegments = centeredSegments.slice(0, segments.length - 1 - i);
|
||||
highlightMeasurementNative.call(this, cs.ray, previousSegments, shape, opacityMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +328,7 @@ export function measure(destination, options={}) {
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) {
|
||||
export function highlightMeasurementNative(ray, previousSegments, tokenShape=[{x: 0, y: 0}], alpha=1) {
|
||||
const spacer = canvas.scene.data.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1;
|
||||
const nMax = Math.max(Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))), 1);
|
||||
const tMax = Array.fromRange(nMax+1).map(t => t / nMax);
|
||||
@@ -304,9 +346,11 @@ export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0
|
||||
if ( x0 === x1 && y0 === y1 ) continue;
|
||||
|
||||
// Highlight the grid position
|
||||
let [xg, yg] = canvas.grid.grid.getPixelsFromGridPosition(x1, y1);
|
||||
const subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xg, y: yg})}], {gridSpaces: true})[0]
|
||||
const color = this.dragRulerGetColorForDistance(startDistance + subDistance);
|
||||
let [xgtl, ygtl] = 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 distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
||||
const color = this.dragRulerGetColorForDistance(distance);
|
||||
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
||||
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
||||
|
||||
@@ -317,9 +361,11 @@ export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0
|
||||
let th = tMax[i - 1] - (0.5 / nMax);
|
||||
let {x, y} = ray.project(th);
|
||||
let [x1h, y1h] = canvas.grid.grid.getGridPositionFromPixels(x, y);
|
||||
let [xgh, ygh] = canvas.grid.grid.getPixelsFromGridPosition(x1h, y1h);
|
||||
const subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xgh, y: ygh})}], {gridSpaces: true})[0]
|
||||
const color = this.dragRulerGetColorForDistance(startDistance + subDistance);
|
||||
let [xghtl, yghtl] = 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 distance = sum(canvas.grid.measureDistances(pathUntilSpace, {gridSpaces: true}));
|
||||
const color = this.dragRulerGetColorForDistance(distance);
|
||||
const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedEntity);
|
||||
const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1);
|
||||
highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha);
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Functions taken directly from the hex-size-support module (https://github.com/Ourobor/Hex-Size-Support/releases/tag/1.1.0).
|
||||
* Unless otherwise stated, these functions are taken as-is.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Altered version of this function.
|
||||
* - Instead of taking a token as a parameter to retrieve the altOrientationFlag, receive the flag value directly
|
||||
* - Instead of taking a grid parameter, get the grid value from the globas canvas
|
||||
*/
|
||||
export function findVertexSnapPoint(x, y, altOrientationFlag) {
|
||||
const grid = canvas.grid.grid;
|
||||
if (grid.columns) {
|
||||
return findSnapPointCols(x, y, grid.h, grid.w, altOrientationFlag);
|
||||
} else {
|
||||
return findSnapPointRows(x, y, grid.h, grid.w, altOrientationFlag);
|
||||
}
|
||||
}
|
||||
|
||||
function findSnapPointRows(x, y, h, w, alt) {
|
||||
let xOffset = 0.0
|
||||
if (canvas.grid.grid.even) {
|
||||
xOffset = -0.5
|
||||
}
|
||||
|
||||
let yOffset1 = 0.75
|
||||
let yOffset2 = 0.00
|
||||
if (alt) {
|
||||
yOffset1 = 0.25
|
||||
yOffset2 = 1.00
|
||||
}
|
||||
|
||||
let row1 = calculateSnapPointsRows(x, y, h, w, 0.5 + xOffset, yOffset1);
|
||||
let row2 = calculateSnapPointsRows(x, y, h, w, 1.0 + xOffset, yOffset2);
|
||||
|
||||
let dist1 = Math.pow((row1.x - x), 2) + Math.pow((row1.y - y), 2)
|
||||
let dist2 = Math.pow((row2.x - x), 2) + Math.pow((row2.y - y), 2)
|
||||
|
||||
if (dist1 < dist2) {
|
||||
return row1
|
||||
}
|
||||
else {
|
||||
return row2
|
||||
}
|
||||
}
|
||||
|
||||
function calculateSnapPointsRows(x, y, h, w, xOff, yOff) {
|
||||
let c = Math.floor(((x + ((0.5 - xOff) * w)) / w) + 1)
|
||||
let r = Math.floor(((y + ((0.75 - yOff) * h)) / (1.5 * h)) + 1)
|
||||
|
||||
let snapX = (c * w) - ((1 - xOff) * w)
|
||||
let snapY = (r * h * 1.5) - ((1.5 - yOff) * h)
|
||||
|
||||
return {x: snapX, y: snapY}
|
||||
}
|
||||
|
||||
function findSnapPointCols(x, y, h, w, alt) {
|
||||
let yOffset = 0.0
|
||||
if (canvas.grid.grid.even) {
|
||||
yOffset = -0.5
|
||||
}
|
||||
|
||||
let xOffset1 = 0.25
|
||||
let xOffset2 = 1.00
|
||||
if (alt) {
|
||||
xOffset1 = 0.75
|
||||
xOffset2 = 0.00
|
||||
}
|
||||
|
||||
let row1 = calculateSnapPointsCols(x, y, h, w, xOffset1, 0.5 + yOffset);
|
||||
let row2 = calculateSnapPointsCols(x, y, h, w, xOffset2, 1.0 + yOffset);
|
||||
|
||||
let dist1 = Math.pow((row1.x - x), 2) + Math.pow((row1.y - y), 2)
|
||||
let dist2 = Math.pow((row2.x - x), 2) + Math.pow((row2.y - y), 2)
|
||||
|
||||
if (dist1 < dist2) {
|
||||
return row1
|
||||
}
|
||||
else {
|
||||
return row2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function calculateSnapPointsCols(x, y, h, w, xOff, yOff) {
|
||||
let c = Math.floor(((x + ((0.75 - xOff) * w)) / (1.5 * w)) + 1)
|
||||
let r = Math.floor(((y + ((0.5 - yOff) * h)) / h) + 1)
|
||||
|
||||
let snapX = (c * w * 1.5) - ((1.5 - xOff) * w)
|
||||
let snapY = (r * h) - ((1 - yOff) * h)
|
||||
|
||||
return {x: snapX, y: snapY}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {getMeasurePosition, setSnapParameterOnOptions} from "./util.js";
|
||||
|
||||
export let disableSnap = false;
|
||||
export let moveWithoutAnimation = false;
|
||||
export let togglePathfinding = false;
|
||||
|
||||
export function registerKeybindings() {
|
||||
game.keybindings.register(settingsKey, "cancelDrag", {
|
||||
@@ -50,11 +51,20 @@ export function registerKeybindings() {
|
||||
}],
|
||||
precedence: -1,
|
||||
});
|
||||
|
||||
game.keybindings.register(settingsKey, "togglePathfinding", {
|
||||
name: "drag-ruler.keybindings.togglePathfinding.name",
|
||||
hint: "drag-ruler.keybindings.togglePathfinding.hint",
|
||||
onDown: handleTogglePathfinding,
|
||||
onUp: handleTogglePathfinding,
|
||||
precedence: -1,
|
||||
restricted: !game.settings.get(settingsKey, "allowPathfinding"),
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteWaypoint() {
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler.draggedEntity)
|
||||
if (!ruler?.draggedEntity)
|
||||
return false;
|
||||
ruler.dragRulerDeleteWaypoint();
|
||||
return true;
|
||||
@@ -81,7 +91,7 @@ function handleCreateWaypoint() {
|
||||
|
||||
function cancelDrag() {
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler.draggedEntity)
|
||||
if (!ruler?.draggedEntity)
|
||||
return false;
|
||||
ruler.dragRulerAbortDrag();
|
||||
return true;
|
||||
@@ -91,15 +101,40 @@ function handleDisableSnap(event) {
|
||||
disableSnap = !event.up;
|
||||
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (!ruler.isDragRuler)
|
||||
if (!ruler?.isDragRuler)
|
||||
return false;
|
||||
if (ruler._state !== Ruler.STATES.MEASURING)
|
||||
return false;
|
||||
|
||||
ruler.measure(getMeasurePosition(), {snap: !disableSnap});
|
||||
ruler.dragRulerSendState();
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleMoveWithoutAnimation(event) {
|
||||
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) {
|
||||
togglePathfinding = !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;
|
||||
}
|
||||
@@ -7,12 +7,44 @@ import {disableSnap, registerKeybindings} from "./keybindings.js";
|
||||
import {libWrapper} from "./libwrapper_shim.js";
|
||||
import {performMigrations} from "./migration.js"
|
||||
import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
|
||||
import {wipePathfindingCache, initializePathfinding, startBackgroundCaching, terrainRulerWrapper} from "./pathfinding.js";
|
||||
import {extendRuler} from "./ruler.js";
|
||||
import {registerSettings, RightClickAction, settingsKey} from "./settings.js"
|
||||
import {recalculate} from "./socket.js";
|
||||
import {SpeedProvider} from "./speed_provider.js"
|
||||
import {setSnapParameterOnOptions} from "./util.js";
|
||||
|
||||
import initGridlessPathfinding, * as GridlessPathfinding from "../wasm/gridless_pathfinding.js"
|
||||
|
||||
CONFIG.debug.dragRuler = false;
|
||||
export let debugGraphics = undefined;
|
||||
|
||||
initGridlessPathfinding().then(() => {
|
||||
Hooks.on("canvasInit", wipePathfindingCache);
|
||||
Hooks.on("canvasReady", () => {
|
||||
wipePathfindingCache();
|
||||
initializePathfinding();
|
||||
});
|
||||
Hooks.on("createWall", wipePathfindingCache);
|
||||
Hooks.on("updateWall", wipePathfindingCache);
|
||||
Hooks.on("deleteWall", wipePathfindingCache);
|
||||
|
||||
// Whenever the current user selects a token, start caching
|
||||
Hooks.on("controlToken", (token, controlled) => {
|
||||
if (controlled) {
|
||||
startBackgroundCaching(token);
|
||||
}
|
||||
});
|
||||
|
||||
// Whenever a token the current user controls updates, start caching
|
||||
Hooks.on("updateToken", (document) => {
|
||||
const token = document.object;
|
||||
if (token._controlled) {
|
||||
startBackgroundCaching(token);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Hooks.once("init", () => {
|
||||
registerSettings()
|
||||
registerKeybindings()
|
||||
@@ -30,6 +62,9 @@ Hooks.once("init", () => {
|
||||
registerSystem,
|
||||
recalculate,
|
||||
resetMovementHistory,
|
||||
private: {
|
||||
terrainRulerWrapper
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -37,6 +72,8 @@ Hooks.once("ready", () => {
|
||||
performMigrations()
|
||||
checkDependencies();
|
||||
Hooks.callAll("dragRuler.ready", SpeedProvider)
|
||||
if (CONFIG.debug.dragRuler)
|
||||
debugGraphics = canvas.controls.addChild(new PIXI.Container());
|
||||
})
|
||||
|
||||
Hooks.on("canvasReady", () => {
|
||||
@@ -132,6 +169,9 @@ function onEntityDragLeftDrop(event) {
|
||||
// This can happen if the user presses ESC during drag (maybe there are other ways too)
|
||||
if (selectedTokens.length === 0)
|
||||
selectedTokens.push(ruler.draggedEntity);
|
||||
// This can happen if the ruler is being dragged so rapidly that the drag move handler hasn't been called before dropping
|
||||
if (ruler._state === Ruler.STATES.STARTING)
|
||||
onMouseMove.call(ruler, event);
|
||||
ruler._state = Ruler.STATES.MOVING
|
||||
moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
|
||||
return true
|
||||
@@ -0,0 +1,514 @@
|
||||
import {getCenterFromGridPositionObj, getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
||||
import {moveWithoutAnimation, togglePathfinding} from "./keybindings.js";
|
||||
import {debugGraphics} from "./main.js";
|
||||
import {settingsKey} from "./settings.js";
|
||||
import {buildSnapPointTokenData, getSnapPointForTokenDataObj, getTokenShape, getTokenShapeForTokenData, isModuleActive, iterPairs} from "./util.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 use5105 = false;
|
||||
let gridlessPathfinders = new Map();
|
||||
let gridWidth, gridHeight;
|
||||
|
||||
export function isPathfindingEnabled() {
|
||||
if (this.user !== game.user)
|
||||
return false;
|
||||
if (!game.user.isGM && !game.settings.get(settingsKey, "allowPathfinding"))
|
||||
return false;
|
||||
if (moveWithoutAnimation)
|
||||
return false;
|
||||
return game.settings.get(settingsKey, "autoPathfinding") != togglePathfinding;
|
||||
}
|
||||
|
||||
export function findPath(from, to, token, previousWaypoints) {
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
let tokenSize = Math.max(token.data.width, token.data.height) * canvas.dimensions.size;
|
||||
let pathfinder = gridlessPathfinders.get(tokenSize);
|
||||
if (!pathfinder) {
|
||||
let radiusMultiplier = game.settings.get(settingsKey, "pathfindingRadius");
|
||||
// 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);
|
||||
}
|
||||
paintGridlessPathfindingDebug(pathfinder);
|
||||
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 {
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
path.push({x: currentNode.node.x, y: currentNode.node.y});
|
||||
currentNode = currentNode.next;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
export function terrainRulerWrapper(from, to) {
|
||||
// TODO Send list of terrain to terrain layer
|
||||
const ray = new Ray(from, to);
|
||||
return terrainRuler.measureDistances([{ray}])[0] / canvas.dimensions.distance * canvas.dimensions.size;
|
||||
}
|
||||
|
||||
function buildTokenData(token) {
|
||||
// Almost all the information we need is for calculating the snap point
|
||||
const tokenData = buildSnapPointTokenData(token);
|
||||
|
||||
// If Wall Height is enabled, which walls matter depends on the token's elevation.
|
||||
// Depending on the settings in Wall Height, the height we care about is either their
|
||||
// foot height (elevation) or eye height (losHeight).
|
||||
if (isModuleActive("wall-height")) {
|
||||
const blockSightMovement = game.settings.get("wall-height", "blockSightMovement");
|
||||
tokenData.elevation = blockSightMovement ? token.losHeight : token.data.elevation;
|
||||
}
|
||||
|
||||
return tokenData;
|
||||
}
|
||||
|
||||
function getNode(pos, cacheLayer, initialize = true) {
|
||||
const node = cacheLayer.nodes[pos.y][pos.x];
|
||||
if (initialize && !node.edges) {
|
||||
node.edges = [];
|
||||
for (const neighborPos of canvas.grid.grid.getNeighbors(pos.y, pos.x).map(([y, x]) => {return {x, y};})) {
|
||||
if (neighborPos.x < 0 || neighborPos.y < 0 || neighborPos.x >= gridWidth || neighborPos.y >= gridHeight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO Work with pixels instead of grid locations
|
||||
if (!stepCollidesWithWall(pos, neighborPos, cacheLayer.tokenData)) {
|
||||
const isDiagonal = node.x !== neighborPos.x && node.y !== neighborPos.y && canvas.grid.type === CONST.GRID_TYPES.SQUARE;
|
||||
let edgeCost;
|
||||
if (window.terrainRuler) {
|
||||
let ray = new Ray(getCenterFromGridPositionObj(pos), getCenterFromGridPositionObj(neighborPos));
|
||||
let measuredDistance = terrainRuler.measureDistances([{ray}], {costFunction: buildCostFunction(cacheLayer.tokenData, getTokenShapeForTokenData(cacheLayer.tokenData))})[0];
|
||||
edgeCost = Math.round(measuredDistance / canvas.dimensions.distance);
|
||||
if (ray.terrainRulerFinalState?.noDiagonals === 1) {
|
||||
edgeCost = 1.5;
|
||||
}
|
||||
// Charge 1.0001 instead of 1 for diagonals to discourage unnecessary diagonals
|
||||
if (isDiagonal && edgeCost == 1) {
|
||||
edgeCost = 1.0001;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Count 5-10-5 diagonals as 1.5 (so two add up to 3) and 5-5-5 diagonals as 1.0001 (to discourage unnecessary diagonals)
|
||||
// TODO Account for difficult terrain
|
||||
edgeCost = isDiagonal ? (use5105 ? 1.5 : 1.0001) : 1;
|
||||
}
|
||||
const neighbor = getNode(neighborPos, cacheLayer, false);
|
||||
node.edges.push({target: neighbor, cost: edgeCost});
|
||||
}
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function calculatePath(from, to, cacheLayer, previousWaypoints) {
|
||||
use5105 = game.system.id === "pf2e" || canvas.grid.diagonalRule === "5105";
|
||||
let startCost = 0;
|
||||
if (use5105 && canvas.grid.type === CONST.GRID_TYPES.SQUARE) {
|
||||
previousWaypoints = previousWaypoints.map(w => getGridPositionFromPixelsObj(w));
|
||||
startCost = (calcNoDiagonals(previousWaypoints) % 2) * 0.5;
|
||||
}
|
||||
|
||||
const nextNodes = new PriorityQueueSet((node1, node2) => node1.node === node2.node, node => node.estimated);
|
||||
const previousNodes = new Set();
|
||||
|
||||
nextNodes.pushWithPriority(
|
||||
{
|
||||
node: getNode(from, cacheLayer),
|
||||
cost: startCost,
|
||||
estimated: startCost + estimateCost(from, to),
|
||||
previous: null
|
||||
}
|
||||
);
|
||||
|
||||
while (nextNodes.hasNext()) {
|
||||
// Get node with cheapest estimate
|
||||
const currentNode = nextNodes.pop();
|
||||
if (currentNode.node.x === to.x && currentNode.node.y === to.y) {
|
||||
return buildPathNodes(currentNode);
|
||||
}
|
||||
previousNodes.add(currentNode.node);
|
||||
for (const edge of currentNode.node.edges) {
|
||||
const neighborNode = getNode(edge.target, cacheLayer);
|
||||
if (previousNodes.has(neighborNode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const neighbor = {
|
||||
node: neighborNode,
|
||||
cost: currentNode.cost + edge.cost,
|
||||
estimated: currentNode.cost + edge.cost + estimateCost(neighborNode, to),
|
||||
previous: currentNode
|
||||
};
|
||||
nextNodes.pushWithPriority(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Now we've found the path, we know the final node, and each node links to the previous one.
|
||||
* Reverse this list and return the first node in the path, with each node linking to the next
|
||||
*/
|
||||
function buildPathNodes(lastNode) {
|
||||
let currentNode = lastNode;
|
||||
let previousNode = null;
|
||||
while (currentNode) {
|
||||
const pathNode = {
|
||||
node: currentNode.node,
|
||||
cost: currentNode.cost,
|
||||
next: previousNode
|
||||
}
|
||||
previousNode = pathNode;
|
||||
currentNode = currentNode.previous;
|
||||
}
|
||||
return previousNode;
|
||||
}
|
||||
|
||||
function calcNoDiagonals(waypoints) {
|
||||
let diagonals = 0;
|
||||
for (const [p1, p2] of iterPairs(waypoints)) {
|
||||
diagonals += Math.min(Math.abs(p1.x - p2.x), Math.abs(p1.y - p2.y));
|
||||
}
|
||||
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) {
|
||||
const distX = Math.abs(pos.x - target.x);
|
||||
const distY = Math.abs(pos.y - target.y);
|
||||
return Math.max(distX, distY) + (use5105 ? Math.min(distX, distY) * 0.5 : 0);
|
||||
}
|
||||
|
||||
function stepCollidesWithWall(from, to, tokenData) {
|
||||
const stepStart = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(from), tokenData);
|
||||
const stepEnd = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(to), tokenData);
|
||||
if (isModuleActive("levels")) {
|
||||
stepStart.z = tokenData.elevation;
|
||||
stepEnd.z = tokenData.elevation;
|
||||
return _levels.testCollision(stepStart, stepEnd, "collision")
|
||||
} else {
|
||||
return canvas.walls.checkCollision(new Ray(stepStart, stepEnd));
|
||||
}
|
||||
}
|
||||
|
||||
export function wipePathfindingCache() {
|
||||
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)
|
||||
return;
|
||||
|
||||
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||
let currentNode = firstNode;
|
||||
while (currentNode) {
|
||||
let text = new PIXI.Text(currentNode.cost.toFixed(1));
|
||||
let pixels = getSnapPointForTokenDataObj(getPixelsFromGridPositionObj(currentNode.node), tokenData);
|
||||
text.anchor.set(0.5, 1.0);
|
||||
text.x = pixels.x;
|
||||
text.y = pixels.y;
|
||||
debugGraphics.addChild(text);
|
||||
currentNode = currentNode.next;
|
||||
}
|
||||
}
|
||||
|
||||
function paintGridlessPathfindingDebug(pathfinder) {
|
||||
if (!CONFIG.debug.dragRuler)
|
||||
return;
|
||||
|
||||
debugGraphics.removeChildren().forEach(c => c.destroy());
|
||||
let graphic = new PIXI.Graphics();
|
||||
graphic.lineStyle(2, 0x440000);
|
||||
for (const point of GridlessPathfinding.debugGetPathfindingPoints(pathfinder)) {
|
||||
graphic.drawCircle(point.x, point.y, 5);
|
||||
}
|
||||
debugGraphics.addChild(graphic);
|
||||
}
|
||||
@@ -32,8 +32,13 @@ export function extendRuler() {
|
||||
const json = super.toJSON();
|
||||
if (this.draggedEntity) {
|
||||
const isToken = this.draggedEntity instanceof Token;
|
||||
json["draggedEntityIsToken"] = isToken;
|
||||
json["draggedEntity"] = this.draggedEntity.id;
|
||||
json.draggedEntityIsToken = isToken;
|
||||
json.draggedEntity = this.draggedEntity.id;
|
||||
json.waypoints = json.waypoints.map(old => {
|
||||
let w = duplicate(old);
|
||||
w.isPathfinding = undefined;
|
||||
return w;
|
||||
});
|
||||
}
|
||||
return json;
|
||||
}
|
||||
@@ -49,12 +54,18 @@ export function extendRuler() {
|
||||
else
|
||||
this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
||||
}
|
||||
else {
|
||||
this.draggedEntity = undefined;
|
||||
}
|
||||
|
||||
super.update(data);
|
||||
}
|
||||
|
||||
measure(destination, options={}) {
|
||||
if (this.isDragRuler) {
|
||||
// If this is the ruler of a remote user take the waypoints as they were transmitted and don't apply any additional snapping to them
|
||||
if (this.user !== game.user)
|
||||
options.snap = false;
|
||||
return measure.call(this, destination, options);
|
||||
}
|
||||
else {
|
||||
@@ -75,6 +86,7 @@ export function extendRuler() {
|
||||
}
|
||||
this.waypoints.push(new PIXI.Point(point.x, point.y));
|
||||
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
||||
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(waypoint => waypoint.isPathfinding = false);
|
||||
}
|
||||
|
||||
dragRulerAddWaypointHistory(waypoints) {
|
||||
@@ -91,6 +103,7 @@ export function extendRuler() {
|
||||
}
|
||||
|
||||
dragRulerDeleteWaypoint(event={preventDefault: () => {return}}, options={}) {
|
||||
this.dragRulerRemovePathfindingWaypoints();
|
||||
options.snap = options.snap ?? true;
|
||||
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
|
||||
event.preventDefault();
|
||||
@@ -107,6 +120,11 @@ export function extendRuler() {
|
||||
}
|
||||
}
|
||||
|
||||
dragRulerRemovePathfindingWaypoints() {
|
||||
this.waypoints.filter(waypoint => waypoint.isPathfinding).forEach(_ => this.labels.removeChild(this.labels.children[this.labels.children.length - 1]).destroy());
|
||||
this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
|
||||
}
|
||||
|
||||
dragRulerAbortDrag(event={preventDefault: () => {return}}) {
|
||||
const token = this.draggedEntity;
|
||||
this._endMeasurement();
|
||||
@@ -185,6 +203,12 @@ export function extendRuler() {
|
||||
if (measureImmediately)
|
||||
ruler.measure(destination, options);
|
||||
}
|
||||
|
||||
dragRulerSendState() {
|
||||
game.user.broadcastActivity({
|
||||
ruler: this.toJSON()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ruler = DragRulerRuler;
|
||||
@@ -1,5 +1,7 @@
|
||||
import {availableSpeedProviders, currentSpeedProvider, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
|
||||
import {SpeedProvider} from "./speed_provider.js"
|
||||
import {wipePathfindingCache} from "./pathfinding.js"
|
||||
import { early_isGM } from "./util.js";
|
||||
|
||||
export const settingsKey = "drag-ruler";
|
||||
|
||||
@@ -9,6 +11,10 @@ export const RightClickAction = Object.freeze({
|
||||
ABORT_DRAG:2,
|
||||
});
|
||||
|
||||
function delayedReload() {
|
||||
window.setTimeout(() => location.reload(), 500);
|
||||
}
|
||||
|
||||
export function registerSettings() {
|
||||
game.settings.register(settingsKey, "dataVersion", {
|
||||
scope: "world",
|
||||
@@ -82,6 +88,33 @@ export function registerSettings() {
|
||||
default: true,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "allowPathfinding", {
|
||||
name: "drag-ruler.settings.allowPathfinding.name",
|
||||
hint: "drag-ruler.settings.allowPathfinding.hint",
|
||||
scope: "world",
|
||||
config: true,
|
||||
type: Boolean,
|
||||
default: false,
|
||||
onChange: delayedReload,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "autoPathfinding", {
|
||||
name: "drag-ruler.settings.autoPathfinding.name",
|
||||
hint: "drag-ruler.settings.autoPathfinding.hint",
|
||||
scope: "client",
|
||||
config: early_isGM() || game.settings.get(settingsKey, "allowPathfinding"),
|
||||
type: Boolean,
|
||||
default: false,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "pathfindingRadius", {
|
||||
scope: "world",
|
||||
config: false,
|
||||
type: Number,
|
||||
default: 0.9,
|
||||
onChange: wipePathfindingCache,
|
||||
});
|
||||
|
||||
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
|
||||
config: false,
|
||||
type: Number,
|
||||
@@ -39,5 +39,7 @@ export function recalculate(tokens) {
|
||||
}
|
||||
|
||||
function _socketRecalculate(tokenIds) {
|
||||
return canvas.controls.ruler.dragRulerRecalculate(tokenIds);
|
||||
const ruler = canvas.controls.ruler;
|
||||
if (ruler.isDragRuler)
|
||||
ruler.dragRulerRecalculate(tokenIds);
|
||||
}
|
||||
@@ -4,19 +4,23 @@ export function getDefaultSpeedAttribute() {
|
||||
return "actor.data.data.attribs.mov.value";
|
||||
case "dcc":
|
||||
return "actor.data.data.attributes.speed.value";
|
||||
case "dnd4e":
|
||||
return "actor.data.data.movement.walk.value";
|
||||
case "dnd5e":
|
||||
return "actor.data.data.attributes.movement.walk"
|
||||
return "actor.data.data.attributes.movement.walk";
|
||||
case "lancer":
|
||||
return "actor.data.data.derived.speed"
|
||||
return "actor.data.data.derived.speed";
|
||||
case "pf1":
|
||||
case "D35E":
|
||||
return "actor.data.data.attributes.speed.land.total"
|
||||
return "actor.data.data.attributes.speed.land.total";
|
||||
case "sfrpg":
|
||||
return "actor.data.data.attributes.speed.value";
|
||||
case "shadowrun5e":
|
||||
return "actor.data.data.movement.walk.value";
|
||||
case "swade":
|
||||
return "actor.data.data.stats.speed.adjusted";
|
||||
case "ds4":
|
||||
return "actor.data.data.combatValues.movement.total";
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -26,15 +30,17 @@ export function getDefaultDashMultiplier() {
|
||||
case "swade":
|
||||
return 0
|
||||
case "dcc":
|
||||
case "dnd4e":
|
||||
case "dnd5e":
|
||||
case "lancer":
|
||||
case "pf1":
|
||||
case "D35E":
|
||||
case "sfrpg":
|
||||
case "shadowrun5e":
|
||||
return 2
|
||||
case "ds4":
|
||||
return 2;
|
||||
case "CoC7":
|
||||
return 5;
|
||||
}
|
||||
return 0
|
||||
return 0;
|
||||
}
|
||||
+82
-36
@@ -1,5 +1,6 @@
|
||||
import {getPixelsFromGridPosition} from "./foundry_fixes.js"
|
||||
import { disableSnap } from "./keybindings.js";
|
||||
import {findVertexSnapPoint} from "./hex_support.js";
|
||||
import {disableSnap} from "./keybindings.js";
|
||||
|
||||
export function* zip(it1, it2) {
|
||||
for (let i = 0;i < Math.min(it1.length, it2.length);i++) {
|
||||
@@ -7,14 +8,56 @@ export function* zip(it1, it2) {
|
||||
}
|
||||
}
|
||||
|
||||
export function* enumeratedZip(it1, it2) {
|
||||
let i = 0;
|
||||
for (const [v1, v2] of zip(it1, it2)) {
|
||||
yield [i, v1, v2];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
export function* iterPairs(l) {
|
||||
for (let i = 1;i < l.length;i++) {
|
||||
yield [l[i - 1], l[i]];
|
||||
}
|
||||
}
|
||||
|
||||
export function sum(arr) {
|
||||
return arr.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
export function buildSnapPointTokenData(token) {
|
||||
const tokenData = {
|
||||
width: token.data.width,
|
||||
height: token.data.height
|
||||
};
|
||||
|
||||
if (isModuleActive("hex-size-support")) {
|
||||
tokenData.hexSizeSupport = {};
|
||||
tokenData.hexSizeSupport.altSnappingFlag = CONFIG.hexSizeSupport.getAltSnappingFlag(token);
|
||||
tokenData.hexSizeSupport.altOrientationFlag = CONFIG.hexSizeSupport.getAltOrientationFlag(token);
|
||||
tokenData.hexSizeSupport.borderSize = token.document.getFlag("hex-size-support", "borderSize");
|
||||
}
|
||||
|
||||
return tokenData;
|
||||
}
|
||||
|
||||
export function getSnapPointForToken(x, y, token) {
|
||||
return getSnapPointForTokenData(x, y, buildSnapPointTokenData(token));
|
||||
}
|
||||
|
||||
export function getSnapPointForTokenDataObj(pos, tokenData) {
|
||||
return getSnapPointForTokenData(pos.x, pos.y, tokenData);
|
||||
}
|
||||
|
||||
function getSnapPointForTokenData(x, y, tokenData) {
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return new PIXI.Point(x, y);
|
||||
}
|
||||
if (canvas.grid.isHex) {
|
||||
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
||||
if (token.document.getFlag("hex-size-support", "borderSize") % 2 === 0) {
|
||||
const snapPoint = CONFIG.hexSizeSupport.findVertexSnapPoint(x, y, token, canvas.grid.grid)
|
||||
if (tokenData.hexSizeSupport?.altSnappingFlag) {
|
||||
if (tokenData.hexSizeSupport.borderSize % 2 === 0) {
|
||||
const snapPoint = findVertexSnapPoint(x, y, tokenData.hexSizeSupport.altOrientationFlag);
|
||||
return new PIXI.Point(snapPoint.x, snapPoint.y)
|
||||
}
|
||||
else {
|
||||
@@ -28,38 +71,38 @@ export function getSnapPointForToken(x, y, token) {
|
||||
|
||||
const [topLeftX, topLeftY] = canvas.grid.getTopLeft(x, y);
|
||||
let cellX, cellY;
|
||||
if (token.data.width % 2 === 0)
|
||||
if (tokenData.width % 2 === 0)
|
||||
cellX = x - canvas.grid.h / 2;
|
||||
else
|
||||
cellX = x;
|
||||
if (token.data.height % 2 === 0)
|
||||
if (tokenData.height % 2 === 0)
|
||||
cellY = y - canvas.grid.h / 2;
|
||||
else
|
||||
cellY = y;
|
||||
const [centerX, centerY] = canvas.grid.getCenter(cellX, cellY);
|
||||
let snapX, snapY;
|
||||
// Tiny tokens can snap to the cells corners
|
||||
if (token.data.width <= 0.5) {
|
||||
if (tokenData.width <= 0.5) {
|
||||
const offsetX = x - topLeftX;
|
||||
const subGridWidth = Math.floor(canvas.grid.w / 2);
|
||||
const subGridPosX = Math.floor(offsetX / subGridWidth);
|
||||
snapX = topLeftX + (subGridPosX + 0.5) * subGridWidth;
|
||||
}
|
||||
// Tokens with odd multipliers (1x1, 3x3, ...) and tokens smaller than 1x1 but bigger than 0.5x0.5 snap to the center of the grid cell
|
||||
else if (Math.round(token.data.width) % 2 === 1 || token.data.width < 1) {
|
||||
else if (Math.round(tokenData.width) % 2 === 1 || tokenData.width < 1) {
|
||||
snapX = centerX;
|
||||
}
|
||||
// All remaining tokens (those with even or fractional multipliers on square grids) snap to the intersection points of the grid
|
||||
else {
|
||||
snapX = centerX + canvas.grid.w / 2;
|
||||
}
|
||||
if (token.data.height <= 0.5) {
|
||||
if (tokenData.height <= 0.5) {
|
||||
const offsetY = y - topLeftY;
|
||||
const subGridHeight = Math.floor(canvas.grid.h / 2);
|
||||
const subGridPosY = Math.floor(offsetY / subGridHeight);
|
||||
snapY = topLeftY + (subGridPosY + 0.5) * subGridHeight;
|
||||
}
|
||||
else if (Math.round(token.data.height) % 2 === 1 || token.data.height < 1) {
|
||||
else if (Math.round(tokenData.height) % 2 === 1 || tokenData.height < 1) {
|
||||
snapY = centerY;
|
||||
}
|
||||
else {
|
||||
@@ -68,27 +111,15 @@ export function getSnapPointForToken(x, y, token) {
|
||||
return new PIXI.Point(snapX, snapY);
|
||||
}
|
||||
|
||||
export function getSnapPointForTokenObj(pos, token) {
|
||||
return getSnapPointForToken(pos.x, pos.y, token);
|
||||
}
|
||||
|
||||
export function getSnapPointForMeasuredTemplate(x, y) {
|
||||
if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return new PIXI.Point(x, y);
|
||||
}
|
||||
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);
|
||||
return canvas.grid.grid.getSnappedPosition(x, y, canvas.templates.gridPrecision);
|
||||
}
|
||||
|
||||
export function getSnapPointForEntity(x, y, entity) {
|
||||
@@ -136,15 +167,19 @@ export function getAreaFromPositionAndShape(position, shape) {
|
||||
}
|
||||
|
||||
export function getTokenShape(token) {
|
||||
if (token.scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return getTokenShapeForTokenData(buildSnapPointTokenData(token), token.scene);
|
||||
}
|
||||
|
||||
export function getTokenShapeForTokenData(tokenData, scene=canvas.scene) {
|
||||
if (scene.data.gridType === CONST.GRID_TYPES.GRIDLESS) {
|
||||
return [{x: 0, y: 0}]
|
||||
}
|
||||
else if (token.scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
|
||||
const topOffset = -Math.floor(token.data.height / 2)
|
||||
const leftOffset = -Math.floor(token.data.width / 2)
|
||||
else if (scene.data.gridType === CONST.GRID_TYPES.SQUARE) {
|
||||
const topOffset = -Math.floor(tokenData.height / 2)
|
||||
const leftOffset = -Math.floor(tokenData.width / 2)
|
||||
const shape = []
|
||||
for (let y = 0;y < token.data.height;y++) {
|
||||
for (let x = 0;x < token.data.width;x++) {
|
||||
for (let y = 0;y < tokenData.height;y++) {
|
||||
for (let x = 0;x < tokenData.width;x++) {
|
||||
shape.push({x: x + leftOffset, y: y + topOffset})
|
||||
}
|
||||
}
|
||||
@@ -152,8 +187,8 @@ export function getTokenShape(token) {
|
||||
}
|
||||
else {
|
||||
// Hex grids
|
||||
if (game.modules.get("hex-size-support")?.active && CONFIG.hexSizeSupport.getAltSnappingFlag(token)) {
|
||||
const borderSize = token.data.flags["hex-size-support"].borderSize;
|
||||
if (game.modules.get("hex-size-support")?.active && tokenData.hexSizeSupport.altSnappingFlag) {
|
||||
const borderSize = tokenData.hexSizeSupport.borderSize;
|
||||
let shape = [{x: 0, y: 0}];
|
||||
if (borderSize >= 2)
|
||||
shape = shape.concat([{x: 0, y: -1}, {x: -1, y: -1}]);
|
||||
@@ -162,7 +197,7 @@ export function getTokenShape(token) {
|
||||
if (borderSize >= 4)
|
||||
shape = shape.concat([{x: -2, y: -1}, {x: 1, y: -1}, {x: -1, y: -2}, {x: 0, y: -2}, {x: 1, y: -2}])
|
||||
|
||||
if (Boolean(CONFIG.hexSizeSupport.getAltOrientationFlag(token)) !== canvas.grid.grid.options.columns)
|
||||
if (Boolean(tokenData.hexSizeSupport.altOrientationFlag) !== canvas.grid.grid.options.columns)
|
||||
shape.forEach(space => space.y *= -1);
|
||||
if (canvas.grid.grid.options.columns)
|
||||
shape = shape.map(space => {return {x: space.y, y: space.x}});
|
||||
@@ -250,3 +285,14 @@ export function getMeasurePosition() {
|
||||
const measurePosition = {x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y};
|
||||
return measurePosition;
|
||||
}
|
||||
|
||||
// isGM function for use during loading when game.user isn't available yet
|
||||
export function early_isGM() {
|
||||
const level = game.data.users.find(u => u._id == game.data.userId).role;
|
||||
const gmLevel = CONST.USER_ROLES.ASSISTANT;
|
||||
return level >= gmLevel;
|
||||
}
|
||||
|
||||
export function isModuleActive(moduleName) {
|
||||
return game.modules.get(moduleName)?.active
|
||||
}
|
||||
@@ -40,9 +40,17 @@
|
||||
"moveWithoutAnimation": {
|
||||
"name": "Token animation deaktivieren",
|
||||
"hint": "Wenn diese Taste gedrückt wird, während ein Token fallen gelassen wird, bewegt es sich ohne Animation zum Zielort."
|
||||
},
|
||||
"togglePathfinding": {
|
||||
"name": "Wegfindung umschalten",
|
||||
"hint": "Wenn diese Taste gedrückt gehalten wird, während ein Token gezogen wird, wird die Wegfindung vorübergehend aktiviert/deaktiviert"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"allowPathfinding": {
|
||||
"name": "Wegfindung für Spieler erlauben",
|
||||
"hint": "Erlaubt es Spielern die Wegfindungs zu benutzen. Bitte beachte, dass die Wegfindung Wege durch unerkundeten Nebel des Kriegs und Ätherische Wände berechnen kann. Dies kann deinen Spielern Geheimnisse lüften, von denen sie noch nicht erfahren sollten."
|
||||
},
|
||||
"alwaysShowSpeedForPCs": {
|
||||
"name": "Geschwindigkeit von Spielercharakteren für jeden anzeigen",
|
||||
"hint": "Wenn diese Einstellung aktiviert ist wird die Färbung der hervorgehobenen Felder bei Spielercharakteren allen Spielern angezeigt, selbst wenn diese keinen Zugriff auf den Charakterbogen haben."
|
||||
|
||||
@@ -40,9 +40,17 @@
|
||||
"moveWithoutAnimation": {
|
||||
"name": "Disable token animation",
|
||||
"hint": "When being held while dropping a token, the token will move to the target location without animating"
|
||||
},
|
||||
"togglePathfinding": {
|
||||
"name": "Toggle pathfinding",
|
||||
"hint": "When being held while dragging a token, the pathfinding functionality will be temporarily enabled/disabled"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"allowPathfinding": {
|
||||
"name": "Allow pathfinding for players",
|
||||
"hint": "Allows players to use Drag Ruler's pathfinding functionality in this world. Be aware that pathfinding can route through unexplored fog of war and Ethereal Walls, which might reveal secrets to your players ahead of time."
|
||||
},
|
||||
"alwaysShowSpeedForPCs": {
|
||||
"name": "Show PC speed to everyone",
|
||||
"hint": "If enabled the coloring based on actor speed for player characters will shown to everyone, even if they don't have observer permission for the character sheet."
|
||||
@@ -51,6 +59,10 @@
|
||||
"name": "Automatically start measuring",
|
||||
"hint": "If enabled, Drag Ruler will start measuring as soon as the token is being dragged. If disabled, Drag Ruler will remain inactive and will only start measuring once the button to add a waypoint is being pressed."
|
||||
},
|
||||
"autoPathfinding": {
|
||||
"name": "Pathfinding by default",
|
||||
"hint": "If enabled, dragging a token will automatically use a pathfinding ruler"
|
||||
},
|
||||
"enableMovementHistory": {
|
||||
"name": "Enable movement history during combat",
|
||||
"hint": "If enabled, Drag Ruler will remember the path a token took during it's turn in combat and will display it when you pick the token back up."
|
||||
|
||||
@@ -33,7 +33,28 @@
|
||||
"dash": "correr"
|
||||
}
|
||||
},
|
||||
"keybindings": {
|
||||
"cancelDrag": "Cancelar arrastre",
|
||||
"createWaypoint": "Crear punto de ruta",
|
||||
"deleteWaypoint": "Borrar punto de ruta",
|
||||
"disableSnap": {
|
||||
"name": "Deshabilitar ajuste",
|
||||
"hint": "El ajuste a la rejilla se deshabilitará temporalmente mientras se presione esta tecla"
|
||||
},
|
||||
"moveWithoutAnimation": {
|
||||
"name": "Deshabilitar animación de icono",
|
||||
"hint": "Si al soltar un icono se presiona esta tecla, se deshabilitará la animación del icono al moverse al destino"
|
||||
},
|
||||
"togglePathfinding": {
|
||||
"name": "Conmutar búsqueda de camino",
|
||||
"hint": "Cuando se presione al arrastrar un icono, la funcionalidad de búsqueda de camino será temporalmente habilitada/deshabilitada"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"allowPathfinding": {
|
||||
"name": "Permitir búsqueda de camino a los jugadores",
|
||||
"hint": "Permite a los jugadores usar en este mundo la funcionalidad de búsqueda de camino. Tenga cuidado porque la ruta puede transcurrir por lugares con niebla de guerra o muros invisibles, lo que podrá revelar algunos secretos a los jugadores antes de tiempo"
|
||||
},
|
||||
"alwaysShowSpeedForPCs": {
|
||||
"name": "Mostrar velocidad de los PJs a todo el mundo",
|
||||
"hint": "Si se habilita, se mostrará a todo el mundo los códigos de colores de las rutas de los PJs, incluso si no tienen permisos de observador para ese personaje"
|
||||
@@ -42,10 +63,23 @@
|
||||
"name": "Comenzar a medir automáticamente",
|
||||
"hint": "Si se habilita, Drag Ruler comenzará a medir en cuanto se comience a arrastrar un icono. Si se deshabilita, Drag Ruler permanecerá inactivo y comenzará a medir únicamente cuando se presione el botón configurado para añadir un nuevo punto de ruta"
|
||||
},
|
||||
"autoPathfinding": {
|
||||
"name": "Búsqueda de camino por defecto",
|
||||
"hint": "Si se habilita, al arrastrar un icono se usará automáticamente la regla de búsqueda de camino"
|
||||
},
|
||||
"enableMovementHistory": {
|
||||
"name": "Habilitar historial de movimiento durante el combate",
|
||||
"hint": "Si se habilita, Drag Ruler recordará la ruta que ha seguido un icono en su turno y la mostrará al seleccionarlo de nuevo"
|
||||
},
|
||||
"rightClickAction": {
|
||||
"name": "Acción del botón derecho",
|
||||
"hint": "Qué acción se llevará a cabo cuando se haga clic derecho mientras se arrastra un icono",
|
||||
"choices": {
|
||||
"create": "Crear punto de ruta",
|
||||
"delete": "Borrar punto de ruta",
|
||||
"cancel": "Cancelar arrastre"
|
||||
}
|
||||
},
|
||||
"showGMRulerToPlayers": {
|
||||
"name": "Mostrar regla del GM a los jugadores",
|
||||
"hint": "Si se deshabilita, no se mostrará la regla de medición de los GMs al resto de usuarios"
|
||||
|
||||
+6
-6
@@ -2,8 +2,8 @@
|
||||
"name": "drag-ruler",
|
||||
"title": "Drag Ruler",
|
||||
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
|
||||
"version": "1.10.0",
|
||||
"minimumCoreVersion" : "9.238",
|
||||
"version": "1.12.8",
|
||||
"minimumCoreVersion" : "9.245",
|
||||
"compatibleCoreVersion" : "9",
|
||||
"authors": [
|
||||
{
|
||||
@@ -13,9 +13,9 @@
|
||||
}
|
||||
],
|
||||
"esmodules": [
|
||||
"src/libwrapper_shim.js",
|
||||
"src/main.js",
|
||||
"src/socket.js"
|
||||
"js/libwrapper_shim.js",
|
||||
"js/main.js",
|
||||
"js/socket.js"
|
||||
],
|
||||
"templates": [
|
||||
"speed_provider_settings.html"
|
||||
@@ -65,7 +65,7 @@
|
||||
],
|
||||
"socket": true,
|
||||
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
|
||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.10.0.zip",
|
||||
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/releases/download/v1.12.8/drag-ruler-1.12.8.zip",
|
||||
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
|
||||
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
|
||||
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
target/
|
||||
Generated
+232
@@ -0,0 +1,232 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gridless-pathfinding"
|
||||
version = "1.12.8"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
"rustc-hash",
|
||||
"sha1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.56"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2"
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "gridless-pathfinding"
|
||||
version = "1.12.8"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
#debug = true
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1.7"
|
||||
js-sys = "0.3.56"
|
||||
rustc-hash = "1.1.0"
|
||||
sha1 = "0.10.1"
|
||||
wasm-bindgen = "0.2.79"
|
||||
@@ -0,0 +1 @@
|
||||
hard_tabs = true
|
||||
@@ -0,0 +1,253 @@
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
pub type JsPoint;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn x(this: &JsPoint) -> f64;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn y(this: &JsPoint) -> f64;
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Point {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
pub fn new(x: f64, y: f64) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub fn from_line_x(line: &Line, x: f64) -> Self {
|
||||
let y = line.calc_y(x);
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
pub fn distance_to(&self, to: Point) -> f64 {
|
||||
(self.y - to.y).hypot(self.x - to.x)
|
||||
}
|
||||
|
||||
pub fn is_same_as(&self, other: &Self) -> bool {
|
||||
let e = 0.000001;
|
||||
(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 PartialEq for Point {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.x == other.x && self.y == other.y
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Point {
|
||||
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||
self.x.to_bits().hash(hasher);
|
||||
self.y.to_bits().hash(hasher);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&JsPoint> for Point {
|
||||
fn from(point: &JsPoint) -> Self {
|
||||
Self::new(point.x(), point.y())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Line {
|
||||
pub m: f64,
|
||||
pub b: f64,
|
||||
pub p1: Point,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
pub fn new(m: f64, b: f64, p1: Point) -> Self {
|
||||
Self { m, b, p1 }
|
||||
}
|
||||
|
||||
pub fn from_points(p1: Point, p2: Point) -> Self {
|
||||
let m = (p1.y - p2.y) / (p1.x - p2.x);
|
||||
let b = p1.y - m * p1.x;
|
||||
Self { m, b, p1 }
|
||||
}
|
||||
|
||||
pub fn from_point_and_angle(p1: Point, angle: f64) -> Self {
|
||||
let p2 = Point {
|
||||
x: p1.x - angle.cos(),
|
||||
y: p1.y - angle.sin(),
|
||||
};
|
||||
Line::from_points(p1, p2)
|
||||
}
|
||||
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
self.m.is_infinite()
|
||||
}
|
||||
|
||||
pub fn is_horizontal(&self) -> bool {
|
||||
self.m == 0.0
|
||||
}
|
||||
|
||||
pub fn calc_x(&self, y: f64) -> f64 {
|
||||
(y - self.b) / self.m
|
||||
}
|
||||
|
||||
pub fn calc_y(&self, x: f64) -> f64 {
|
||||
self.m * x + self.b
|
||||
}
|
||||
|
||||
pub fn intersection(&self, other: &Line) -> Option<Point> {
|
||||
// Are both lines vertical?
|
||||
if self.is_vertical() && other.is_vertical() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Are the lines paralell?
|
||||
if (self.m - other.m).abs() < 0.00000005 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Is one of the lines vertical?
|
||||
if self.is_vertical() || other.is_vertical() {
|
||||
let vertical;
|
||||
let regular;
|
||||
if self.is_vertical() {
|
||||
vertical = self;
|
||||
regular = other;
|
||||
} else {
|
||||
vertical = other;
|
||||
regular = self;
|
||||
}
|
||||
return Some(Point::from_line_x(regular, vertical.p1.x));
|
||||
}
|
||||
|
||||
// Calculate x coordinate of intersection point between both lines
|
||||
// Find intersection point: x * m1 + b1 = x * m2 + b2
|
||||
// Solve for x: x = (b1 - b2) / (m2 - m1)
|
||||
let x = (self.b - other.b) / (other.m - self.m);
|
||||
if self.m.abs() < other.m.abs() {
|
||||
Some(Point::from_line_x(self, x))
|
||||
} else {
|
||||
Some(Point::from_line_x(other, x))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_perpendicular_through_point(&self, p: Point) -> Self {
|
||||
let m = -1.0 / self.m;
|
||||
let b = p.y - m * p.x;
|
||||
Self { m, b, p1: p }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LineSegment {
|
||||
pub p1: Point,
|
||||
pub p2: Point,
|
||||
pub line: Line,
|
||||
}
|
||||
|
||||
impl LineSegment {
|
||||
pub fn new(p1: Point, p2: Point) -> Self {
|
||||
Self {
|
||||
p1,
|
||||
p2,
|
||||
line: Line::from_points(p1, p2),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersection(&self, other: &LineSegment) -> Option<Point> {
|
||||
let intersection = self.line.intersection(&other.line);
|
||||
intersection.filter(|intersection| {
|
||||
self.is_intersection_on_segment(*intersection)
|
||||
&& other.is_intersection_on_segment(*intersection)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_intersection_on_segment(&self, intersection: Point) -> bool {
|
||||
if intersection.is_same_as(&self.p1) || intersection.is_same_as(&self.p2) {
|
||||
return true;
|
||||
}
|
||||
if self.line.is_vertical() || self.line.m.abs() > 1.0 {
|
||||
return between(intersection.y, self.p1.y, self.p2.y);
|
||||
}
|
||||
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 {
|
||||
let (min, max) = if a < b { (a, b) } else { (b, a) };
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
use js_sys::Array;
|
||||
use sha1::{Digest, Sha1};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
geometry::{Circle, LineSegment, Point, Rectangle},
|
||||
pathfinder::{DiscoveredNodePtr, Pathfinder},
|
||||
util::Windows,
|
||||
};
|
||||
|
||||
#[allow(unused)]
|
||||
macro_rules! log {
|
||||
( $( $t:tt )* ) => {
|
||||
log(&format!( $( $t )* ));
|
||||
};
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console, js_name=warn)]
|
||||
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]
|
||||
extern "C" {
|
||||
pub type JsWall;
|
||||
pub type JsWallData;
|
||||
pub type JsWallFlags;
|
||||
pub type JsWallHeight;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn data(this: &JsWall) -> JsWallData;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn c(this: &JsWallData) -> Vec<f64>;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = "door")]
|
||||
fn door_type(this: &JsWallData) -> DoorType;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = "ds")]
|
||||
fn door_state(this: &JsWallData) -> DoorState;
|
||||
|
||||
#[wasm_bindgen(method, getter, js_name = "move")]
|
||||
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]
|
||||
extern "C" {
|
||||
pub type JsPoint;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn x(this: &JsPoint) -> f64;
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
fn y(this: &JsPoint) -> f64;
|
||||
}
|
||||
|
||||
impl From<JsPoint> for Point {
|
||||
fn from(point: JsPoint) -> Self {
|
||||
Point {
|
||||
x: point.x(),
|
||||
y: point.y(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum DoorState {
|
||||
CLOSED = 0,
|
||||
OPEN = 1,
|
||||
LOCKED = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<usize> for DoorState {
|
||||
type Error = ();
|
||||
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
x if x == Self::CLOSED as usize => Ok(Self::CLOSED),
|
||||
x if x == Self::OPEN as usize => Ok(Self::OPEN),
|
||||
x if x == Self::LOCKED as usize => Ok(Self::LOCKED),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum DoorType {
|
||||
NONE = 0,
|
||||
DOOR = 1,
|
||||
SECRET = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<usize> for DoorType {
|
||||
type Error = ();
|
||||
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
x if x == Self::NONE as usize => Ok(Self::NONE),
|
||||
x if x == Self::DOOR as usize => Ok(Self::DOOR),
|
||||
x if x == Self::SECRET as usize => Ok(Self::SECRET),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum WallSenseType {
|
||||
NONE = 0,
|
||||
LIMITED = 10,
|
||||
NORMAL = 20,
|
||||
}
|
||||
|
||||
impl TryFrom<usize> for WallSenseType {
|
||||
type Error = ();
|
||||
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
x if x == Self::NONE as usize => Ok(Self::NONE),
|
||||
x if x == Self::LIMITED as usize => Ok(Self::LIMITED),
|
||||
x if x == Self::NORMAL as usize => Ok(Self::NORMAL),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)]
|
||||
pub struct Wall {
|
||||
pub p1: Point,
|
||||
pub p2: Point,
|
||||
pub door_type: DoorType,
|
||||
pub door_state: DoorState,
|
||||
pub move_type: WallSenseType,
|
||||
pub height: WallHeight,
|
||||
}
|
||||
|
||||
impl Wall {
|
||||
pub fn new(
|
||||
p1: Point,
|
||||
p2: Point,
|
||||
door_type: DoorType,
|
||||
door_state: DoorState,
|
||||
move_type: WallSenseType,
|
||||
height: WallHeight,
|
||||
) -> Self {
|
||||
Self {
|
||||
p1,
|
||||
p2,
|
||||
door_type,
|
||||
door_state,
|
||||
move_type,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_door(&self) -> bool {
|
||||
self.door_type != DoorType::NONE
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.door_state == DoorState::OPEN
|
||||
}
|
||||
}
|
||||
|
||||
impl Wall {
|
||||
fn from_js(wall: &JsWall, enable_height: bool) -> Self {
|
||||
let data = wall.data();
|
||||
let mut c = data.c();
|
||||
c.iter_mut().for_each(|val| *val = val.round());
|
||||
let height = if enable_height {
|
||||
data.flags().wall_height().into()
|
||||
} else {
|
||||
WallHeight::default()
|
||||
};
|
||||
Self::new(
|
||||
Point::new(c[0], c[1]),
|
||||
Point::new(c[2], c[3]),
|
||||
data.door_type(),
|
||||
data.door_state(),
|
||||
data.move_type(),
|
||||
height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen]
|
||||
pub fn initialize(
|
||||
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());
|
||||
for wall in js_walls {
|
||||
let wall = JsWall::from(wall);
|
||||
walls.push(Wall::from_js(&wall, enable_height));
|
||||
}
|
||||
let terrain = js_terrain.iter().map(|terrain| terrain.into()).collect();
|
||||
Pathfinder::initialize(walls, terrain, token_size, token_elevation)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen]
|
||||
pub fn free(pathfinder: Pathfinder) {
|
||||
drop(pathfinder);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen(js_name=findPath)]
|
||||
pub fn find_path(pathfinder: &mut Pathfinder, from: JsPoint, to: JsPoint) -> Option<Array> {
|
||||
pathfinder
|
||||
.find_path(from.into(), to.into())
|
||||
.map(|first_node| first_node.iter_path().map(JsValue::from).collect())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen(js_name=debugGetPathfindingPoints)]
|
||||
pub fn debug_get_pathfinding_points(pathfinder: &Pathfinder) -> Array {
|
||||
pathfinder
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|node| node.borrow().point)
|
||||
.map(JsValue::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[wasm_bindgen]
|
||||
pub fn sha1(input: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(input);
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
trait IteratePath {
|
||||
fn iter_path(&self) -> PathIterator;
|
||||
}
|
||||
|
||||
impl IteratePath for DiscoveredNodePtr {
|
||||
fn iter_path(&self) -> PathIterator {
|
||||
PathIterator {
|
||||
current_node: Some(self.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PathIterator {
|
||||
current_node: Option<DiscoveredNodePtr>,
|
||||
}
|
||||
|
||||
impl Iterator for PathIterator {
|
||||
type Item = Point;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(node) = self.current_node.clone() {
|
||||
let point = node.borrow().node.borrow().point;
|
||||
self.current_node = node.borrow().previous.clone();
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
mod geometry;
|
||||
#[macro_use]
|
||||
mod js_api;
|
||||
mod pathfinder;
|
||||
mod ptr_indexed_hash_set;
|
||||
mod util;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main() {
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
use std::{cell::RefCell, f64::consts::PI, rc::Rc};
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{
|
||||
geometry::{LineSegment, Point, Rectangle},
|
||||
js_api::{distance_with_terrain, Terrain, TerrainShape, Wall, WallSenseType, log},
|
||||
ptr_indexed_hash_set::PtrIndexedHashSet,
|
||||
};
|
||||
|
||||
pub struct Edge {
|
||||
target: NodePtr,
|
||||
cost: f64,
|
||||
}
|
||||
|
||||
pub struct Node {
|
||||
pub point: Point,
|
||||
edges: Option<Vec<Edge>>,
|
||||
final_edge: Option<Option<Edge>>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn new(point: Point) -> Self {
|
||||
Self {
|
||||
point,
|
||||
edges: None,
|
||||
final_edge: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn iter_edges(
|
||||
&self,
|
||||
) -> std::iter::Chain<std::slice::Iter<'_, Edge>, std::option::Iter<'_, Edge>> {
|
||||
self.edges
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.chain(self.final_edge.as_ref().unwrap().iter())
|
||||
}
|
||||
}
|
||||
|
||||
type NodePtr = Rc<RefCell<Node>>;
|
||||
|
||||
impl From<Node> for NodePtr {
|
||||
fn from(node: Node) -> Self {
|
||||
Rc::new(RefCell::new(node))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiscoveredNode {
|
||||
pub node: NodePtr,
|
||||
cost: f64,
|
||||
estimated: f64,
|
||||
pub previous: Option<DiscoveredNodePtr>,
|
||||
}
|
||||
|
||||
pub type DiscoveredNodePtr = Rc<RefCell<DiscoveredNode>>;
|
||||
|
||||
impl From<DiscoveredNode> for DiscoveredNodePtr {
|
||||
fn from(node: DiscoveredNode) -> Self {
|
||||
Rc::new(RefCell::new(node))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct NodeStorage {
|
||||
regular_nodes: Vec<NodePtr>,
|
||||
final_node: Option<NodePtr>,
|
||||
}
|
||||
|
||||
pub type NodeStorageIterator<'a> = std::iter::Chain<
|
||||
std::slice::Iter<'a, Rc<RefCell<Node>>>,
|
||||
std::option::Iter<'a, Rc<RefCell<Node>>>,
|
||||
>;
|
||||
|
||||
impl NodeStorage {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn push(&mut self, node: NodePtr) {
|
||||
self.regular_nodes.push(node);
|
||||
}
|
||||
|
||||
fn initialize_edges(&mut self, node: &NodePtr, walls: &[LineSegment], terrain: &[Rectangle]) {
|
||||
if node.borrow().final_edge.is_none() {
|
||||
let final_edge = self
|
||||
.final_node
|
||||
.as_ref()
|
||||
.filter(|neighbor| {
|
||||
!self.collides_with_wall(
|
||||
&LineSegment::new(node.borrow().point, neighbor.borrow().point),
|
||||
walls,
|
||||
)
|
||||
})
|
||||
.map(|neighbor| Edge {
|
||||
target: neighbor.clone(),
|
||||
cost: Self::measure_distance(
|
||||
node.borrow().point,
|
||||
neighbor.borrow().point,
|
||||
terrain,
|
||||
),
|
||||
});
|
||||
node.borrow_mut().final_edge = Some(final_edge);
|
||||
}
|
||||
|
||||
if node.borrow().edges.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let point = node.borrow().point;
|
||||
let mut edges = Vec::new();
|
||||
for neighbor in &self.regular_nodes {
|
||||
if Rc::ptr_eq(neighbor, node) {
|
||||
continue;
|
||||
}
|
||||
let neighbor_point = neighbor.borrow().point;
|
||||
if !self.collides_with_wall(&LineSegment::new(point, neighbor_point), walls) {
|
||||
let cost =
|
||||
Self::measure_distance(node.borrow().point, neighbor.borrow().point, terrain);
|
||||
edges.push(Edge {
|
||||
target: neighbor.clone(),
|
||||
cost,
|
||||
});
|
||||
}
|
||||
}
|
||||
node.borrow_mut().edges = Some(edges);
|
||||
}
|
||||
|
||||
fn collides_with_wall(&self, line: &LineSegment, walls: &[LineSegment]) -> bool {
|
||||
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) {
|
||||
for node in &self.regular_nodes {
|
||||
node.borrow_mut().final_edge = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> NodeStorageIterator {
|
||||
self.regular_nodes.iter().chain(self.final_node.iter())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Pathfinder {
|
||||
#[wasm_bindgen(skip)]
|
||||
pub nodes: NodeStorage,
|
||||
#[wasm_bindgen(skip)]
|
||||
pub walls: Vec<LineSegment>,
|
||||
#[wasm_bindgen(skip)]
|
||||
pub terrain: Vec<Rectangle>,
|
||||
}
|
||||
|
||||
impl Pathfinder {
|
||||
pub fn initialize(
|
||||
walls: Vec<Wall>,
|
||||
terrain: Vec<Terrain>,
|
||||
token_size: f64,
|
||||
token_elevation: f64,
|
||||
) -> Self {
|
||||
log!("{:#?}", terrain);
|
||||
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();
|
||||
|
||||
for segments in walls.iter().chain(polygon_terrain.iter()) {
|
||||
let x_diff = segments.p2.x - segments.p1.x;
|
||||
let y_diff = segments.p2.y - segments.p1.y;
|
||||
let p1_angle = y_diff.atan2(x_diff).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)] {
|
||||
let angles = endpoints.entry(point).or_insert_with(Vec::new);
|
||||
angles.push(angle);
|
||||
}
|
||||
}
|
||||
endpoints
|
||||
.values_mut()
|
||||
.for_each(|angles| angles.sort_by(|a, b| a.partial_cmp(b).unwrap()));
|
||||
let mut nodes = NodeStorage::new();
|
||||
for (point, angles) in endpoints {
|
||||
assert!(!angles.is_empty());
|
||||
for i in 1..angles.len() {
|
||||
let angle1 = angles[i - 1];
|
||||
let angle2 = angles[i];
|
||||
if angle1 == angle2 {
|
||||
continue;
|
||||
}
|
||||
let angle_diff = angle2 - angle1;
|
||||
if angle_diff <= PI {
|
||||
continue;
|
||||
}
|
||||
let angle_between = angle_diff / 2.0 + angle1;
|
||||
nodes.push(calc_pathfinding_node(
|
||||
point,
|
||||
angle_between,
|
||||
distance_from_walls,
|
||||
&mut walls,
|
||||
));
|
||||
nodes.push(calc_pathfinding_node(
|
||||
point,
|
||||
angle1 + 0.5 * PI,
|
||||
distance_from_walls,
|
||||
&mut walls,
|
||||
));
|
||||
nodes.push(calc_pathfinding_node(
|
||||
point,
|
||||
angle2 - 0.5 * PI,
|
||||
distance_from_walls,
|
||||
&mut walls,
|
||||
));
|
||||
}
|
||||
let angle1 = angles.last().unwrap();
|
||||
let angle2 = angles.first().unwrap() + 2.0 * PI;
|
||||
let angle_diff = angle2 - angle1;
|
||||
if angle_diff <= PI {
|
||||
continue;
|
||||
}
|
||||
let angle_between = angle_diff / 2.0 + angle1;
|
||||
nodes.push(calc_pathfinding_node(
|
||||
point,
|
||||
angle_between,
|
||||
distance_from_walls,
|
||||
&mut walls,
|
||||
));
|
||||
nodes.push(calc_pathfinding_node(
|
||||
point,
|
||||
angle1 + 0.5 * PI,
|
||||
distance_from_walls,
|
||||
&mut walls,
|
||||
));
|
||||
nodes.push(calc_pathfinding_node(
|
||||
point,
|
||||
angle2 - 0.5 * PI,
|
||||
distance_from_walls,
|
||||
&mut walls,
|
||||
));
|
||||
}
|
||||
|
||||
// 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
|
||||
Self {
|
||||
nodes,
|
||||
walls,
|
||||
terrain: terrain.iter().map(|terrain| terrain.bounding_box).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_path(&mut self, from: Point, to: Point) -> Option<DiscoveredNodePtr> {
|
||||
self.nodes.cleanup_final_edges();
|
||||
let mut nodes = self.nodes.clone();
|
||||
nodes.final_node = Some(NodePtr::from(Node::new(from)));
|
||||
let to_node = NodePtr::from(Node::new(to));
|
||||
nodes.initialize_edges(&to_node, &self.walls, &self.terrain);
|
||||
let to = DiscoveredNode {
|
||||
node: to_node,
|
||||
cost: 0.0,
|
||||
estimated: to.distance_to(from),
|
||||
previous: None,
|
||||
};
|
||||
// TODO Use a sorted set for next_nodes for better performance
|
||||
let mut next_nodes = vec![DiscoveredNodePtr::from(to)];
|
||||
let mut previous_nodes = PtrIndexedHashSet::new();
|
||||
while !next_nodes.is_empty() {
|
||||
// Sort by estimated cost, high to low
|
||||
// TODO Maybe tere's a faster way to do this than re-sorting every iteration?
|
||||
next_nodes.sort_by(|a, b| {
|
||||
b.borrow()
|
||||
.estimated
|
||||
.partial_cmp(&a.borrow().estimated)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
// Get node with cheapest estimate
|
||||
let current_node = next_nodes.pop().unwrap();
|
||||
if current_node.borrow().node.borrow().point.x == from.x
|
||||
&& current_node.borrow().node.borrow().point.y == from.y
|
||||
{
|
||||
return Some(current_node);
|
||||
}
|
||||
previous_nodes.insert(current_node.borrow().node.clone());
|
||||
for edge in current_node.borrow().node.borrow().iter_edges() {
|
||||
let neighbor = &edge.target;
|
||||
if previous_nodes.contains(neighbor) {
|
||||
continue;
|
||||
}
|
||||
nodes.initialize_edges(neighbor, &self.walls, &self.terrain);
|
||||
// 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 discovered_neighbor = DiscoveredNode {
|
||||
node: neighbor.clone(),
|
||||
cost,
|
||||
estimated: cost + neighbor.borrow().point.distance_to(from),
|
||||
previous: Some(current_node.clone()),
|
||||
};
|
||||
let neighbor_entry = next_nodes
|
||||
.iter()
|
||||
.find(|node| Rc::ptr_eq(&node.borrow().node, neighbor));
|
||||
if let Some(entry) = neighbor_entry {
|
||||
// If the neighbor is cheaper to reach via the current route than through previously discovered routes, replace it
|
||||
if entry.borrow().cost > cost {
|
||||
*entry.borrow_mut() = discovered_neighbor;
|
||||
}
|
||||
} else {
|
||||
next_nodes.push(discovered_neighbor.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn calc_pathfinding_node(
|
||||
p: Point,
|
||||
angle: f64,
|
||||
distance_from_walls: f64,
|
||||
walls: &mut Vec<LineSegment>,
|
||||
) -> NodePtr {
|
||||
let offset_x = angle.cos() * distance_from_walls;
|
||||
let offset_y = angle.sin() * distance_from_walls;
|
||||
walls.push(LineSegment::new(
|
||||
p,
|
||||
Point {
|
||||
x: p.x + offset_x * 0.99,
|
||||
y: p.y + offset_y * 0.99,
|
||||
},
|
||||
));
|
||||
NodePtr::from(Node::new(Point {
|
||||
x: p.x + offset_x,
|
||||
y: p.y + offset_y,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::collections::hash_set;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PtrIndexedHashSet<T>(FxHashSet<PtrIndexedRc<T>>);
|
||||
|
||||
impl<T> PtrIndexedHashSet<T> {
|
||||
pub fn new() -> Self {
|
||||
PtrIndexedHashSet(FxHashSet::default())
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, value: Rc<T>) -> bool {
|
||||
self.0.insert(PtrIndexedRc(value))
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, value: &Rc<T>) -> bool {
|
||||
self.0.remove(&PtrIndexedRc(Rc::clone(value)))
|
||||
}
|
||||
|
||||
pub fn contains(&mut self, value: &Rc<T>) -> bool {
|
||||
self.0.contains(&PtrIndexedRc(Rc::clone(value)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Debug> std::fmt::Debug for PtrIndexedHashSet<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_set().entries(self.0.iter().map(|e| &e.0)).finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct PtrIndexedRc<T>(Rc<T>);
|
||||
|
||||
impl<T> Hash for PtrIndexedRc<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
Rc::as_ptr(&self.0).hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq for PtrIndexedRc<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Rc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for PtrIndexedRc<T> {}
|
||||
|
||||
pub struct PtrIndexedHashSetIterator<'a, T>(hash_set::Iter<'a, PtrIndexedRc<T>>);
|
||||
|
||||
impl<'a, T> Iterator for PtrIndexedHashSetIterator<'a, T> {
|
||||
type Item = &'a Rc<T>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.0.next() {
|
||||
Some(item) => Some(&item.0),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> IntoIterator for &'a PtrIndexedHashSet<T> {
|
||||
type Item = &'a Rc<T>;
|
||||
type IntoIter = PtrIndexedHashSetIterator<'a, T>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
PtrIndexedHashSetIterator::<T>((&self.0).iter())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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