659 lines
20 KiB
JavaScript
659 lines
20 KiB
JavaScript
|
|
/**
|
||
|
|
* A type of Placeable Object which highlights an area of the grid as covered by some area of effect.
|
||
|
|
* @category - Canvas
|
||
|
|
* @see {@link MeasuredTemplateDocument}
|
||
|
|
* @see {@link TemplateLayer}
|
||
|
|
*/
|
||
|
|
class MeasuredTemplate extends PlaceableObject {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The geometry shape used for testing point intersection
|
||
|
|
* @type {PIXI.Circle | PIXI.Ellipse | PIXI.Polygon | PIXI.Rectangle | PIXI.RoundedRectangle}
|
||
|
|
*/
|
||
|
|
shape;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The tiling texture used for this template, if any
|
||
|
|
* @type {PIXI.Texture}
|
||
|
|
*/
|
||
|
|
texture;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The template graphics
|
||
|
|
* @type {PIXI.Graphics}
|
||
|
|
*/
|
||
|
|
template;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The measurement ruler label
|
||
|
|
* @type {PreciseText}
|
||
|
|
*/
|
||
|
|
ruler;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Internal property used to configure the control border thickness
|
||
|
|
* @type {number}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_borderThickness = 3;
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
static embeddedName = "MeasuredTemplate";
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
static RENDER_FLAGS = {
|
||
|
|
redraw: {propagate: ["refresh"]},
|
||
|
|
refresh: {propagate: ["refreshState", "refreshPosition", "refreshShape", "refreshElevation"], alias: true},
|
||
|
|
refreshState: {},
|
||
|
|
refreshPosition: {propagate: ["refreshGrid"]},
|
||
|
|
refreshShape: {propagate: ["refreshTemplate", "refreshGrid", "refreshText"]},
|
||
|
|
refreshTemplate: {},
|
||
|
|
refreshGrid: {},
|
||
|
|
refreshText: {},
|
||
|
|
refreshElevation: {}
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Properties */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A convenient reference for whether the current User is the author of the MeasuredTemplate document.
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
get isAuthor() {
|
||
|
|
return this.document.isAuthor;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
get bounds() {
|
||
|
|
const {x, y} = this.document;
|
||
|
|
const d = canvas.dimensions;
|
||
|
|
const r = this.document.distance * (d.size / d.distance);
|
||
|
|
return new PIXI.Rectangle(x-r, y-r, 2*r, 2*r);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Is this MeasuredTemplate currently visible on the Canvas?
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
get isVisible() {
|
||
|
|
return !this.document.hidden || this.isAuthor || game.user.isGM;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A unique identifier which is used to uniquely identify related objects like a template effect or grid highlight.
|
||
|
|
* @type {string}
|
||
|
|
*/
|
||
|
|
get highlightId() {
|
||
|
|
return this.objectId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Initial Drawing */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
async _draw(options) {
|
||
|
|
|
||
|
|
// Load Fill Texture
|
||
|
|
if ( this.document.texture ) {
|
||
|
|
this.texture = await loadTexture(this.document.texture, {fallback: "icons/svg/hazard.svg"});
|
||
|
|
} else {
|
||
|
|
this.texture = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Template Shape
|
||
|
|
this.template = this.addChild(new PIXI.Graphics());
|
||
|
|
|
||
|
|
// Control Icon
|
||
|
|
this.controlIcon = this.addChild(this.#createControlIcon());
|
||
|
|
await this.controlIcon.draw();
|
||
|
|
|
||
|
|
// Ruler Text
|
||
|
|
this.ruler = this.addChild(this.#drawRulerText());
|
||
|
|
|
||
|
|
// Enable highlighting for this template
|
||
|
|
canvas.interface.grid.addHighlightLayer(this.highlightId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Draw the ControlIcon for the MeasuredTemplate
|
||
|
|
* @returns {ControlIcon}
|
||
|
|
*/
|
||
|
|
#createControlIcon() {
|
||
|
|
const size = Math.max(Math.round((canvas.dimensions.size * 0.5) / 20) * 20, 40);
|
||
|
|
let icon = new ControlIcon({texture: CONFIG.controlIcons.template, size: size});
|
||
|
|
icon.x -= (size * 0.5);
|
||
|
|
icon.y -= (size * 0.5);
|
||
|
|
return icon;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Draw the Text label used for the MeasuredTemplate
|
||
|
|
* @returns {PreciseText}
|
||
|
|
*/
|
||
|
|
#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);
|
||
|
|
text.anchor.set(0, 1);
|
||
|
|
return text;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_destroy(options) {
|
||
|
|
canvas.interface.grid.destroyHighlightLayer(this.highlightId);
|
||
|
|
this.texture?.destroy();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Incremental Refresh */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_applyRenderFlags(flags) {
|
||
|
|
if ( flags.refreshState ) this._refreshState();
|
||
|
|
if ( flags.refreshPosition ) this._refreshPosition();
|
||
|
|
if ( flags.refreshShape ) this._refreshShape();
|
||
|
|
if ( flags.refreshTemplate ) this._refreshTemplate();
|
||
|
|
if ( flags.refreshGrid ) this.highlightGrid();
|
||
|
|
if ( flags.refreshText ) this._refreshRulerText();
|
||
|
|
if ( flags.refreshElevation ) this._refreshElevation();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the displayed state of the MeasuredTemplate.
|
||
|
|
* This refresh occurs when the user interaction state changes.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshState() {
|
||
|
|
|
||
|
|
// Template Visibility
|
||
|
|
const wasVisible = this.visible;
|
||
|
|
this.visible = this.isVisible && !this.hasPreview;
|
||
|
|
if ( this.visible !== wasVisible ) MouseInteractionManager.emulateMoveEvent();
|
||
|
|
|
||
|
|
// Sort on top of others on hover
|
||
|
|
this.zIndex = this.hover ? 1 : 0;
|
||
|
|
|
||
|
|
// Control Icon Visibility
|
||
|
|
const isHidden = this.document.hidden;
|
||
|
|
this.controlIcon.refresh({
|
||
|
|
visible: this.visible && this.layer.active && this.document.isOwner,
|
||
|
|
iconColor: isHidden ? 0xFF3300 : 0xFFFFFF,
|
||
|
|
borderColor: isHidden ? 0xFF3300 : 0xFF5500,
|
||
|
|
borderVisible: this.hover || this.layer.highlightObjects
|
||
|
|
});
|
||
|
|
|
||
|
|
// Alpha transparency
|
||
|
|
const alpha = isHidden ? 0.5 : 1;
|
||
|
|
this.template.alpha = alpha;
|
||
|
|
this.ruler.alpha = alpha;
|
||
|
|
const highlightLayer = canvas.interface.grid.getHighlightLayer(this.highlightId);
|
||
|
|
highlightLayer.visible = this.visible;
|
||
|
|
// FIXME the elevation is not considered in sort order of the highlight layers
|
||
|
|
highlightLayer.zIndex = this.document.sort;
|
||
|
|
highlightLayer.alpha = alpha;
|
||
|
|
this.alpha = this._getTargetAlpha();
|
||
|
|
|
||
|
|
// Ruler Visibility
|
||
|
|
this.ruler.visible = this.visible && this.layer.active;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the elevation of the control icon.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshElevation() {
|
||
|
|
this.controlIcon.elevation = this.document.elevation;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_getTargetAlpha() {
|
||
|
|
return this.isPreview ? 0.8 : 1.0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the position of the MeasuredTemplate
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshPosition() {
|
||
|
|
const {x, y} = this.document;
|
||
|
|
if ( (this.position.x !== x) || (this.position.y !== y) ) MouseInteractionManager.emulateMoveEvent();
|
||
|
|
this.position.set(x, y);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the underlying geometric shape of the MeasuredTemplate.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshShape() {
|
||
|
|
let {x, y, direction, distance} = this.document;
|
||
|
|
|
||
|
|
// Grid type
|
||
|
|
if ( game.settings.get("core", "gridTemplates") ) {
|
||
|
|
this.ray = new Ray({x, y}, canvas.grid.getTranslatedPoint({x, y}, direction, distance));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Euclidean type
|
||
|
|
else {
|
||
|
|
this.ray = Ray.fromAngle(x, y, Math.toRadians(direction), distance * canvas.dimensions.distancePixels);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the Template shape
|
||
|
|
this.shape = this._computeShape();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Compute the geometry for the template using its document data.
|
||
|
|
* Subclasses can override this method to take control over how different shapes are rendered.
|
||
|
|
* @returns {PIXI.Circle|PIXI.Rectangle|PIXI.Polygon}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_computeShape() {
|
||
|
|
const {t, distance, direction, angle, width} = this.document;
|
||
|
|
switch ( t ) {
|
||
|
|
case "circle":
|
||
|
|
return this.constructor.getCircleShape(distance);
|
||
|
|
case "cone":
|
||
|
|
return this.constructor.getConeShape(distance, direction, angle);
|
||
|
|
case "rect":
|
||
|
|
return this.constructor.getRectShape(distance, direction);
|
||
|
|
case "ray":
|
||
|
|
return this.constructor.getRayShape(distance, direction, width);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the display of the template outline and shape.
|
||
|
|
* Subclasses may override this method to take control over how the template is visually rendered.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshTemplate() {
|
||
|
|
const t = this.template.clear();
|
||
|
|
|
||
|
|
// Draw the Template outline
|
||
|
|
t.lineStyle(this._borderThickness, this.document.borderColor, 0.75).beginFill(0x000000, 0.0);
|
||
|
|
|
||
|
|
// Fill Color or Texture
|
||
|
|
if ( this.texture ) t.beginTextureFill({texture: this.texture});
|
||
|
|
else t.beginFill(0x000000, 0.0);
|
||
|
|
|
||
|
|
// Draw the shape
|
||
|
|
t.drawShape(this.shape);
|
||
|
|
|
||
|
|
// Draw origin and destination points
|
||
|
|
t.lineStyle(this._borderThickness, 0x000000)
|
||
|
|
.beginFill(0x000000, 0.5)
|
||
|
|
.drawCircle(0, 0, 6)
|
||
|
|
.drawCircle(this.ray.dx, this.ray.dy, 6)
|
||
|
|
.endFill();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get a Circular area of effect given a radius of effect
|
||
|
|
* @param {number} distance The radius of the circle in grid units
|
||
|
|
* @returns {PIXI.Circle|PIXI.Polygon}
|
||
|
|
*/
|
||
|
|
static getCircleShape(distance) {
|
||
|
|
|
||
|
|
// Grid circle
|
||
|
|
if ( game.settings.get("core", "gridTemplates") ) {
|
||
|
|
return new PIXI.Polygon(canvas.grid.getCircle({x: 0, y: 0}, distance));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Euclidean circle
|
||
|
|
return new PIXI.Circle(0, 0, distance * canvas.dimensions.distancePixels);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get a Conical area of effect given a direction, angle, and distance
|
||
|
|
* @param {number} distance The radius of the cone in grid units
|
||
|
|
* @param {number} direction The direction of the cone in degrees
|
||
|
|
* @param {number} angle The angle of the cone in degrees
|
||
|
|
* @returns {PIXI.Polygon|PIXI.Circle}
|
||
|
|
*/
|
||
|
|
static getConeShape(distance, direction, angle) {
|
||
|
|
|
||
|
|
// Grid cone
|
||
|
|
if ( game.settings.get("core", "gridTemplates") ) {
|
||
|
|
return new PIXI.Polygon(canvas.grid.getCone({x: 0, y: 0}, distance, direction, angle));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Euclidean cone
|
||
|
|
if ( (distance <= 0) || (angle <= 0) ) return new PIXI.Polygon();
|
||
|
|
distance *= canvas.dimensions.distancePixels;
|
||
|
|
const coneType = game.settings.get("core", "coneTemplateType");
|
||
|
|
|
||
|
|
// For round cones - approximate the shape with a ray every 3 degrees
|
||
|
|
let angles;
|
||
|
|
if ( coneType === "round" ) {
|
||
|
|
if ( angle >= 360 ) return new PIXI.Circle(0, 0, distance);
|
||
|
|
const da = Math.min(angle, 3);
|
||
|
|
angles = Array.fromRange(Math.floor(angle/da)).map(a => (angle/-2) + (a*da)).concat([angle/2]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// For flat cones, direct point-to-point
|
||
|
|
else {
|
||
|
|
angle = Math.min(angle, 179);
|
||
|
|
angles = [(angle/-2), (angle/2)];
|
||
|
|
distance /= Math.cos(Math.toRadians(angle/2));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the cone shape as a polygon
|
||
|
|
const rays = angles.map(a => Ray.fromAngle(0, 0, Math.toRadians(direction + a), distance));
|
||
|
|
const points = rays.reduce((arr, r) => {
|
||
|
|
return arr.concat([r.B.x, r.B.y]);
|
||
|
|
}, [0, 0]).concat([0, 0]);
|
||
|
|
return new PIXI.Polygon(points);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get a Rectangular area of effect given a width and height
|
||
|
|
* @param {number} distance The length of the diagonal in grid units
|
||
|
|
* @param {number} direction The direction of the diagonal in degrees
|
||
|
|
* @returns {PIXI.Rectangle}
|
||
|
|
*/
|
||
|
|
static getRectShape(distance, direction) {
|
||
|
|
let endpoint;
|
||
|
|
|
||
|
|
// Grid rectangle
|
||
|
|
if ( game.settings.get("core", "gridTemplates") ) {
|
||
|
|
endpoint = canvas.grid.getTranslatedPoint({x: 0, y: 0}, direction, distance);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Euclidean rectangle
|
||
|
|
else endpoint = Ray.fromAngle(0, 0, Math.toRadians(direction), distance * canvas.dimensions.distancePixels).B;
|
||
|
|
|
||
|
|
return new PIXI.Rectangle(0, 0, endpoint.x, endpoint.y).normalize();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get a rotated Rectangular area of effect given a width, height, and direction
|
||
|
|
* @param {number} distance The length of the ray in grid units
|
||
|
|
* @param {number} direction The direction of the ray in degrees
|
||
|
|
* @param {number} width The width of the ray in grid units
|
||
|
|
* @returns {PIXI.Polygon}
|
||
|
|
*/
|
||
|
|
static getRayShape(distance, direction, width) {
|
||
|
|
const d = canvas.dimensions;
|
||
|
|
width *= d.distancePixels;
|
||
|
|
const p00 = Ray.fromAngle(0, 0, Math.toRadians(direction - 90), width / 2).B;
|
||
|
|
const p01 = Ray.fromAngle(0, 0, Math.toRadians(direction + 90), width / 2).B;
|
||
|
|
let p10;
|
||
|
|
let p11;
|
||
|
|
|
||
|
|
// Grid ray
|
||
|
|
if ( game.settings.get("core", "gridTemplates") ) {
|
||
|
|
p10 = canvas.grid.getTranslatedPoint(p00, direction, distance);
|
||
|
|
p11 = canvas.grid.getTranslatedPoint(p01, direction, distance);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Euclidean ray
|
||
|
|
else {
|
||
|
|
distance *= d.distancePixels;
|
||
|
|
direction = Math.toRadians(direction);
|
||
|
|
p10 = Ray.fromAngle(p00.x, p00.y, direction, distance).B;
|
||
|
|
p11 = Ray.fromAngle(p01.x, p01.y, direction, distance).B;
|
||
|
|
}
|
||
|
|
|
||
|
|
return new PIXI.Polygon(p00.x, p00.y, p10.x, p10.y, p11.x, p11.y, p01.x, p01.y);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update the displayed ruler tooltip text
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshRulerText() {
|
||
|
|
const {distance, t} = this.document;
|
||
|
|
const grid = canvas.grid;
|
||
|
|
if ( t === "rect" ) {
|
||
|
|
const {A: {x: x0, y: y0}, B: {x: x1, y: y1}} = this.ray;
|
||
|
|
const dx = grid.measurePath([{x: x0, y: y0}, {x: x1, y: y0}]).distance;
|
||
|
|
const dy = grid.measurePath([{x: x0, y: y0}, {x: x0, y: y1}]).distance;
|
||
|
|
const w = Math.round(dx * 10) / 10;
|
||
|
|
const h = Math.round(dy * 10) / 10;
|
||
|
|
this.ruler.text = `${w}${grid.units} x ${h}${grid.units}`;
|
||
|
|
} else {
|
||
|
|
const r = Math.round(distance * 10) / 10;
|
||
|
|
this.ruler.text = `${r}${grid.units}`;
|
||
|
|
}
|
||
|
|
this.ruler.position.set(this.ray.dx + 10, this.ray.dy + 5);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Highlight the grid squares which should be shown under the area of effect
|
||
|
|
*/
|
||
|
|
highlightGrid() {
|
||
|
|
// Clear the existing highlight layer
|
||
|
|
canvas.interface.grid.clearHighlightLayer(this.highlightId);
|
||
|
|
|
||
|
|
// Highlight colors
|
||
|
|
const border = this.document.borderColor;
|
||
|
|
const color = this.document.fillColor;
|
||
|
|
|
||
|
|
// If we are in grid-less mode, highlight the shape directly
|
||
|
|
if ( canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ) {
|
||
|
|
const shape = this._getGridHighlightShape();
|
||
|
|
canvas.interface.grid.highlightPosition(this.highlightId, {border, color, shape});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Otherwise, highlight specific grid positions
|
||
|
|
else {
|
||
|
|
const positions = this._getGridHighlightPositions();
|
||
|
|
for ( const {x, y} of positions ) {
|
||
|
|
canvas.interface.grid.highlightPosition(this.highlightId, {x, y, border, color});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the shape to highlight on a Scene which uses grid-less mode.
|
||
|
|
* @returns {PIXI.Polygon|PIXI.Circle|PIXI.Rectangle}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_getGridHighlightShape() {
|
||
|
|
const shape = this.shape.clone();
|
||
|
|
if ( "points" in shape ) {
|
||
|
|
shape.points = shape.points.map((p, i) => {
|
||
|
|
if ( i % 2 ) return this.y + p;
|
||
|
|
else return this.x + p;
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
shape.x += this.x;
|
||
|
|
shape.y += this.y;
|
||
|
|
}
|
||
|
|
return shape;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get an array of points which define top-left grid spaces to highlight for square or hexagonal grids.
|
||
|
|
* @returns {Point[]}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_getGridHighlightPositions() {
|
||
|
|
const grid = canvas.grid;
|
||
|
|
const {x: ox, y: oy} = this.document;
|
||
|
|
const shape = this.shape;
|
||
|
|
const bounds = shape.getBounds();
|
||
|
|
bounds.x += ox;
|
||
|
|
bounds.y += oy;
|
||
|
|
bounds.fit(canvas.dimensions.rect);
|
||
|
|
bounds.pad(1);
|
||
|
|
|
||
|
|
// Identify grid space that have their center points covered by the template shape
|
||
|
|
const positions = [];
|
||
|
|
const [i0, j0, i1, j1] = grid.getOffsetRange(bounds);
|
||
|
|
for ( let i = i0; i < i1; i++ ) {
|
||
|
|
for ( let j = j0; j < j1; j++ ) {
|
||
|
|
const offset = {i, j};
|
||
|
|
const {x: cx, y: cy} = grid.getCenterPoint(offset);
|
||
|
|
|
||
|
|
// If the origin of the template is a grid space center, this grid space is highlighted
|
||
|
|
let covered = (Math.max(Math.abs(cx - ox), Math.abs(cy - oy)) < 1);
|
||
|
|
if ( !covered ) {
|
||
|
|
for ( let dx = -0.5; dx <= 0.5; dx += 0.5 ) {
|
||
|
|
for ( let dy = -0.5; dy <= 0.5; dy += 0.5 ) {
|
||
|
|
if ( shape.contains(cx - ox + dx, cy - oy + dy) ) {
|
||
|
|
covered = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ( !covered ) continue;
|
||
|
|
positions.push(grid.getTopLeftPoint(offset));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return positions;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Methods */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
async rotate(angle, snap) {
|
||
|
|
if ( game.paused && !game.user.isGM ) {
|
||
|
|
ui.notifications.warn("GAME.PausedWarning", {localize: true});
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
const direction = this._updateRotation({angle, snap});
|
||
|
|
await this.document.update({direction});
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Document Event Handlers */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritDoc */
|
||
|
|
_onUpdate(changed, options, userId) {
|
||
|
|
super._onUpdate(changed, options, userId);
|
||
|
|
|
||
|
|
// Incremental Refresh
|
||
|
|
this.renderFlags.set({
|
||
|
|
redraw: "texture" in changed,
|
||
|
|
refreshState: ("sort" in changed) || ("hidden" in changed),
|
||
|
|
refreshPosition: ("x" in changed) || ("y" in changed),
|
||
|
|
refreshElevation: "elevation" in changed,
|
||
|
|
refreshShape: ["t", "angle", "direction", "distance", "width"].some(k => k in changed),
|
||
|
|
refreshTemplate: "borderColor" in changed,
|
||
|
|
refreshGrid: ("borderColor" in changed) || ("fillColor" in changed)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Interactivity */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_canControl(user, event) {
|
||
|
|
if ( !this.layer.active || this.isPreview ) return false;
|
||
|
|
return user.isGM || (user === this.document.author);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
_canHUD(user, event) {
|
||
|
|
return this.isOwner; // Allow template owners to right-click
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
_canConfigure(user, event) {
|
||
|
|
return false; // Double-right does nothing
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_canView(user, event) {
|
||
|
|
return this._canControl(user, event);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
_onClickRight(event) {
|
||
|
|
this.document.update({hidden: !this.document.hidden});
|
||
|
|
if ( !this._propagateRightClick(event) ) event.stopPropagation();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Deprecations and Compatibility */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v12
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
get borderColor() {
|
||
|
|
const msg = "MeasuredTemplate#borderColor has been deprecated. Use MeasuredTemplate#document#borderColor instead.";
|
||
|
|
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||
|
|
return this.document.borderColor.valueOf();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v12
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
get fillColor() {
|
||
|
|
const msg = "MeasuredTemplate#fillColor has been deprecated. Use MeasuredTemplate#document#fillColor instead.";
|
||
|
|
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||
|
|
return this.document.fillColor.valueOf();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v12
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
get owner() {
|
||
|
|
const msg = "MeasuredTemplate#owner has been deprecated. Use MeasuredTemplate#isOwner instead.";
|
||
|
|
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
|
||
|
|
return this.isOwner;
|
||
|
|
}
|
||
|
|
}
|