1018 lines
38 KiB
JavaScript
1018 lines
38 KiB
JavaScript
|
|
/**
|
||
|
|
* @typedef {Object} CanvasHistory
|
||
|
|
* @property {string} type The type of operation stored as history (create, update, delete)
|
||
|
|
* @property {Object[]} data The data corresponding to the action which may later be un-done
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @typedef {Object} PlaceablesLayerOptions
|
||
|
|
* @property {boolean} controllableObjects Can placeable objects in this layer be controlled?
|
||
|
|
* @property {boolean} rotatableObjects Can placeable objects in this layer be rotated?
|
||
|
|
* @property {boolean} confirmDeleteKey Confirm placeable object deletion with a dialog?
|
||
|
|
* @property {PlaceableObject} objectClass The class used to represent an object on this layer.
|
||
|
|
* @property {boolean} quadtree Does this layer use a quadtree to track object positions?
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A subclass of Canvas Layer which is specifically designed to contain multiple PlaceableObject instances,
|
||
|
|
* each corresponding to an embedded Document.
|
||
|
|
* @category - Canvas
|
||
|
|
*/
|
||
|
|
class PlaceablesLayer extends InteractionLayer {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sort order for placeables belonging to this layer.
|
||
|
|
* @type {number}
|
||
|
|
*/
|
||
|
|
static SORT_ORDER = 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Placeable Layer Objects
|
||
|
|
* @type {PIXI.Container|null}
|
||
|
|
*/
|
||
|
|
objects = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Preview Object Placement
|
||
|
|
*/
|
||
|
|
preview = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Keep track of history so that CTRL+Z can undo changes
|
||
|
|
* @type {CanvasHistory[]}
|
||
|
|
*/
|
||
|
|
history = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Keep track of an object copied with CTRL+C which can be pasted later
|
||
|
|
* @type {PlaceableObject[]}
|
||
|
|
*/
|
||
|
|
_copy = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A Quadtree which partitions and organizes Walls into quadrants for efficient target identification.
|
||
|
|
* @type {Quadtree|null}
|
||
|
|
*/
|
||
|
|
quadtree = this.options.quadtree ? new CanvasQuadtree() : null;
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Attributes */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Configuration options for the PlaceablesLayer.
|
||
|
|
* @type {PlaceablesLayerOptions}
|
||
|
|
*/
|
||
|
|
static get layerOptions() {
|
||
|
|
return foundry.utils.mergeObject(super.layerOptions, {
|
||
|
|
baseClass: PlaceablesLayer,
|
||
|
|
controllableObjects: false,
|
||
|
|
rotatableObjects: false,
|
||
|
|
confirmDeleteKey: false,
|
||
|
|
objectClass: CONFIG[this.documentName]?.objectClass,
|
||
|
|
quadtree: true
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A reference to the named Document type which is contained within this Canvas Layer.
|
||
|
|
* @type {string}
|
||
|
|
*/
|
||
|
|
static documentName;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Creation states affected to placeables during their construction.
|
||
|
|
* @enum {number}
|
||
|
|
*/
|
||
|
|
static CREATION_STATES = {
|
||
|
|
NONE: 0,
|
||
|
|
POTENTIAL: 1,
|
||
|
|
CONFIRMED: 2,
|
||
|
|
COMPLETED: 3
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Obtain a reference to the Collection of embedded Document instances within the currently viewed Scene
|
||
|
|
* @type {Collection|null}
|
||
|
|
*/
|
||
|
|
get documentCollection() {
|
||
|
|
return canvas.scene?.getEmbeddedCollection(this.constructor.documentName) || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Obtain a reference to the PlaceableObject class definition which represents the Document type in this layer.
|
||
|
|
* @type {Function}
|
||
|
|
*/
|
||
|
|
static get placeableClass() {
|
||
|
|
return CONFIG[this.documentName].objectClass;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* If objects on this PlaceablesLayer have a HUD UI, provide a reference to its instance
|
||
|
|
* @type {BasePlaceableHUD|null}
|
||
|
|
*/
|
||
|
|
get hud() {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A convenience method for accessing the placeable object instances contained in this layer
|
||
|
|
* @type {PlaceableObject[]}
|
||
|
|
*/
|
||
|
|
get placeables() {
|
||
|
|
if ( !this.objects ) return [];
|
||
|
|
return this.objects.children;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* An Array of placeable objects in this layer which have the _controlled attribute
|
||
|
|
* @returns {PlaceableObject[]}
|
||
|
|
*/
|
||
|
|
get controlled() {
|
||
|
|
return Array.from(this.#controlledObjects.values());
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Iterates over placeable objects that are eligible for control/select.
|
||
|
|
* @yields A placeable object
|
||
|
|
* @returns {Generator<PlaceableObject>}
|
||
|
|
*/
|
||
|
|
*controllableObjects() {
|
||
|
|
if ( !this.options.controllableObjects ) return;
|
||
|
|
for ( const placeable of this.placeables ) {
|
||
|
|
if ( placeable.visible ) yield placeable;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Track the set of PlaceableObjects on this layer which are currently controlled.
|
||
|
|
* @type {Map<string,PlaceableObject>}
|
||
|
|
*/
|
||
|
|
get controlledObjects() {
|
||
|
|
return this.#controlledObjects;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @private */
|
||
|
|
#controlledObjects = new Map();
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Track the PlaceableObject on this layer which is currently hovered upon.
|
||
|
|
* @type {PlaceableObject|null}
|
||
|
|
*/
|
||
|
|
get hover() {
|
||
|
|
return this.#hover;
|
||
|
|
}
|
||
|
|
|
||
|
|
set hover(object) {
|
||
|
|
if ( object instanceof this.constructor.placeableClass ) this.#hover = object;
|
||
|
|
else this.#hover = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
#hover = null;
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Track whether "highlight all objects" is currently active
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
highlightObjects = false;
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the maximum sort value of all placeables.
|
||
|
|
* @returns {number} The maximum sort value (-Infinity if there are no objects)
|
||
|
|
*/
|
||
|
|
getMaxSort() {
|
||
|
|
let sort = -Infinity;
|
||
|
|
const collection = this.documentCollection;
|
||
|
|
if ( !collection?.documentClass.schema.has("sort") ) return sort;
|
||
|
|
for ( const document of collection ) sort = Math.max(sort, document.sort);
|
||
|
|
return sort;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Send the controlled objects of this layer to the back or bring them to the front.
|
||
|
|
* @param {boolean} front Bring to front instead of send to back?
|
||
|
|
* @returns {boolean} Returns true if the layer has sortable object, and false otherwise
|
||
|
|
* @internal
|
||
|
|
*/
|
||
|
|
_sendToBackOrBringToFront(front) {
|
||
|
|
const collection = this.documentCollection;
|
||
|
|
const documentClass = collection?.documentClass;
|
||
|
|
if ( !documentClass?.schema.has("sort") ) return false;
|
||
|
|
if ( !this.controlled.length ) return true;
|
||
|
|
|
||
|
|
// Determine to-be-updated objects and the minimum/maximum sort value of the other objects
|
||
|
|
const toUpdate = [];
|
||
|
|
let target = front ? -Infinity : Infinity;
|
||
|
|
for ( const document of collection ) {
|
||
|
|
if ( document.object?.controlled && !document.locked ) toUpdate.push(document);
|
||
|
|
else target = (front ? Math.max : Math.min)(target, document.sort);
|
||
|
|
}
|
||
|
|
if ( !Number.isFinite(target) ) return true;
|
||
|
|
target += (front ? 1 : -toUpdate.length);
|
||
|
|
|
||
|
|
// Sort the to-be-updated objects by sort in ascending order
|
||
|
|
toUpdate.sort((a, b) => a.sort - b.sort);
|
||
|
|
|
||
|
|
// Update the to-be-updated objects
|
||
|
|
const updates = toUpdate.map((document, i) => ({_id: document.id, sort: target + i}));
|
||
|
|
canvas.scene.updateEmbeddedDocuments(documentClass.documentName, updates);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Snaps the given point to grid. The layer defines the snapping behavior.
|
||
|
|
* @param {Point} point The point that is to be snapped
|
||
|
|
* @returns {Point} The snapped point
|
||
|
|
*/
|
||
|
|
getSnappedPoint(point) {
|
||
|
|
const M = CONST.GRID_SNAPPING_MODES;
|
||
|
|
const grid = canvas.grid;
|
||
|
|
return grid.getSnappedPoint(point, {
|
||
|
|
mode: grid.isHexagonal && !this.options.controllableObjects
|
||
|
|
? M.CENTER | M.VERTEX
|
||
|
|
: M.CENTER | M.VERTEX | M.CORNER | M.SIDE_MIDPOINT,
|
||
|
|
resolution: 1
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Rendering
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Obtain an iterable of objects which should be added to this PlaceablesLayer
|
||
|
|
* @returns {Document[]}
|
||
|
|
*/
|
||
|
|
getDocuments() {
|
||
|
|
return this.documentCollection || [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritDoc */
|
||
|
|
async _draw(options) {
|
||
|
|
await super._draw(options);
|
||
|
|
|
||
|
|
// Create objects container which can be sorted
|
||
|
|
this.objects = this.addChild(new PIXI.Container());
|
||
|
|
this.objects.sortableChildren = true;
|
||
|
|
this.objects.visible = false;
|
||
|
|
const cls = getDocumentClass(this.constructor.documentName);
|
||
|
|
if ( (cls.schema.get("elevation") instanceof foundry.data.fields.NumberField)
|
||
|
|
&& (cls.schema.get("sort") instanceof foundry.data.fields.NumberField) ) {
|
||
|
|
this.objects.sortChildren = PlaceablesLayer.#sortObjectsByElevationAndSort;
|
||
|
|
}
|
||
|
|
this.objects.on("childAdded", obj => {
|
||
|
|
if ( !(obj instanceof this.constructor.placeableClass) ) {
|
||
|
|
console.error(`An object of type ${obj.constructor.name} was added to ${this.constructor.name}#objects. `
|
||
|
|
+ `The object must be an instance of ${this.constructor.placeableClass.name}.`);
|
||
|
|
}
|
||
|
|
if ( obj instanceof PlaceableObject ) obj._updateQuadtree();
|
||
|
|
});
|
||
|
|
this.objects.on("childRemoved", obj => {
|
||
|
|
if ( obj instanceof PlaceableObject ) obj._updateQuadtree();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Create preview container which is always above objects
|
||
|
|
this.preview = this.addChild(new PIXI.Container());
|
||
|
|
|
||
|
|
// Create and draw objects
|
||
|
|
const documents = this.getDocuments();
|
||
|
|
const promises = documents.map(doc => {
|
||
|
|
const obj = doc._object = this.createObject(doc);
|
||
|
|
this.objects.addChild(obj);
|
||
|
|
return obj.draw();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Wait for all objects to draw
|
||
|
|
await Promise.all(promises);
|
||
|
|
this.objects.visible = this.active;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Draw a single placeable object
|
||
|
|
* @param {ClientDocument} document The Document instance used to create the placeable object
|
||
|
|
* @returns {PlaceableObject}
|
||
|
|
*/
|
||
|
|
createObject(document) {
|
||
|
|
return new this.constructor.placeableClass(document);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritDoc */
|
||
|
|
async _tearDown(options) {
|
||
|
|
this.history = [];
|
||
|
|
if ( this.options.controllableObjects ) {
|
||
|
|
this.controlledObjects.clear();
|
||
|
|
}
|
||
|
|
if ( this.hud ) this.hud.clear();
|
||
|
|
if ( this.quadtree ) this.quadtree.clear();
|
||
|
|
this.objects = null;
|
||
|
|
return super._tearDown(options);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The method to sort the objects elevation and sort before sorting by the z-index.
|
||
|
|
* @type {Function}
|
||
|
|
*/
|
||
|
|
static #sortObjectsByElevationAndSort = function() {
|
||
|
|
for ( let i = 0; i < this.children.length; i++ ) {
|
||
|
|
this.children[i]._lastSortedIndex = i;
|
||
|
|
}
|
||
|
|
this.children.sort((a, b) => (a.document.elevation - b.document.elevation)
|
||
|
|
|| (a.document.sort - b.document.sort)
|
||
|
|
|| (a.zIndex - b.zIndex)
|
||
|
|
|| (a._lastSortedIndex - b._lastSortedIndex)
|
||
|
|
);
|
||
|
|
this.sortDirty = false;
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Methods */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_activate() {
|
||
|
|
this.objects.visible = true;
|
||
|
|
this.placeables.forEach(l => l.renderFlags.set({refreshState: true}));
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_deactivate() {
|
||
|
|
this.objects.visible = false;
|
||
|
|
this.releaseAll();
|
||
|
|
this.placeables.forEach(l => l.renderFlags.set({refreshState: true}));
|
||
|
|
this.clearPreviewContainer();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clear the contents of the preview container, restoring visibility of original (non-preview) objects.
|
||
|
|
*/
|
||
|
|
clearPreviewContainer() {
|
||
|
|
if ( !this.preview ) return;
|
||
|
|
this.preview.removeChildren().forEach(c => {
|
||
|
|
c._onDragEnd();
|
||
|
|
c.destroy({children: true});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get a PlaceableObject contained in this layer by its ID.
|
||
|
|
* Returns undefined if the object doesn't exist or if the canvas is not rendering a Scene.
|
||
|
|
* @param {string} objectId The ID of the contained object to retrieve
|
||
|
|
* @returns {PlaceableObject} The object instance, or undefined
|
||
|
|
*/
|
||
|
|
get(objectId) {
|
||
|
|
return this.documentCollection?.get(objectId)?.object || undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Acquire control over all PlaceableObject instances which are visible and controllable within the layer.
|
||
|
|
* @param {object} options Options passed to the control method of each object
|
||
|
|
* @returns {PlaceableObject[]} An array of objects that were controlled
|
||
|
|
*/
|
||
|
|
controlAll(options={}) {
|
||
|
|
if ( !this.options.controllableObjects ) return [];
|
||
|
|
options.releaseOthers = false;
|
||
|
|
for ( const placeable of this.controllableObjects() ) {
|
||
|
|
placeable.control(options);
|
||
|
|
}
|
||
|
|
return this.controlled;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Release all controlled PlaceableObject instance from this layer.
|
||
|
|
* @param {object} options Options passed to the release method of each object
|
||
|
|
* @returns {number} The number of PlaceableObject instances which were released
|
||
|
|
*/
|
||
|
|
releaseAll(options={}) {
|
||
|
|
let released = 0;
|
||
|
|
for ( let o of this.placeables ) {
|
||
|
|
if ( !o.controlled ) continue;
|
||
|
|
o.release(options);
|
||
|
|
released++;
|
||
|
|
}
|
||
|
|
return released;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Simultaneously rotate multiple PlaceableObjects using a provided angle or incremental.
|
||
|
|
* This executes a single database operation using Scene#updateEmbeddedDocuments.
|
||
|
|
* @param {object} options Options which configure how multiple objects are rotated
|
||
|
|
* @param {number} [options.angle] A target angle of rotation (in degrees) where zero faces "south"
|
||
|
|
* @param {number} [options.delta] An incremental angle of rotation (in degrees)
|
||
|
|
* @param {number} [options.snap] Snap the resulting angle to a multiple of some increment (in degrees)
|
||
|
|
* @param {Array} [options.ids] An Array of object IDs to target for rotation
|
||
|
|
* @param {boolean} [options.includeLocked=false] Rotate objects whose documents are locked?
|
||
|
|
* @returns {Promise<PlaceableObject[]>} An array of objects which were rotated
|
||
|
|
* @throws An error if an explicitly provided id is not valid
|
||
|
|
*/
|
||
|
|
async rotateMany({angle, delta, snap, ids, includeLocked=false}={}) {
|
||
|
|
if ( (angle ?? delta ?? null) === null ) {
|
||
|
|
throw new Error("Either a target angle or relative delta must be provided.");
|
||
|
|
}
|
||
|
|
|
||
|
|
// Rotation is not permitted
|
||
|
|
if ( !this.options.rotatableObjects ) return [];
|
||
|
|
if ( game.paused && !game.user.isGM ) {
|
||
|
|
ui.notifications.warn("GAME.PausedWarning", {localize: true});
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Identify the objects requested for rotation
|
||
|
|
const objects = this._getMovableObjects(ids, includeLocked);
|
||
|
|
if ( !objects.length ) return objects;
|
||
|
|
|
||
|
|
// Conceal any active HUD
|
||
|
|
this.hud?.clear();
|
||
|
|
|
||
|
|
// Commit updates to the Scene
|
||
|
|
const updateData = objects.map(o => ({
|
||
|
|
_id: o.id,
|
||
|
|
rotation: o._updateRotation({angle, delta, snap})
|
||
|
|
}));
|
||
|
|
await canvas.scene.updateEmbeddedDocuments(this.constructor.documentName, updateData);
|
||
|
|
return objects;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Simultaneously move multiple PlaceableObjects via keyboard movement offsets.
|
||
|
|
* This executes a single database operation using Scene#updateEmbeddedDocuments.
|
||
|
|
* @param {object} options Options which configure how multiple objects are moved
|
||
|
|
* @param {-1|0|1} [options.dx=0] Horizontal movement direction
|
||
|
|
* @param {-1|0|1} [options.dy=0] Vertical movement direction
|
||
|
|
* @param {boolean} [options.rotate=false] Rotate the placeable to direction instead of moving
|
||
|
|
* @param {string[]} [options.ids] An Array of object IDs to target for movement.
|
||
|
|
* The default is the IDs of controlled objects.
|
||
|
|
* @param {boolean} [options.includeLocked=false] Move objects whose documents are locked?
|
||
|
|
* @returns {Promise<PlaceableObject[]>} An array of objects which were moved during the operation
|
||
|
|
* @throws An error if an explicitly provided id is not valid
|
||
|
|
*/
|
||
|
|
async moveMany({dx=0, dy=0, rotate=false, ids, includeLocked=false}={}) {
|
||
|
|
if ( ![-1, 0, 1].includes(dx) ) throw new Error("Invalid argument: dx must be -1, 0, or 1");
|
||
|
|
if ( ![-1, 0, 1].includes(dy) ) throw new Error("Invalid argument: dy must be -1, 0, or 1");
|
||
|
|
if ( !dx && !dy ) return [];
|
||
|
|
if ( game.paused && !game.user.isGM ) {
|
||
|
|
ui.notifications.warn("GAME.PausedWarning", {localize: true});
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Identify the objects requested for movement
|
||
|
|
const objects = this._getMovableObjects(ids, includeLocked);
|
||
|
|
if ( !objects.length ) return objects;
|
||
|
|
|
||
|
|
// Define rotation angles
|
||
|
|
const rotationAngles = {
|
||
|
|
square: [45, 135, 225, 315],
|
||
|
|
hexR: [30, 150, 210, 330],
|
||
|
|
hexQ: [60, 120, 240, 300]
|
||
|
|
};
|
||
|
|
|
||
|
|
// Determine the rotation angle
|
||
|
|
let offsets = [dx, dy];
|
||
|
|
let angle = 0;
|
||
|
|
if ( rotate ) {
|
||
|
|
let angles = rotationAngles.square;
|
||
|
|
const gridType = canvas.grid.type;
|
||
|
|
if ( gridType >= CONST.GRID_TYPES.HEXODDQ ) angles = rotationAngles.hexQ;
|
||
|
|
else if ( gridType >= CONST.GRID_TYPES.HEXODDR ) angles = rotationAngles.hexR;
|
||
|
|
if (offsets.equals([0, 1])) angle = 0;
|
||
|
|
else if (offsets.equals([-1, 1])) angle = angles[0];
|
||
|
|
else if (offsets.equals([-1, 0])) angle = 90;
|
||
|
|
else if (offsets.equals([-1, -1])) angle = angles[1];
|
||
|
|
else if (offsets.equals([0, -1])) angle = 180;
|
||
|
|
else if (offsets.equals([1, -1])) angle = angles[2];
|
||
|
|
else if (offsets.equals([1, 0])) angle = 270;
|
||
|
|
else if (offsets.equals([1, 1])) angle = angles[3];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Conceal any active HUD
|
||
|
|
this.hud?.clear();
|
||
|
|
|
||
|
|
// Commit updates to the Scene
|
||
|
|
const updateData = objects.map(obj => {
|
||
|
|
let update = {_id: obj.id};
|
||
|
|
if ( rotate ) update.rotation = angle;
|
||
|
|
else foundry.utils.mergeObject(update, obj._getShiftedPosition(...offsets));
|
||
|
|
return update;
|
||
|
|
});
|
||
|
|
await canvas.scene.updateEmbeddedDocuments(this.constructor.documentName, updateData);
|
||
|
|
return objects;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* An internal helper method to identify the array of PlaceableObjects which can be moved or rotated.
|
||
|
|
* @param {string[]} ids An explicit array of IDs requested.
|
||
|
|
* @param {boolean} includeLocked Include locked objects which would otherwise be ignored?
|
||
|
|
* @returns {PlaceableObject[]} An array of objects which can be moved or rotated
|
||
|
|
* @throws An error if any explicitly requested ID is not valid
|
||
|
|
* @internal
|
||
|
|
*/
|
||
|
|
_getMovableObjects(ids, includeLocked) {
|
||
|
|
if ( ids instanceof Array ) return ids.reduce((arr, id) => {
|
||
|
|
const object = this.get(id);
|
||
|
|
if ( !object ) throw new Error(`"${id} is not a valid ${this.constructor.documentName} in the current Scene`);
|
||
|
|
if ( includeLocked || !object.document.locked ) arr.push(object);
|
||
|
|
return arr;
|
||
|
|
}, []);
|
||
|
|
return this.controlled.filter(object => includeLocked || !object.document.locked);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Undo a change to the objects in this layer
|
||
|
|
* This method is typically activated using CTRL+Z while the layer is active
|
||
|
|
* @returns {Promise<Document[]>} An array of documents which were modified by the undo operation
|
||
|
|
*/
|
||
|
|
async undoHistory() {
|
||
|
|
if ( game.paused && !game.user.isGM ) {
|
||
|
|
ui.notifications.warn("GAME.PausedWarning", {localize: true});
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
const type = this.constructor.documentName;
|
||
|
|
if ( !this.history.length ) {
|
||
|
|
ui.notifications.info(game.i18n.format("CONTROLS.EmptyUndoHistory", {
|
||
|
|
type: game.i18n.localize(getDocumentClass(type).metadata.label)}));
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
let event = this.history.pop();
|
||
|
|
|
||
|
|
// Undo creation with deletion
|
||
|
|
if ( event.type === "create" ) {
|
||
|
|
const ids = event.data.map(d => d._id);
|
||
|
|
const deleted = await canvas.scene.deleteEmbeddedDocuments(type, ids, {isUndo: true});
|
||
|
|
if ( deleted.length !== 1 ) ui.notifications.info(game.i18n.format("CONTROLS.UndoCreateObjects",
|
||
|
|
{count: deleted.length, type: game.i18n.localize(getDocumentClass(type).metadata.label)}));
|
||
|
|
return deleted;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Undo updates with update
|
||
|
|
else if ( event.type === "update" ) {
|
||
|
|
return canvas.scene.updateEmbeddedDocuments(type, event.data, {isUndo: true});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Undo deletion with creation
|
||
|
|
else if ( event.type === "delete" ) {
|
||
|
|
const created = await canvas.scene.createEmbeddedDocuments(type, event.data, {isUndo: true, keepId: true});
|
||
|
|
if ( created.length !== 1 ) ui.notifications.info(game.i18n.format("CONTROLS.UndoDeleteObjects",
|
||
|
|
{count: created.length, type: game.i18n.localize(getDocumentClass(type).metadata.label)}));
|
||
|
|
return created;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A helper method to prompt for deletion of all PlaceableObject instances within the Scene
|
||
|
|
* Renders a confirmation dialogue to confirm with the requester that all objects will be deleted
|
||
|
|
* @returns {Promise<Document[]>} An array of Document objects which were deleted by the operation
|
||
|
|
*/
|
||
|
|
async deleteAll() {
|
||
|
|
const type = this.constructor.documentName;
|
||
|
|
if ( !game.user.isGM ) {
|
||
|
|
throw new Error(`You do not have permission to delete ${type} objects from the Scene.`);
|
||
|
|
}
|
||
|
|
const typeLabel = game.i18n.localize(getDocumentClass(type).metadata.label);
|
||
|
|
return Dialog.confirm({
|
||
|
|
title: game.i18n.localize("CONTROLS.ClearAll"),
|
||
|
|
content: `<p>${game.i18n.format("CONTROLS.ClearAllHint", {type: typeLabel})}</p>`,
|
||
|
|
yes: async () => {
|
||
|
|
const deleted = await canvas.scene.deleteEmbeddedDocuments(type, [], {deleteAll: true});
|
||
|
|
ui.notifications.info(game.i18n.format("CONTROLS.DeletedObjects",
|
||
|
|
{count: deleted.length, type: typeLabel}));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Record a new CRUD event in the history log so that it can be undone later
|
||
|
|
* @param {string} type The event type (create, update, delete)
|
||
|
|
* @param {Object[]} data The object data
|
||
|
|
*/
|
||
|
|
storeHistory(type, data) {
|
||
|
|
if ( data.every(d => !("_id" in d)) ) throw new Error("The data entries must contain the _id key");
|
||
|
|
if ( type === "update" ) data = data.filter(d => Object.keys(d).length > 1); // Filter entries without changes
|
||
|
|
if ( data.length === 0 ) return; // Don't store empty history data
|
||
|
|
if ( this.history.length >= 10 ) this.history.shift();
|
||
|
|
this.history.push({type, data});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Copy currently controlled PlaceableObjects to a temporary Array, ready to paste back into the scene later
|
||
|
|
* @returns {PlaceableObject[]} The Array of copied PlaceableObject instances
|
||
|
|
*/
|
||
|
|
copyObjects() {
|
||
|
|
if ( this.options.controllableObjects ) this._copy = [...this.controlled];
|
||
|
|
else if ( this.hover ) this._copy = [this.hover];
|
||
|
|
else this._copy = [];
|
||
|
|
const typeLabel = game.i18n.localize(getDocumentClass(this.constructor.documentName).metadata.label);
|
||
|
|
ui.notifications.info(game.i18n.format("CONTROLS.CopiedObjects", {
|
||
|
|
count: this._copy.length,
|
||
|
|
type: typeLabel
|
||
|
|
}));
|
||
|
|
return this._copy;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Paste currently copied PlaceableObjects back to the layer by creating new copies
|
||
|
|
* @param {Point} position The destination position for the copied data.
|
||
|
|
* @param {object} [options] Options which modify the paste operation
|
||
|
|
* @param {boolean} [options.hidden=false] Paste data in a hidden state, if applicable. Default is false.
|
||
|
|
* @param {boolean} [options.snap=true] Snap the resulting objects to the grid. Default is true.
|
||
|
|
* @returns {Promise<Document[]>} An Array of created Document instances
|
||
|
|
*/
|
||
|
|
async pasteObjects(position, {hidden=false, snap=true}={}) {
|
||
|
|
if ( !this._copy.length ) return [];
|
||
|
|
|
||
|
|
// Get the center of all copies
|
||
|
|
const center = {x: 0, y: 0};
|
||
|
|
for ( const copy of this._copy ) {
|
||
|
|
const c = copy.center;
|
||
|
|
center.x += c.x;
|
||
|
|
center.y += c.y;
|
||
|
|
}
|
||
|
|
center.x /= this._copy.length;
|
||
|
|
center.y /= this._copy.length;
|
||
|
|
|
||
|
|
// Offset of the destination position relative to the center
|
||
|
|
const offset = {x: position.x - center.x, y: position.y - center.y};
|
||
|
|
|
||
|
|
// Iterate over objects
|
||
|
|
const toCreate = [];
|
||
|
|
for ( const copy of this._copy ) {
|
||
|
|
toCreate.push(this._pasteObject(copy, offset, {hidden, snap}));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A hook event that fires when any PlaceableObject is pasted onto the
|
||
|
|
* Scene. Substitute the PlaceableObject name in the hook event to target a
|
||
|
|
* specific PlaceableObject type, for example "pasteToken".
|
||
|
|
* @function pastePlaceableObject
|
||
|
|
* @memberof hookEvents
|
||
|
|
* @param {PlaceableObject[]} copied The PlaceableObjects that were copied
|
||
|
|
* @param {object[]} createData The new objects that will be added to the Scene
|
||
|
|
*/
|
||
|
|
Hooks.call(`paste${this.constructor.documentName}`, this._copy, toCreate);
|
||
|
|
|
||
|
|
// Create all objects
|
||
|
|
const created = await canvas.scene.createEmbeddedDocuments(this.constructor.documentName, toCreate);
|
||
|
|
ui.notifications.info(game.i18n.format("CONTROLS.PastedObjects", {count: created.length,
|
||
|
|
type: game.i18n.localize(getDocumentClass(this.constructor.documentName).metadata.label)}));
|
||
|
|
return created;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the data of the copied object pasted at the position given by the offset.
|
||
|
|
* Called by {@link PlaceablesLayer#pasteObjects} for each copied object.
|
||
|
|
* @param {PlaceableObject} copy The copied object that is pasted
|
||
|
|
* @param {Point} offset The offset relative from the current position to the destination
|
||
|
|
* @param {object} [options] Options of {@link PlaceablesLayer#pasteObjects}
|
||
|
|
* @param {boolean} [options.hidden=false] Paste in a hidden state, if applicable. Default is false.
|
||
|
|
* @param {boolean} [options.snap=true] Snap to the grid. Default is true.
|
||
|
|
* @returns {object} The update data
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_pasteObject(copy, offset, {hidden=false, snap=true}={}) {
|
||
|
|
const {x, y} = copy.document;
|
||
|
|
let position = {x: x + offset.x, y: y + offset.y};
|
||
|
|
if ( snap ) position = this.getSnappedPoint(position);
|
||
|
|
const d = canvas.dimensions;
|
||
|
|
position.x = Math.clamp(position.x, 0, d.width - 1);
|
||
|
|
position.y = Math.clamp(position.y, 0, d.height - 1);
|
||
|
|
const data = copy.document.toObject();
|
||
|
|
delete data._id;
|
||
|
|
data.x = position.x;
|
||
|
|
data.y = position.y;
|
||
|
|
data.hidden ||= hidden;
|
||
|
|
return data;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Select all PlaceableObject instances which fall within a coordinate rectangle.
|
||
|
|
* @param {object} [options={}]
|
||
|
|
* @param {number} [options.x] The top-left x-coordinate of the selection rectangle.
|
||
|
|
* @param {number} [options.y] The top-left y-coordinate of the selection rectangle.
|
||
|
|
* @param {number} [options.width] The width of the selection rectangle.
|
||
|
|
* @param {number} [options.height] The height of the selection rectangle.
|
||
|
|
* @param {object} [options.releaseOptions={}] Optional arguments provided to any called release() method.
|
||
|
|
* @param {object} [options.controlOptions={}] Optional arguments provided to any called control() method.
|
||
|
|
* @param {object} [aoptions] Additional options to configure selection behaviour.
|
||
|
|
* @param {boolean} [aoptions.releaseOthers=true] Whether to release other selected objects.
|
||
|
|
* @returns {boolean} A boolean for whether the controlled set was changed in the operation.
|
||
|
|
*/
|
||
|
|
selectObjects({x, y, width, height, releaseOptions={}, controlOptions={}}={}, {releaseOthers=true}={}) {
|
||
|
|
if ( !this.options.controllableObjects ) return false;
|
||
|
|
const oldSet = new Set(this.controlled);
|
||
|
|
|
||
|
|
// Identify selected objects
|
||
|
|
const newSet = new Set();
|
||
|
|
const rectangle = new PIXI.Rectangle(x, y, width, height);
|
||
|
|
for ( const placeable of this.controllableObjects() ) {
|
||
|
|
if ( placeable._overlapsSelection(rectangle) ) newSet.add(placeable);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Release objects that are no longer controlled
|
||
|
|
const toRelease = oldSet.difference(newSet);
|
||
|
|
if ( releaseOthers ) toRelease.forEach(placeable => placeable.release(releaseOptions));
|
||
|
|
|
||
|
|
// Control objects that were not controlled before
|
||
|
|
if ( foundry.utils.isEmpty(controlOptions) ) controlOptions.releaseOthers = false;
|
||
|
|
const toControl = newSet.difference(oldSet);
|
||
|
|
toControl.forEach(placeable => placeable.control(controlOptions));
|
||
|
|
|
||
|
|
// Return a boolean for whether the control set was changed
|
||
|
|
return (releaseOthers && (toRelease.size > 0)) || (toControl.size > 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update all objects in this layer with a provided transformation.
|
||
|
|
* Conditionally filter to only apply to objects which match a certain condition.
|
||
|
|
* @param {Function|object} transformation An object of data or function to apply to all matched objects
|
||
|
|
* @param {Function|null} condition A function which tests whether to target each object
|
||
|
|
* @param {object} [options] Additional options passed to Document.update
|
||
|
|
* @returns {Promise<Document[]>} An array of updated data once the operation is complete
|
||
|
|
*/
|
||
|
|
async updateAll(transformation, condition=null, options={}) {
|
||
|
|
const hasTransformer = transformation instanceof Function;
|
||
|
|
if ( !hasTransformer && (foundry.utils.getType(transformation) !== "Object") ) {
|
||
|
|
throw new Error("You must provide a data object or transformation function");
|
||
|
|
}
|
||
|
|
const hasCondition = condition instanceof Function;
|
||
|
|
const updates = this.placeables.reduce((arr, obj) => {
|
||
|
|
if ( hasCondition && !condition(obj) ) return arr;
|
||
|
|
const update = hasTransformer ? transformation(obj) : foundry.utils.deepClone(transformation);
|
||
|
|
update._id = obj.id;
|
||
|
|
arr.push(update);
|
||
|
|
return arr;
|
||
|
|
},[]);
|
||
|
|
return canvas.scene.updateEmbeddedDocuments(this.constructor.documentName, updates, options);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the world-transformed drop position.
|
||
|
|
* @param {DragEvent} event
|
||
|
|
* @param {object} [options]
|
||
|
|
* @param {boolean} [options.center=true] Return the coordinates of the center of the nearest grid element.
|
||
|
|
* @returns {number[]|boolean} Returns the transformed x, y coordinates, or false if the drag event was outside
|
||
|
|
* the canvas.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_canvasCoordinatesFromDrop(event, {center=true}={}) {
|
||
|
|
let coords = canvas.canvasCoordinatesFromClient({x: event.clientX, y: event.clientY});
|
||
|
|
if ( center ) coords = canvas.grid.getCenterPoint(coords);
|
||
|
|
if ( canvas.dimensions.rect.contains(coords.x, coords.y) ) return [coords.x, coords.y];
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a preview of this layer's object type from a world document and show its sheet to be finalized.
|
||
|
|
* @param {object} createData The data to create the object with.
|
||
|
|
* @param {object} [options] Options which configure preview creation
|
||
|
|
* @param {boolean} [options.renderSheet] Render the preview object config sheet?
|
||
|
|
* @param {number} [options.top] The offset-top position where the sheet should be rendered
|
||
|
|
* @param {number} [options.left] The offset-left position where the sheet should be rendered
|
||
|
|
* @returns {PlaceableObject} The created preview object
|
||
|
|
* @internal
|
||
|
|
*/
|
||
|
|
async _createPreview(createData, {renderSheet=true, top=0, left=0}={}) {
|
||
|
|
const documentName = this.constructor.documentName;
|
||
|
|
const cls = getDocumentClass(documentName);
|
||
|
|
const document = new cls(createData, {parent: canvas.scene});
|
||
|
|
if ( !document.canUserModify(game.user, "create") ) {
|
||
|
|
return ui.notifications.warn(game.i18n.format("PERMISSION.WarningNoCreate", {document: documentName}));
|
||
|
|
}
|
||
|
|
|
||
|
|
const object = new CONFIG[documentName].objectClass(document);
|
||
|
|
this.activate();
|
||
|
|
this.preview.addChild(object);
|
||
|
|
await object.draw();
|
||
|
|
|
||
|
|
if ( renderSheet ) object.sheet.render(true, {top, left});
|
||
|
|
return object;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Event Listeners and Handlers */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_onClickLeft(event) {
|
||
|
|
if ( !event.target.hasActiveHUD ) this.hud?.clear();
|
||
|
|
if ( this.options.controllableObjects && game.settings.get("core", "leftClickRelease") && !this.hover ) {
|
||
|
|
this.releaseAll();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_canDragLeftStart(user, event) {
|
||
|
|
if ( game.paused && !game.user.isGM ) {
|
||
|
|
ui.notifications.warn("GAME.PausedWarning", {localize: true});
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_onDragLeftStart(event) {
|
||
|
|
this.clearPreviewContainer();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_onDragLeftMove(event) {
|
||
|
|
const preview = event.interactionData.preview;
|
||
|
|
if ( !preview || preview._destroyed ) return;
|
||
|
|
if ( preview.parent === null ) { // In theory this should never happen, but rarely does
|
||
|
|
this.preview.addChild(preview);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_onDragLeftDrop(event) {
|
||
|
|
const preview = event.interactionData.preview;
|
||
|
|
if ( !preview || preview._destroyed ) return;
|
||
|
|
event.interactionData.clearPreviewContainer = false;
|
||
|
|
const cls = getDocumentClass(this.constructor.documentName);
|
||
|
|
cls.create(preview.document.toObject(false), {parent: canvas.scene})
|
||
|
|
.finally(() => this.clearPreviewContainer());
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_onDragLeftCancel(event) {
|
||
|
|
if ( event.interactionData?.clearPreviewContainer !== false ) {
|
||
|
|
this.clearPreviewContainer();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_onClickRight(event) {
|
||
|
|
if ( !event.target.hasActiveHUD ) this.hud?.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_onMouseWheel(event) {
|
||
|
|
|
||
|
|
// Prevent wheel rotation during dragging
|
||
|
|
if ( this.preview.children.length ) return;
|
||
|
|
|
||
|
|
// Determine the incremental angle of rotation from event data
|
||
|
|
const snap = event.shiftKey ? (canvas.grid.isHexagonal ? 30 : 45) : 15;
|
||
|
|
const delta = snap * Math.sign(event.delta);
|
||
|
|
return this.rotateMany({delta, snap});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
async _onDeleteKey(event) {
|
||
|
|
if ( game.paused && !game.user.isGM ) {
|
||
|
|
ui.notifications.warn("GAME.PausedWarning", {localize: true});
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Identify objects which are candidates for deletion
|
||
|
|
const objects = this.options.controllableObjects ? this.controlled : (this.hover ? [this.hover] : []);
|
||
|
|
if ( !objects.length ) return;
|
||
|
|
|
||
|
|
// Restrict to objects which can be deleted
|
||
|
|
const ids = objects.reduce((ids, o) => {
|
||
|
|
const isDragged = (o.interactionState === MouseInteractionManager.INTERACTION_STATES.DRAG);
|
||
|
|
if ( isDragged || o.document.locked || !o.document.canUserModify(game.user, "delete") ) return ids;
|
||
|
|
if ( this.hover === o ) this.hover = null;
|
||
|
|
ids.push(o.id);
|
||
|
|
return ids;
|
||
|
|
}, []);
|
||
|
|
if ( ids.length ) {
|
||
|
|
const typeLabel = game.i18n.localize(getDocumentClass(this.constructor.documentName).metadata.label);
|
||
|
|
if ( this.options.confirmDeleteKey ) {
|
||
|
|
const confirmed = await foundry.applications.api.DialogV2.confirm({
|
||
|
|
window: {
|
||
|
|
title: game.i18n.format("DOCUMENT.Delete", {type: typeLabel})
|
||
|
|
},
|
||
|
|
content: `<p>${game.i18n.localize("AreYouSure")}</p>`,
|
||
|
|
rejectClose: false
|
||
|
|
});
|
||
|
|
if ( !confirmed ) return;
|
||
|
|
}
|
||
|
|
const deleted = await canvas.scene.deleteEmbeddedDocuments(this.constructor.documentName, ids);
|
||
|
|
if ( deleted.length !== 1) ui.notifications.info(game.i18n.format("CONTROLS.DeletedObjects", {
|
||
|
|
count: deleted.length, type: typeLabel}));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Deprecations and Compatibility */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v12
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
get gridPrecision() {
|
||
|
|
const msg = "PlaceablesLayer#gridPrecision is deprecated. Use PlaceablesLayer#getSnappedPoint "
|
||
|
|
+ "instead of GridLayer#getSnappedPosition and PlaceablesLayer#gridPrecision.";
|
||
|
|
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
|
||
|
|
const grid = canvas.grid;
|
||
|
|
if ( grid.type === CONST.GRID_TYPES.GRIDLESS ) return 0; // No snapping for gridless
|
||
|
|
if ( grid.type === CONST.GRID_TYPES.SQUARE ) return 2; // Corners and centers
|
||
|
|
return this.options.controllableObjects ? 2 : 5; // Corners or vertices
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v11
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
get _highlight() {
|
||
|
|
const msg = "PlaceablesLayer#_highlight is deprecated. Use PlaceablesLayer#highlightObjects instead.";
|
||
|
|
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
|
||
|
|
return this.highlightObjects;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v11
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
set _highlight(state) {
|
||
|
|
const msg = "PlaceablesLayer#_highlight is deprecated. Use PlaceablesLayer#highlightObjects instead.";
|
||
|
|
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
|
||
|
|
this.highlightObjects = !!state;
|
||
|
|
}
|
||
|
|
}
|