/*
|
|
* This file is part of the warpgate module (https://github.com/trioderegion/warpgate)
|
|
* Copyright (c) 2021 Matthew Haentschke.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, version 3.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import { logger } from './logger.js'
|
|
import { MODULE } from './module.js'
|
|
|
|
/** @typedef {import('@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/data/data.mjs/measuredTemplateData.js').MeasuredTemplateDataProperties} MeasuredTemplateProperties */
|
|
|
|
/**
|
|
* Contains all fields from `MeasuredTemplate#toObject`, plus the following.
|
|
*
|
|
* @typedef {Object} CrosshairsData
|
|
* @borrows MeasuredTemplateProperties
|
|
* @prop {boolean} cancelled Workflow cancelled via right click (true)
|
|
* @prop {Scene} scene Scene on this crosshairs was last active
|
|
* @prop {number} radius Final radius of template, in pixels
|
|
* @prop {number} size Final diameter of template, in grid units
|
|
*/
|
|
|
|
/**
|
|
* @class
|
|
*/
|
|
export class Crosshairs extends MeasuredTemplate {
|
|
|
|
//constructor(gridSize = 1, data = {}){
|
|
constructor(config, callbacks = {}) {
|
|
const templateData = {
|
|
t: "circle",
|
|
user: game.user.id,
|
|
distance: config.size,
|
|
x: config.x,
|
|
y: config.y,
|
|
fillColor: config.fillColor,
|
|
width: 1,
|
|
texture: config.texture,
|
|
direction: config.direction,
|
|
}
|
|
|
|
const template = new CONFIG.MeasuredTemplate.documentClass(templateData, {parent: canvas.scene});
|
|
super(template);
|
|
|
|
/** @TODO all of these fields should be part of the source data schema for this class **/
|
|
/** image path to display in the center (under mouse cursor) */
|
|
this.icon = config.icon ?? Crosshairs.ERROR_TEXTURE;
|
|
|
|
/** text to display below crosshairs' circle */
|
|
this.label = config.label;
|
|
|
|
/** Offsets the default position of the label (in pixels) */
|
|
this.labelOffset = config.labelOffset;
|
|
|
|
/**
|
|
* Arbitrary field used to identify this instance
|
|
* of a Crosshairs in the canvas.templates.preview
|
|
* list
|
|
*/
|
|
this.tag = config.tag;
|
|
|
|
/** Should the center icon be shown? */
|
|
this.drawIcon = config.drawIcon;
|
|
|
|
/** Should the outer circle be shown? */
|
|
this.drawOutline = config.drawOutline;
|
|
|
|
/** Opacity of the fill color */
|
|
this.fillAlpha = config.fillAlpha;
|
|
|
|
/** Should the texture (if any) be tiled
|
|
* or scaled and offset? */
|
|
this.tileTexture = config.tileTexture;
|
|
|
|
/** locks the size of crosshairs (shift+scroll) */
|
|
this.lockSize = config.lockSize;
|
|
|
|
/** locks the position of crosshairs */
|
|
this.lockPosition = config.lockPosition;
|
|
|
|
/** Number of quantization steps along
|
|
* a square's edge (N+1 snap points
|
|
* along each edge, conting endpoints)
|
|
*/
|
|
this.interval = config.interval;
|
|
|
|
/** Callback functions to execute
|
|
* at particular times
|
|
*/
|
|
this.callbacks = callbacks;
|
|
|
|
/** Indicates if the user is actively
|
|
* placing the crosshairs.
|
|
* Setting this to true in the show
|
|
* callback will stop execution
|
|
* and report the current mouse position
|
|
* as the chosen location
|
|
*/
|
|
this.inFlight = false;
|
|
|
|
/** indicates if the placement of
|
|
* crosshairs was canceled (with
|
|
* a right click)
|
|
*/
|
|
this.cancelled = true;
|
|
|
|
/**
|
|
* Indicators on where cancel was initiated
|
|
* for determining if it was a drag or a cancel
|
|
*/
|
|
this.rightX = 0;
|
|
this.rightY = 0;
|
|
|
|
/** @type {number} */
|
|
this.radius = this.document.distance * this.scene.grid.size / 2;
|
|
}
|
|
|
|
/**
|
|
* @returns {CrosshairsData} Current Crosshairs class data
|
|
*/
|
|
toObject() {
|
|
|
|
/** @type {CrosshairsData} */
|
|
const data = foundry.utils.mergeObject(this.document.toObject(), {
|
|
cancelled: this.cancelled,
|
|
scene: this.scene,
|
|
radius: this.radius,
|
|
size: this.document.distance,
|
|
});
|
|
delete data.width;
|
|
return data;
|
|
}
|
|
|
|
static ERROR_TEXTURE = 'icons/svg/hazard.svg'
|
|
|
|
/**
|
|
* Will retrieve the active crosshairs instance with the defined tag identifier.
|
|
* @param {string} key Crosshairs identifier. Will be compared against the Crosshairs `tag` field for strict equality.
|
|
* @returns {PIXI.DisplayObject|undefined}
|
|
*/
|
|
static getTag(key) {
|
|
return canvas.templates.preview.children.find( child => child.tag === key )
|
|
}
|
|
|
|
static getSnappedPosition({x,y}, interval){
|
|
const offset = interval < 0 ? canvas.grid.size/2 : 0;
|
|
const snapped = canvas.grid.getSnappedPosition(x - offset, y - offset, interval);
|
|
return {x: snapped.x + offset, y: snapped.y + offset};
|
|
}
|
|
|
|
/* -----------EXAMPLE CODE FROM MEASUREDTEMPLATE.JS--------- */
|
|
/* Portions of the core package (MeasuredTemplate) repackaged
|
|
* in accordance with the "Limited License Agreement for Module
|
|
* Development, found here: https://foundryvtt.com/article/license/
|
|
* Changes noted where possible
|
|
*/
|
|
|
|
/**
|
|
* Set the displayed ruler tooltip text and position
|
|
* @private
|
|
*/
|
|
//BEGIN WARPGATE
|
|
_setRulerText() {
|
|
this.ruler.text = this.label;
|
|
/** swap the X and Y to use the default dx/dy of a ray (pointed right)
|
|
//to align the text to the bottom of the template */
|
|
this.ruler.position.set(-this.ruler.width / 2 + this.labelOffset.x, this.template.height / 2 + 5 + this.labelOffset.y);
|
|
//END WARPGATE
|
|
}
|
|
|
|
/** @override */
|
|
async draw() {
|
|
this.clear();
|
|
|
|
// Load the texture
|
|
const texture = this.document.texture;
|
|
if ( texture ) {
|
|
this._texture = await loadTexture(texture, {fallback: 'icons/svg/hazard.svg'});
|
|
} else {
|
|
this._texture = null;
|
|
}
|
|
|
|
// Template shape
|
|
this.template = this.addChild(new PIXI.Graphics());
|
|
|
|
// Rotation handle
|
|
//BEGIN WARPGATE
|
|
//this.handle = this.addChild(new PIXI.Graphics());
|
|
//END WARPGATE
|
|
|
|
// Draw the control icon
|
|
//if(this.drawIcon)
|
|
this.controlIcon = this.addChild(this._drawControlIcon());
|
|
|
|
// Draw the ruler measurement
|
|
this.ruler = this.addChild(this._drawRulerText());
|
|
|
|
// Update the shape and highlight grid squares
|
|
this.refresh();
|
|
//BEGIN WARPGATE
|
|
this._setRulerText();
|
|
//this.highlightGrid();
|
|
//END WARPGATE
|
|
|
|
// Enable interactivity, only if the Tile has a true ID
|
|
if ( this.id ) this.activateListeners();
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Draw the Text label used for the MeasuredTemplate
|
|
* @return {PreciseText}
|
|
* @protected
|
|
*/
|
|
_drawRulerText() {
|
|
const style = CONFIG.canvasTextStyle.clone();
|
|
style.fontSize = Math.max(Math.round(canvas.dimensions.size * 0.36 * 12) / 12, 36);
|
|
const text = new PreciseText(null, style);
|
|
//BEGIN WARPGATE
|
|
//text.anchor.set(0.5, 0);
|
|
text.anchor.set(0, 0);
|
|
//END WARPGATE
|
|
return text;
|
|
}
|
|
|
|
/**
|
|
* Draw the ControlIcon for the MeasuredTemplate
|
|
* @return {ControlIcon}
|
|
* @protected
|
|
*/
|
|
_drawControlIcon() {
|
|
const size = Math.max(Math.round((canvas.dimensions.size * 0.5) / 20) * 20, 40);
|
|
|
|
//BEGIN WARPGATE
|
|
let icon = new ControlIcon({texture: this.icon, size: size});
|
|
icon.visible = this.drawIcon;
|
|
//END WARPGATE
|
|
|
|
icon.pivot.set(size*0.5, size*0.5);
|
|
//icon.x -= (size * 0.5);
|
|
//icon.y -= (size * 0.5);
|
|
icon.angle = this.document.direction;
|
|
return icon;
|
|
}
|
|
|
|
/** @override */
|
|
refresh() {
|
|
if (!this.template) return;
|
|
let d = canvas.dimensions;
|
|
const document = this.document;
|
|
this.position.set(document.x, document.y);
|
|
|
|
// Extract and prepare data
|
|
let {direction, distance} = document;
|
|
distance *= (d.size/2);
|
|
//BEGIN WARPGATE
|
|
//width *= (d.size / d.distance);
|
|
//END WARPGATE
|
|
direction = Math.toRadians(direction);
|
|
|
|
// Create ray and bounding rectangle
|
|
this.ray = Ray.fromAngle(document.x, document.y, direction, distance);
|
|
|
|
// Get the Template shape
|
|
switch (document.t) {
|
|
case "circle":
|
|
this.shape = this._getCircleShape(distance);
|
|
break;
|
|
default: logger.error("Non-circular Crosshairs is unsupported!");
|
|
}
|
|
|
|
// Draw the Template outline
|
|
this.template.clear()
|
|
.lineStyle(this._borderThickness, this.borderColor, this.drawOutline ? 0.75 : 0)
|
|
|
|
// Fill Color or Texture
|
|
|
|
if (this._texture) {
|
|
/* assume 0,0 is top left of texture
|
|
* and scale/offset this texture (due to origin
|
|
* at center of template). tileTexture indicates
|
|
* that this texture is tilable and does not
|
|
* need to be scaled/offset */
|
|
const scale = this.tileTexture ? 1 : distance * 2 / this._texture.width;
|
|
const offset = this.tileTexture ? 0 : distance;
|
|
this.template.beginTextureFill({
|
|
texture: this._texture,
|
|
matrix: new PIXI.Matrix().scale(scale, scale).translate(-offset, -offset)
|
|
});
|
|
} else {
|
|
this.template.beginFill(this.fillColor, this.fillAlpha);
|
|
}
|
|
|
|
// Draw the shape
|
|
this.template.drawShape(this.shape);
|
|
|
|
// Draw origin and destination points
|
|
//BEGIN WARPGATE
|
|
//this.template.lineStyle(this._borderThickness, 0x000000, this.drawOutline ? 0.75 : 0)
|
|
// .beginFill(0x000000, 0.5)
|
|
//.drawCircle(0, 0, 6)
|
|
//.drawCircle(this.ray.dx, this.ray.dy, 6);
|
|
//END WARPGATE
|
|
|
|
// Update visibility
|
|
if (this.drawIcon) {
|
|
this.controlIcon.visible = true;
|
|
this.controlIcon.border.visible = this._hover
|
|
this.controlIcon.angle = document.direction;
|
|
}
|
|
|
|
// Draw ruler text
|
|
//BEGIN WARPGATE
|
|
this._setRulerText()
|
|
//END WARPGATE
|
|
return this;
|
|
}
|
|
|
|
/* END MEASUREDTEMPLATE.JS USAGE */
|
|
|
|
|
|
/* -----------EXAMPLE CODE FROM ABILITY-TEMPLATE.JS--------- */
|
|
/* Foundry VTT 5th Edition
|
|
* Copyright (C) 2019 Foundry Network
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* Original License:
|
|
* https://gitlab.com/foundrynet/dnd5e/-/blob/master/LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* Creates a preview of the spell template
|
|
*/
|
|
async drawPreview() {
|
|
// Draw the template and switch to the template layer
|
|
this.initialLayer = canvas.activeLayer;
|
|
this.layer.activate();
|
|
this.draw();
|
|
this.layer.preview.addChild(this);
|
|
this.layer.interactiveChildren = false;
|
|
|
|
// Hide the sheet that originated the preview
|
|
//BEGIN WARPGATE
|
|
this.inFlight = true;
|
|
|
|
// Activate interactivity
|
|
this.activatePreviewListeners();
|
|
|
|
// Callbacks
|
|
this.callbacks?.show?.(this);
|
|
|
|
/* wait _indefinitely_ for placement to be decided. */
|
|
await MODULE.waitFor(() => !this.inFlight, -1)
|
|
if (this.activeHandlers) {
|
|
this.clearHandlers();
|
|
}
|
|
|
|
//END WARPGATE
|
|
return this;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
_mouseMoveHandler(event) {
|
|
event.stopPropagation();
|
|
|
|
/* if our position is locked, do not update it */
|
|
if (this.lockPosition) return;
|
|
|
|
// Apply a 20ms throttle
|
|
let now = Date.now();
|
|
if (now - this.moveTime <= 20) return;
|
|
|
|
const center = event.data.getLocalPosition(this.layer);
|
|
const {x,y} = Crosshairs.getSnappedPosition(center, this.interval);
|
|
this.document.updateSource({x, y});
|
|
this.refresh();
|
|
this.moveTime = now;
|
|
canvas._onDragCanvasPan(event.data.originalEvent);
|
|
}
|
|
|
|
_leftClickHandler(event) {
|
|
const document = this.document;
|
|
const thisSceneSize = this.scene.grid.size;
|
|
|
|
const destination = Crosshairs.getSnappedPosition(this.document, this.interval);
|
|
this.radius = document.distance * thisSceneSize / 2;
|
|
this.cancelled = false;
|
|
|
|
this.document.updateSource({ ...destination });
|
|
|
|
this.clearHandlers(event);
|
|
}
|
|
|
|
// Rotate the template by 3 degree increments (mouse-wheel)
|
|
// none = rotate 5 degrees
|
|
// shift = scale size
|
|
// ctrl = rotate 30 or 15 degrees (square/hex)
|
|
// alt = zoom canvas
|
|
_mouseWheelHandler(event) {
|
|
|
|
if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
|
|
if (!event.altKey) event.stopPropagation();
|
|
|
|
const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
|
|
const snap = event.ctrlKey ? delta : 5;
|
|
//BEGIN WARPGATE
|
|
const document = this.document;
|
|
const thisSceneSize = this.scene.grid.size;
|
|
if (event.shiftKey && !this.lockSize) {
|
|
let distance = document.distance + 0.25 * (Math.sign(event.deltaY));
|
|
distance = Math.max(distance, 0.25);
|
|
this.document.updateSource({ distance });
|
|
this.radius = document.distance * thisSceneSize / 2;
|
|
} else if (!event.altKey) {
|
|
const direction = document.direction + (snap * Math.sign(event.deltaY));
|
|
this.document.updateSource({ direction });
|
|
}
|
|
//END WARPGATE
|
|
this.refresh();
|
|
}
|
|
|
|
_rightDownHandler(event) {
|
|
if (event.button !== 2) return;
|
|
|
|
this.rightX = event.screenX;
|
|
this.rightY = event.screenY;
|
|
}
|
|
|
|
_rightUpHandler(event) {
|
|
if (event.button !== 2) return;
|
|
|
|
const isWithinThreshold = (current, previous) => Math.abs(current - previous) < 10;
|
|
if (isWithinThreshold(this.rightX, event.screenX)
|
|
&& isWithinThreshold(this.rightY, event.screenY)
|
|
) {
|
|
this.cancelled = true;
|
|
this.clearHandlers(event);
|
|
}
|
|
}
|
|
|
|
_clearHandlers(event) {
|
|
//WARPGATE BEGIN
|
|
/* remove only ourselves, in case of multiple */
|
|
this.layer.preview.removeChild(this);
|
|
|
|
canvas.stage.off("mousemove", this.activeMoveHandler);
|
|
canvas.stage.off("mousedown", this.activeLeftClickHandler);
|
|
canvas.app.view.onmousedown = null;
|
|
canvas.app.view.onmouseup = null;
|
|
canvas.app.view.onwheel = null;
|
|
//WARPGATE END
|
|
|
|
/* re-enable interactivity on this layer */
|
|
this.layer.interactiveChildren = true;
|
|
|
|
/* moving off this layer also deletes ALL active previews?
|
|
* unexpected, but manageable
|
|
*/
|
|
if (this.layer.preview.children.length == 0) {
|
|
this.initialLayer.activate();
|
|
}
|
|
|
|
//BEGIN WARPGATE
|
|
// Show the sheet that originated the preview
|
|
if (this.actorSheet) this.actorSheet.maximize();
|
|
this.activeHandlers = false;
|
|
this.inFlight = false;
|
|
|
|
/* mark this pixi element as destroyed */
|
|
this._destroyed = true;
|
|
//END WARPGATE
|
|
}
|
|
|
|
/**
|
|
* Activate listeners for the template preview
|
|
*/
|
|
activatePreviewListeners() {
|
|
this.moveTime = 0;
|
|
//BEGIN WARPGATE
|
|
this.activeHandlers = true;
|
|
|
|
/* Activate listeners */
|
|
this.activeMoveHandler = this._mouseMoveHandler.bind(this);
|
|
this.activeLeftClickHandler = this._leftClickHandler.bind(this);
|
|
this.rightDownHandler = this._rightDownHandler.bind(this);
|
|
this.rightUpHandler = this._rightUpHandler.bind(this);
|
|
this.activeWheelHandler = this._mouseWheelHandler.bind(this);
|
|
|
|
this.clearHandlers = this._clearHandlers.bind(this);
|
|
|
|
// Update placement (mouse-move)
|
|
canvas.stage.on("mousemove", this.activeMoveHandler);
|
|
|
|
// Confirm the workflow (left-click)
|
|
canvas.stage.on("mousedown", this.activeLeftClickHandler);
|
|
|
|
// Mouse Wheel rotate
|
|
canvas.app.view.onwheel = this.activeWheelHandler;
|
|
|
|
// Right click cancel
|
|
canvas.app.view.onmousedown = this.rightDownHandler;
|
|
canvas.app.view.onmouseup = this.rightUpHandler;
|
|
|
|
// END WARPGATE
|
|
}
|
|
|
|
/** END ABILITY-TEMPLATE.JS USAGE */
|
|
}
|