diff --git a/src/api.js b/src/api.js index 18f94cb..5e2c0c4 100644 --- a/src/api.js +++ b/src/api.js @@ -103,12 +103,12 @@ export function getUnreachableColorFromSpeedProvider() { } } -export function getCostFromSpeedProvider(token, area) { +export function getCostFromSpeedProvider(token, area, options) { try { if (currentSpeedProvider instanceof Function) { - return SpeedProvider.prototype.getCostForStep.call(undefined, token, area); + return SpeedProvider.prototype.getCostForStep.call(undefined, token, area, options); } - return currentSpeedProvider.getCostForStep(token, area); + return currentSpeedProvider.getCostForStep(token, area, options); } catch (e) { console.error(e); diff --git a/src/compatibility.js b/src/compatibility.js index 666d1d7..edc5849 100644 --- a/src/compatibility.js +++ b/src/compatibility.js @@ -16,24 +16,26 @@ export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape= } export function measureDistances(segments, entity, shape, options={}) { - const isToken = entity instanceof Token; const opts = duplicate(options) - if (!opts.gridSpaces) - opts.gridSpaces = true; - const terrainRulerAvailable = isToken && game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS); - if (terrainRulerAvailable) { + if (opts.enableTerrainRuler) { + opts.gridSpaces = true; const firstNewSegmentIndex = segments.findIndex(segment => !segment.ray.dragRulerVisitedSpaces); const previousSegments = segments.slice(0, firstNewSegmentIndex); 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) => getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), {x, y}); + opts.costFunction = (x, y, costOptions={}) => { return getCostFromSpeedProvider(entity, getAreaFromPositionAndShape({x, y}, shape), costOptions); } if (previousSegments.length > 0) opts.terrainRulerInitialState = previousSegments[previousSegments.length - 1].ray.dragRulerFinalState; return distances.concat(terrainRuler.measureDistances(newSegments, opts)); } else { + // If another module wants to enable grid measurements but disable grid highlighting, + // manually set the *duplicate* option's gridSpaces value to true for the Foundry logic to work properly + if(!opts.ignoreGrid) { + opts.gridSpaces = true; + } return canvas.grid.measureDistances(segments, opts); } } diff --git a/src/foundry_imports.js b/src/foundry_imports.js index d91e860..4eac54b 100644 --- a/src/foundry_imports.js +++ b/src/foundry_imports.js @@ -148,22 +148,30 @@ function scheduleMeasurement(destination, event) { } // This is a modified version of Ruler.measure form foundry 0.7.9 -export function measure(destination, {gridSpaces=true, snap=false} = {}) { +export function measure(destination, options={}) { const isToken = this.draggedEntity instanceof Token; if (isToken && !this.draggedEntity.isVisible) return [] - if (snap) { + if (options.snap) { destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity); } - const terrainRulerAvailable = isToken && game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS); + if(options.gridSpaces === undefined) { + options.gridSpaces = canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS; + } + + if(options.ignoreGrid === undefined) { + options.ignoreGrid = false; + } + + options.enableTerrainRuler = isToken && game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS); const waypoints = this.waypoints.concat([destination]); // Move the waypoints to the center of the grid if a size is used that measures from edge to edge const centeredWaypoints = isToken ? applyTokenSizeOffset(waypoints, this.draggedEntity) : duplicate(waypoints); // Foundries native ruler requires the waypoints to sit in the dead center of the square to work properly - if (!terrainRulerAvailable) + if (!options.enableTerrainRuler && !options.ignoreGrid) centeredWaypoints.forEach(w => [w.x, w.y] = canvas.grid.getCenter(w.x, w.y)); const r = this.ruler; @@ -197,7 +205,7 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) { const shape = isToken ? getTokenShape(this.draggedEntity) : null; // Compute measured distance - const distances = measureDistances(centeredSegments, this.draggedEntity, shape, {gridSpaces}); + const distances = measureDistances(centeredSegments, this.draggedEntity, shape, options); let totalDistance = 0; for (let [i, d] of distances.entries()) { @@ -216,7 +224,7 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) { // Draw measured path r.clear(); let rulerColor - if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) + if (!options.gridSpaces || canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) rulerColor = getColorForDistance.call(this, totalDistance) else rulerColor = this.color @@ -252,8 +260,8 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) { } // Highlight grid positions - if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) { - if (terrainRulerAvailable) + if (isToken && canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS && options.gridSpaces) { + if (options.enableTerrainRuler) highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier) else highlightMeasurementNative.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier); diff --git a/src/main.js b/src/main.js index 68c1c12..9cc53c0 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,7 @@ import {getMovementHistory, resetMovementHistory} from "./movement_tracking.js"; import {registerSettings, settingsKey} from "./settings.js" import {recalculate} from "./socket.js"; import {SpeedProvider} from "./speed_provider.js" +import {setSnapParameterOnOptions} from "./util.js"; Hooks.once("init", () => { registerSettings() @@ -141,7 +142,7 @@ function onEntityLeftDragStart(event) { ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y}; if (isToken && game.settings.get(settingsKey, "enableMovementHistory")) ruler.dragRulerAddWaypointHistory(getMovementHistory(this)); - ruler.dragRulerAddWaypoint(entityCenter, false); + ruler.dragRulerAddWaypoint(entityCenter, {snap: false}); } function onEntityLeftDragMove(event) { @@ -171,13 +172,15 @@ function onEntityDragLeftCancel(event) { if (!ruler.isDragRuler || ruler._state === Ruler.STATES.MOVING) return false if (ruler._state === Ruler.STATES.MEASURING) { + let options = {}; + setSnapParameterOnOptions(this, event, options); + if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) { - ruler.dragRulerDeleteWaypoint(event); + ruler.dragRulerDeleteWaypoint(event, options); } else { - event.preventDefault() - const snap = !event.shiftKey - ruler.dragRulerAddWaypoint(ruler.destination, snap); + event.preventDefault(); + ruler.dragRulerAddWaypoint(ruler.destination, options); } } return true diff --git a/src/ruler.js b/src/ruler.js index cc3c7f4..381956c 100644 --- a/src/ruler.js +++ b/src/ruler.js @@ -1,7 +1,7 @@ import {measure} from "./foundry_imports.js" import {getMovementHistory} from "./movement_tracking.js"; import {settingsKey} from "./settings.js"; -import {getSnapPointForEntity} from "./util.js"; +import {getSnapPointForEntity, setSnapParameterOnOptions} from "./util.js"; export class DragRulerRuler extends Ruler { // Functions below are overridden versions of functions in Ruler @@ -22,12 +22,15 @@ export class DragRulerRuler extends Ruler { // This function is invoked by left clicking if (!this.isDragRuler) return await super.moveToken(event); + + let options = {}; + setSnapParameterOnOptions(this, event, options); + if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) { - const snap = !event.shiftKey; - this.dragRulerAddWaypoint(this.destination, snap); + this.dragRulerAddWaypoint(this.destination, options); } else { - this.dragRulerDeleteWaypoint(); + this.dragRulerDeleteWaypoint(event, options); } } @@ -52,6 +55,7 @@ export class DragRulerRuler extends Ruler { else this.draggedEntity = canvas.templates.get(data.draggedEntity); } + super.update(data); } @@ -70,8 +74,9 @@ export class DragRulerRuler extends Ruler { } // The functions below aren't present in the orignal Ruler class and are added by Drag Ruler - dragRulerAddWaypoint(point, snap=true) { - if (snap) { + 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)); @@ -91,12 +96,16 @@ export class DragRulerRuler extends Ruler { this.labels.removeChildren().forEach(c => c.destroy()); } - dragRulerDeleteWaypoint(event={preventDefault: () => {return}}) { + dragRulerDeleteWaypoint(event={preventDefault: () => {return}}, options={}) { + options.snap = options.snap ?? true; if (this.waypoints.filter(w => !w.isPrevious).length > 1) { event.preventDefault(); const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens); const rulerOffset = this.rulerOffset; - this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y}); + + // 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 { @@ -123,7 +132,7 @@ export class DragRulerRuler extends Ruler { if (game.settings.get(settingsKey, "enableMovementHistory")) this.dragRulerAddWaypointHistory(getMovementHistory(this.draggedEntity)); for (const waypoint of waypoints) { - this.dragRulerAddWaypoint(waypoint, false); + this.dragRulerAddWaypoint(waypoint, {snap: false}); } this.measure(this.destination); game.user.broadcastActivity({ruler: this}); diff --git a/src/speed_provider.js b/src/speed_provider.js index b9ca0df..20a8d53 100644 --- a/src/speed_provider.js +++ b/src/speed_provider.js @@ -61,13 +61,18 @@ export class SpeedProvider { * The area indicates the whole area that the token will occupy (for tokens larger than 1x1) the array will more than one entry. * The return value should be an integer indicating a multiplicator by that the cost of that step should be increased. * (1 is regular cost, 2 costs double, 3 costs triple, ...) + * + * Parameters: + * - options: An object used to configure TerrainLayer's cost calculation. Ex: If options.ignoreGrid is set to true, then Euclidean measurement can be forced on a gridded map. + * * This function is only called if the TerrainLayer and TerrainRuler modules are enabled. * * Implementing this method is optional and only needs to be done if you want to provide a custom cost function (for example to allow tokens to ignore difficult terrain) */ - getCostForStep(token, area) { + getCostForStep(token, area, options={}) { // Lookup the cost for each square occupied by the token - const costs = area.map(space => terrainRuler.getCost(space.x, space.y, {token})); + options.token = token; + const costs = area.map(space => terrainRuler.getCost(space.x, space.y, options)); // Return the maximum of the costs return costs.reduce((max, current) => Math.max(max, current)) } diff --git a/src/util.js b/src/util.js index 0d78478..3856f2a 100644 --- a/src/util.js +++ b/src/util.js @@ -198,3 +198,15 @@ export function applyTokenSizeOffset(waypoints, token) { return waypoints.map(w => new PIXI.Point(w.x + waypointOffset.x, w.y + waypointOffset.y)) } + +export function setSnapParameterOnOptions(sourceObject, event, options) { + // Allow outside modules to override snapping + if (sourceObject.snapOverride?.active) { + options.snapOverrideActive = true; + options.snap = sourceObject.snapOverride.snap; + sourceObject.snapOverride = undefined; // remove it to prevent any lingering data issues + } + else { + options.snap = !event.shiftKey; + } +}