506 lines
16 KiB
JavaScript
506 lines
16 KiB
JavaScript
import {
|
|
currentSpeedProvider,
|
|
getColorForDistanceAndToken,
|
|
getRangesFromSpeedProvider,
|
|
} from "./api.js";
|
|
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
|
|
import {getGridPositionFromPixelsObj, getPixelsFromGridPositionObj} from "./foundry_fixes.js";
|
|
import {cancelScheduledMeasurement, highlightMeasurementNative} from "./foundry_imports.js";
|
|
import {disableSnap} from "./keybindings.js";
|
|
import {getMovementHistory} from "./movement_tracking.js";
|
|
import {settingsKey} from "./settings.js";
|
|
import {
|
|
applyTokenSizeOffset,
|
|
getSnapPointForEntity,
|
|
getSnapPointForTokenObj,
|
|
getEntityCenter,
|
|
getTokenShape,
|
|
isPathfindingEnabled,
|
|
} from "./util.js";
|
|
import {getPointer} from "./util.js";
|
|
|
|
export function extendRuler() {
|
|
class DragRulerRuler extends CONFIG.Canvas.rulerClass {
|
|
// Functions below are overridden versions of functions in Ruler
|
|
constructor(user, {color = null} = {}) {
|
|
super(user, {color});
|
|
this.previousWaypoints = [];
|
|
this.previousLabels = this.addChild(new PIXI.Container());
|
|
}
|
|
|
|
clear() {
|
|
super.clear();
|
|
this.previousWaypoints = [];
|
|
this.previousLabels.removeChildren().forEach(c => c.destroy());
|
|
this.dragRulerRanges = undefined;
|
|
cancelScheduledMeasurement.call(this);
|
|
}
|
|
|
|
async moveToken(event) {
|
|
// Disable moveToken if Drag Ruler is active
|
|
if (!this.isDragRuler) return await super.moveToken(event);
|
|
}
|
|
|
|
toJSON() {
|
|
const json = super.toJSON();
|
|
if (this.draggedEntity) {
|
|
const isToken = this.draggedEntity instanceof Token;
|
|
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;
|
|
}
|
|
|
|
update(data) {
|
|
// Don't show a GMs drag ruler to non GM players
|
|
if (
|
|
data.draggedEntity &&
|
|
this.user.isGM &&
|
|
!game.user.isGM &&
|
|
!game.settings.get(settingsKey, "showGMRulerToPlayers")
|
|
)
|
|
return;
|
|
|
|
if (data.draggedEntity) {
|
|
if (data.draggedEntityIsToken) this.draggedEntity = canvas.tokens.get(data.draggedEntity);
|
|
else this.draggedEntity = canvas.templates.get(data.draggedEntity);
|
|
} else {
|
|
this.draggedEntity = undefined;
|
|
}
|
|
|
|
super.update(data);
|
|
}
|
|
|
|
measure(destination, options = {}) {
|
|
if (!this.isDragRuler) {
|
|
return super.measure(destination, options);
|
|
}
|
|
if (options.gridSpaces === undefined) {
|
|
options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS;
|
|
}
|
|
this.dragRulerGridSpaces = options.gridSpaces;
|
|
const isToken = this.draggedEntity instanceof Token;
|
|
if (isToken && !this.draggedEntity.isVisible) {
|
|
return [];
|
|
}
|
|
if (canvas.grid.diagonalRule === "EUCL") {
|
|
options.gridSpaces = false;
|
|
options.ignoreGrid = true;
|
|
}
|
|
if (options.ignoreGrid === undefined) {
|
|
options.ignoreGrid = false;
|
|
}
|
|
this.dragRulerIgnoreGrid = options.ignoreGrid;
|
|
// 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;
|
|
}
|
|
this.dragRulerSnap = options.snap ?? !disableSnap;
|
|
this.dragRulerEnableTerrainRuler = isToken && window.terrainRuler;
|
|
|
|
// Compute the measurement destination, segments, and distance
|
|
const d = this._getMeasurementDestination(destination);
|
|
if (d.x === this.destination.x && d.y === this.destination.y) return;
|
|
this.destination = d;
|
|
|
|
// TODO Check if we can reuse the old path
|
|
this.dragRulerRemovePathfindingWaypoints();
|
|
if (this.pathfindingJob) {
|
|
routinglib.cancelPathfinding(this.pathfindingJob);
|
|
this.pathfindingJob = undefined;
|
|
}
|
|
|
|
if (isToken && isPathfindingEnabled.call(this)) {
|
|
// TODO Show a busy indicator
|
|
const from = getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]);
|
|
const to = getGridPositionFromPixelsObj(destination);
|
|
|
|
const pathfindingJob = routinglib.calculatePath(from, to, {token: this.draggedEntity});
|
|
this.pathfindingJob = pathfindingJob;
|
|
return this.pathfindingJob.then(result => {
|
|
if (pathfindingJob === this.pathfindingJob) {
|
|
this.pathfindingJob = undefined;
|
|
this.addPathToWaypoints(result?.path);
|
|
return this.performPostPathfindingActions(options);
|
|
}
|
|
});
|
|
}
|
|
|
|
return this.performPostPathfindingActions(options);
|
|
}
|
|
|
|
addPathToWaypoints(path) {
|
|
if (!path) {
|
|
// TODO Show an indicator informing that there is no path
|
|
|
|
// Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable
|
|
this.destination = this.waypoints[this.waypoints.length - 1];
|
|
return;
|
|
}
|
|
|
|
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 (this.dragRulerSnap) {
|
|
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);
|
|
}
|
|
|
|
performPostPathfindingActions(options) {
|
|
// TODO Clear pathfinding busy indicator
|
|
this.segments = this._getMeasurementSegments();
|
|
this._computeDistance(options.gridSpaces);
|
|
|
|
// Draw the ruler graphic
|
|
this.ruler.clear();
|
|
this._drawMeasuredPath();
|
|
|
|
// Draw grid highlight
|
|
this.highlightLayer.clear();
|
|
const isToken = this.draggedEntity instanceof Token;
|
|
if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && this.dragRulerGridSpaces) {
|
|
const shape = getTokenShape(this.draggedEntity);
|
|
if (!this.dragRulerEnableTerrainRuler) {
|
|
for (const [i, segment] of [...this.segments].reverse().entries()) {
|
|
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
|
const previousSegments = this.segments.slice(0, this.segments.length - 1 - i);
|
|
highlightMeasurementNative.call(
|
|
this,
|
|
segment.ray,
|
|
previousSegments,
|
|
shape,
|
|
opacityMultiplier,
|
|
);
|
|
}
|
|
} else {
|
|
for (const segment of [...this.segments].reverse()) {
|
|
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
|
highlightMeasurementTerrainRuler.call(
|
|
this,
|
|
segment.ray,
|
|
segment.startDistance,
|
|
shape,
|
|
opacityMultiplier,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return this.segments;
|
|
}
|
|
|
|
_getMeasurementDestination(destination) {
|
|
if (this.isDragRuler) {
|
|
if (this.dragRulerSnap) {
|
|
return getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
|
|
} else {
|
|
return destination;
|
|
}
|
|
} else {
|
|
return super._getMeasurementDestination(destination);
|
|
}
|
|
}
|
|
|
|
_getMeasurementSegments() {
|
|
if (this.isDragRuler) {
|
|
const unsnappedWaypoints = this.waypoints.concat([this.destination]);
|
|
const waypoints =
|
|
this.draggedEntity instanceof Token
|
|
? applyTokenSizeOffset(unsnappedWaypoints, this.draggedEntity)
|
|
: duplicate(unsnappedWaypoints);
|
|
const unsnappedSegments = [];
|
|
const segments = [];
|
|
for (const [i, p1] of waypoints.entries()) {
|
|
if (i === 0) continue;
|
|
const unsnappedP1 = unsnappedWaypoints[i];
|
|
const p0 = waypoints[i - 1];
|
|
const unsnappedP0 = unsnappedWaypoints[i - 1];
|
|
const label = this.labels.children[i - 1];
|
|
const ray = new Ray(p0, p1);
|
|
const unsnappedRay = new Ray(unsnappedP0, unsnappedP1);
|
|
ray.isPrevious = Boolean(unsnappedP0.isPrevious);
|
|
unsnappedRay.isPrevious = ray.isPrevious;
|
|
ray.dragRulerVisitedSpaces = unsnappedP0.dragRulerVisitedSpaces;
|
|
unsnappedRay.dragRulerVisitedSpaces = ray.dragRulerVisitedSpaces;
|
|
ray.dragRulerFinalState = unsnappedP0.dragRulerFinalState;
|
|
unsnappedRay.dragRulerFinalState = ray.dragRulerFinalState;
|
|
if (ray.distance < 10) {
|
|
if (label) label.visible = false;
|
|
continue;
|
|
}
|
|
segments.push({ray, label});
|
|
unsnappedSegments.push({ray: unsnappedRay, label});
|
|
}
|
|
this.dragRulerUnsnappedSegments = unsnappedSegments;
|
|
return segments;
|
|
} else {
|
|
return super._getMeasurementSegments();
|
|
}
|
|
}
|
|
|
|
_computeDistance(gridSpaces) {
|
|
if (!this.isDragRuler) {
|
|
return super._computeDistance(gridSpaces);
|
|
}
|
|
if (!this.dragRulerEnableTerrainRuler) {
|
|
if (!this.dragRulerIgnoreGrid) {
|
|
gridSpaces = true;
|
|
}
|
|
super._computeDistance(gridSpaces);
|
|
} else {
|
|
const shape = this.draggedEntity ? getTokenShape(this.draggedEntity) : null;
|
|
const options = {
|
|
ignoreGrid: this.dragRulerIgnoreGrid,
|
|
gridSpaces,
|
|
enableTerrainRuler: this.dragRulerEnableTerrainRuler,
|
|
};
|
|
const distances = measureDistances(this.segments, this.draggedEntity, shape, options);
|
|
let totalDistance = 0;
|
|
for (const [i, d] of distances.entries()) {
|
|
let s = this.segments[i];
|
|
s.startDistance = totalDistance;
|
|
totalDistance += d;
|
|
s.last = i === this.segments.length - 1;
|
|
s.distance = d;
|
|
s.text = this._getSegmentLabel(s, totalDistance);
|
|
}
|
|
}
|
|
for (const [i, segment] of this.segments.entries()) {
|
|
const unsnappedSegment = this.dragRulerUnsnappedSegments[i];
|
|
unsnappedSegment.startDistance = segment.startDistance;
|
|
unsnappedSegment.last = segment.last;
|
|
unsnappedSegment.distance = segment.distance;
|
|
unsnappedSegment.text = segment.text;
|
|
}
|
|
}
|
|
|
|
_drawMeasuredPath() {
|
|
if (!this.isDragRuler) {
|
|
return super._drawMeasuredPath();
|
|
}
|
|
let rulerColor = this.color;
|
|
if (!this.dragRulerGridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) {
|
|
const totalDistance = this.segments.reduce((total, current) => total + current.distance, 0);
|
|
rulerColor = this.dragRulerGetColorForDistance(totalDistance);
|
|
}
|
|
const r = this.ruler.beginFill(rulerColor, 0.25);
|
|
for (const segment of this.dragRulerUnsnappedSegments) {
|
|
const opacityMultiplier = segment.ray.isPrevious ? 0.33 : 1;
|
|
const {ray, distance, label, text, last} = segment;
|
|
if (distance === 0) continue;
|
|
|
|
// Draw Line
|
|
r.moveTo(ray.A.x, ray.A.y)
|
|
.lineStyle(6, 0x000000, 0.5 * opacityMultiplier)
|
|
.lineTo(ray.B.x, ray.B.y)
|
|
.lineStyle(4, rulerColor, 0.25 * opacityMultiplier)
|
|
.moveTo(ray.A.x, ray.A.y)
|
|
.lineTo(ray.B.x, ray.B.y);
|
|
|
|
// Draw Waypoints
|
|
r.lineStyle(2, 0x000000, 0.5).drawCircle(ray.A.x, ray.A.y, 8);
|
|
if (last) r.drawCircle(ray.B.x, ray.B.y, 8);
|
|
|
|
// Draw Label
|
|
if (label) {
|
|
label.text = text;
|
|
label.alpha = last ? 1.0 : 0.5;
|
|
label.visible = true;
|
|
let labelPosition = ray.project((ray.distance + 50) / ray.distance);
|
|
label.position.set(labelPosition.x, labelPosition.y);
|
|
}
|
|
}
|
|
r.endFill();
|
|
}
|
|
|
|
_endMeasurement() {
|
|
super._endMeasurement();
|
|
this.draggedEntity = null;
|
|
if (this.pathfindingJob) {
|
|
routinglib.cancelPathfinding(this.pathfindingJob);
|
|
this.pathfindingJob = undefined;
|
|
}
|
|
}
|
|
|
|
// The functions below aren't present in the orignal Ruler class and are added by Drag Ruler
|
|
dragRulerAddWaypoint(point, options = {}) {
|
|
options.snap = options.snap ?? true;
|
|
if (options.snap) {
|
|
point = getSnapPointForEntity(point.x, point.y, this.draggedEntity);
|
|
}
|
|
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) {
|
|
waypoints.forEach(waypoint => (waypoint.isPrevious = true));
|
|
this.waypoints = this.waypoints.concat(waypoints);
|
|
for (const waypoint of waypoints) {
|
|
this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle));
|
|
}
|
|
}
|
|
|
|
dragRulerClearWaypoints() {
|
|
this.waypoints = [];
|
|
this.labels.removeChildren().forEach(c => c.destroy());
|
|
}
|
|
|
|
dragRulerDeleteWaypoint(
|
|
event = {
|
|
preventDefault: () => {
|
|
return;
|
|
},
|
|
},
|
|
options = {},
|
|
) {
|
|
this.dragRulerRemovePathfindingWaypoints();
|
|
options.snap = options.snap ?? true;
|
|
if (this.waypoints.filter(w => !w.isPrevious).length > 1) {
|
|
event.preventDefault();
|
|
const mousePosition = getPointer().getLocalPosition(
|
|
canvas.tokens,
|
|
);
|
|
const rulerOffset = this.rulerOffset;
|
|
|
|
// Options are not passed to _removeWaypoint in vanilla Foundry.
|
|
// Send them in case other modules have overriden that behavior and accept an options parameter (Toggle Snap to Grid)
|
|
this._removeWaypoint(
|
|
{x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y},
|
|
options,
|
|
);
|
|
game.user.broadcastActivity({ruler: this});
|
|
} else {
|
|
this.dragRulerAbortDrag(event);
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
// Deactivate the drag workflow in mouse
|
|
token.mouseInteractionManager._deactivateDragEvents();
|
|
token.mouseInteractionManager.state = token.mouseInteractionManager.states.HOVER;
|
|
|
|
// This will cancel the current drag operation
|
|
// Pass in a fake event that hopefully is enough to allow other modules to function
|
|
token._onDragLeftCancel(event);
|
|
}
|
|
|
|
async dragRulerRecalculate(tokenIds) {
|
|
if (this._state !== Ruler.STATES.MEASURING) return;
|
|
if (tokenIds && !tokenIds.includes(this.draggedEntity.id)) return;
|
|
const waypoints = this.waypoints.filter(waypoint => !waypoint.isPrevious);
|
|
this.dragRulerClearWaypoints();
|
|
if (game.settings.get(settingsKey, "enableMovementHistory"))
|
|
this.dragRulerAddWaypointHistory(getMovementHistory(this.draggedEntity));
|
|
for (const waypoint of waypoints) {
|
|
this.dragRulerAddWaypoint(waypoint, {snap: false});
|
|
}
|
|
this.measure(this.destination);
|
|
game.user.broadcastActivity({ruler: this});
|
|
}
|
|
|
|
static dragRulerGetRaysFromWaypoints(waypoints, destination) {
|
|
if (destination) waypoints = waypoints.concat([destination]);
|
|
return waypoints.slice(1).map((wp, i) => {
|
|
const ray = new Ray(waypoints[i], wp);
|
|
ray.isPrevious = Boolean(waypoints[i].isPrevious);
|
|
return ray;
|
|
});
|
|
}
|
|
|
|
dragRulerGetColorForDistance(distance) {
|
|
if (!this.isDragRuler) return this.color;
|
|
if (!this.draggedEntity.actor) {
|
|
return this.color;
|
|
}
|
|
// Don't apply colors if the current user doesn't have at least observer permissions
|
|
if (this.draggedEntity.actor.permission < 2) {
|
|
// If this is a pc and alwaysShowSpeedForPCs is enabled we show the color anyway
|
|
if (
|
|
!(
|
|
this.draggedEntity.actor.type === "character" &&
|
|
game.settings.get(settingsKey, "alwaysShowSpeedForPCs")
|
|
)
|
|
)
|
|
return this.color;
|
|
}
|
|
distance = Math.round(distance * 100) / 100;
|
|
if (!this.dragRulerRanges)
|
|
this.dragRulerRanges = getRangesFromSpeedProvider(this.draggedEntity);
|
|
return (
|
|
getColorForDistanceAndToken(distance, this.draggedEntity, this.dragRulerRanges) ??
|
|
this.color
|
|
);
|
|
}
|
|
|
|
dragRulerStart(options, measureImmediately = true) {
|
|
const entity = this.draggedEntity;
|
|
const isToken = entity instanceof Token;
|
|
if (isToken && !currentSpeedProvider.usesRuler(entity)) return;
|
|
const ruler = canvas.controls.ruler;
|
|
ruler.clear();
|
|
ruler._state = Ruler.STATES.STARTING;
|
|
const entityCenter = getEntityCenter(this.draggedEntity);
|
|
if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
|
|
ruler.dragRulerAddWaypointHistory(getMovementHistory(entity));
|
|
ruler.dragRulerAddWaypoint(entityCenter, {snap: false});
|
|
const mousePosition = getPointer().getLocalPosition(
|
|
canvas.tokens,
|
|
);
|
|
const destination = {
|
|
x: mousePosition.x + ruler.rulerOffset.x,
|
|
y: mousePosition.y + ruler.rulerOffset.y,
|
|
};
|
|
if (measureImmediately) ruler.measure(destination, options);
|
|
}
|
|
|
|
dragRulerSendState() {
|
|
game.user.broadcastActivity({
|
|
ruler: this.toJSON(),
|
|
});
|
|
}
|
|
}
|
|
|
|
CONFIG.Canvas.rulerClass = DragRulerRuler;
|
|
}
|