"use strict"; import { getColorForDistanceAndToken, getMovedDistanceFromToken, getRangesFromSpeedProvider, initApi, registerModule, registerSystem, } from "./api.js"; import {checkDependencies} from "./compatibility.js"; import {moveEntities, onMouseMove} from "./foundry_imports.js"; 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 {extendRuler} from "./ruler.js"; import {registerSettings, RightClickAction, settingsKey} from "./settings.js"; import {recalculate} from "./socket.js"; import {SpeedProvider} from "./speed_provider.js"; import {getEntityCenter, setSnapParameterOnOptions} from "./util.js"; CONFIG.debug.dragRuler = false; export let debugGraphics = undefined; Hooks.once("init", () => { registerSettings(); registerKeybindings(); initApi(); hookDragHandlers(Token); hookDragHandlers(MeasuredTemplate); libWrapper.register( "drag-ruler", "TokenLayer.prototype.undoHistory", tokenLayerUndoHistory, "WRAPPER", ); extendRuler(); window.dragRuler = { getRangesFromSpeedProvider, getColorForDistanceAndToken, getMovedDistanceFromToken, registerModule, registerSystem, recalculate, resetMovementHistory, }; }); Hooks.once("ready", () => { performMigrations(); checkDependencies(); Hooks.callAll("dragRuler.ready", SpeedProvider); if (CONFIG.debug.dragRuler) debugGraphics = canvas.controls.addChild(new PIXI.Container()); }); Hooks.on("canvasReady", () => { canvas.controls.rulers.children.forEach(ruler => { ruler.draggedEntity = null; Object.defineProperty(ruler, "isDragRuler", { get: function isDragRuler() { return Boolean(this.draggedEntity) && this._state !== Ruler.STATES.INACTIVE; }, }); }); }); Hooks.on("getCombatTrackerEntryContext", function (html, menu) { const entry = { name: "drag-ruler.resetMovementHistory", icon: '', callback: li => resetMovementHistory(ui.combat.viewed, li.data("combatant-id")), }; menu.splice(1, 0, entry); }); function forwardIfUnahndled(newFn) { return function (oldFn, ...args) { const eventHandled = newFn(...args); if (!eventHandled) oldFn(...args); }; } function hookDragHandlers(entityType) { const entityName = entityType.name; libWrapper.register( "drag-ruler", `${entityName}.prototype._onDragLeftStart`, onEntityLeftDragStart, "WRAPPER", ); if (entityType === Token) libWrapper.register( "drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMoveSnap, "WRAPPER", ); else libWrapper.register( "drag-ruler", `${entityName}.prototype._onDragLeftMove`, onEntityLeftDragMove, "WRAPPER", ); libWrapper.register( "drag-ruler", `${entityName}.prototype._onDragLeftDrop`, forwardIfUnahndled(onEntityDragLeftDrop), "MIXED", ); libWrapper.register( "drag-ruler", `${entityName}.prototype._onDragLeftCancel`, forwardIfUnahndled(onEntityDragLeftCancel), "MIXED", ); } async function tokenLayerUndoHistory(wrapped) { const historyEntry = this.history[this.history.length - 1]; const returnValue = await wrapped(); if (historyEntry.type === "update") { for (const entry of historyEntry.data) { const token = canvas.tokens.get(entry._id); removeLastHistoryEntryIfAt(token, entry.x, entry.y); } } return returnValue; } function onEntityLeftDragStart(wrapped, event) { wrapped(event); const isToken = this instanceof Token; const ruler = canvas.controls.ruler; ruler.draggedEntity = this; const entityCenter = getEntityCenter(this); ruler.rulerOffset = { x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y, }; if (game.settings.get(settingsKey, "autoStartMeasurement")) { let options = {}; setSnapParameterOnOptions(ruler, options); ruler.dragRulerStart(options, false); } } function onEntityLeftDragMoveSnap(wrapped, event) { applyGridlessSnapping.call(this, event); onEntityLeftDragMove.call(this, wrapped, event); } function onEntityLeftDragMove(wrapped, event) { wrapped(event); const ruler = canvas.controls.ruler; if (ruler.isDragRuler) onMouseMove.call(ruler, event); } function onEntityDragLeftDrop(event) { const ruler = canvas.controls.ruler; if (!ruler.isDragRuler) { ruler.draggedEntity = undefined; return false; } // When we're dragging a measured template no token will ever be selected, // resulting in only the dragged template to be moved as would be expected const selectedTokens = canvas.tokens.controlled; // 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; } function onEntityDragLeftCancel(event) { // This function is invoked by right clicking const ruler = canvas.controls.ruler; if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING) return false; const rightClickAction = game.settings.get(settingsKey, "rightClickAction"); let options = {}; setSnapParameterOnOptions(ruler, options); if (ruler._state === Ruler.STATES.INACTIVE) { if (rightClickAction !== RightClickAction.CREATE_WAYPOINT) return false; ruler.dragRulerStart(options); event.preventDefault(); } else if (ruler._state === Ruler.STATES.MEASURING) { switch (rightClickAction) { case RightClickAction.CREATE_WAYPOINT: event.preventDefault(); ruler.dragRulerAddWaypoint(ruler.destination, options); break; case RightClickAction.DELETE_WAYPOINT: ruler.dragRulerDeleteWaypoint(event, options); break; case RightClickAction.ABORT_DRAG: ruler.dragRulerAbortDrag(); break; } } return true; } function applyGridlessSnapping(event) { const ruler = canvas.controls.ruler; if (!game.settings.get(settingsKey, "useGridlessRaster")) return; if (!ruler.isDragRuler) return; if (disableSnap) return; if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) return; const rasterWidth = 35 / canvas.stage.scale.x; const tokenX = event.data.destination.x; const tokenY = event.data.destination.y; const destination = {x: tokenX + ruler.rulerOffset.x, y: tokenY + ruler.rulerOffset.y}; const ranges = getRangesFromSpeedProvider(ruler.draggedEntity); const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active; if (terrainRulerAvailable) { const segments = ruler.constructor .dragRulerGetRaysFromWaypoints(ruler.waypoints, destination) .map(ray => { return {ray}; }); const pinpointDistances = new Map(); for (const range of ranges) { pinpointDistances.set(range.range, null); } terrainRuler.measureDistances(segments, {pinpointDistances}); const targetDistance = Array.from(pinpointDistances.entries()) .filter(([_key, val]) => val) .reduce((value, current) => (value[0] > current[0] ? value : current), [0, null]); const rasterLocation = targetDistance[1]; if (rasterLocation) { const deltaX = destination.x - rasterLocation.x; const deltaY = destination.y - rasterLocation.y; const rasterDistance = Math.hypot(deltaX, deltaY); if (rasterDistance < rasterWidth) { event.data.destination.x = rasterLocation.x - ruler.rulerOffset.x; event.data.destination.y = rasterLocation.y - ruler.rulerOffset.y; } } } else { let waypointDistance = 0; let origin = event.data.origin; if (ruler.waypoints.length > 1) { const segments = ruler.constructor .dragRulerGetRaysFromWaypoints(ruler.waypoints, destination) .map(ray => { return {ray}; }); origin = segments.pop().ray.A; waypointDistance = canvas.grid.measureDistances(segments).reduce((a, b) => a + b); origin = {x: origin.x - ruler.rulerOffset.x, y: origin.y - ruler.rulerOffset.y}; } const deltaX = tokenX - origin.x; const deltaY = tokenY - origin.y; const distance = Math.hypot(deltaX, deltaY); // targetRange will be the largest range that's still smaller than distance let targetDistance = ranges .map(range => range.range) .map(range => range - waypointDistance) .map(range => (range * canvas.dimensions.size) / canvas.dimensions.distance) .filter(range => range < distance) .reduce((a, b) => Math.max(a, b), 0); if (targetDistance) { if (distance < targetDistance + rasterWidth) { event.data.destination.x = origin.x + (deltaX * targetDistance) / distance; event.data.destination.y = origin.y + (deltaY * targetDistance) / distance; } } } }