/** * An Abstract Base Class which defines a Placeable Object which represents a Document placed on the Canvas * @extends {PIXI.Container} * @abstract * @interface * * @param {abstract.Document} document The Document instance which is represented by this object */ class PlaceableObject extends RenderFlagsMixin(PIXI.Container) { constructor(document) { super(); if ( !(document instanceof foundry.abstract.Document) || !document.isEmbedded ) { throw new Error("You must provide an embedded Document instance as the input for a PlaceableObject"); } /** * Retain a reference to the Scene within which this Placeable Object resides * @type {Scene} */ this.scene = document.parent; /** * A reference to the Scene embedded Document instance which this object represents * @type {abstract.Document} */ this.document = document; /** * A control icon for interacting with the object * @type {ControlIcon|null} */ this.controlIcon = null; /** * A mouse interaction manager instance which handles mouse workflows related to this object. * @type {MouseInteractionManager} */ this.mouseInteractionManager = null; // Allow objects to be culled when off-screen this.cullable = true; } /* -------------------------------------------- */ /* Properties */ /* -------------------------------------------- */ /** * Identify the official Document name for this PlaceableObject class * @type {string} */ static embeddedName; /** * The flags declared here are required for all PlaceableObject subclasses to also support. * @override */ static RENDER_FLAGS = { redraw: {propagate: ["refresh"]}, refresh: {propagate: ["refreshState"], alias: true}, refreshState: {} }; /** * The object that this object is a preview of if this object is a preview. * @type {PlaceableObject|undefined} */ get _original() { return this.#original; } /** * The object that this object is a preview of if this object is a preview. * @type {PlaceableObject|undefined} */ #original; /* -------------------------------------------- */ /** * The bounds that the placeable was added to the quadtree with. * @type {PIXI.Rectangle} */ #lastQuadtreeBounds; /** * An internal reference to a Promise in-progress to draw the Placeable Object. * @type {Promise} */ #drawing = Promise.resolve(this); /** * Has this Placeable Object been drawn and is there no drawing in progress? * @type {boolean} */ #drawn = false; /* -------------------------------------------- */ /** * A convenient reference for whether the current User has full control over the document. * @type {boolean} */ get isOwner() { return this.document.isOwner; } /* -------------------------------------------- */ /** * The mouse interaction state of this placeable. * @type {MouseInteractionManager.INTERACTION_STATES|undefined} */ get interactionState() { return this._original?.mouseInteractionManager?.state ?? this.mouseInteractionManager?.state; } /* -------------------------------------------- */ /** * The bounding box for this PlaceableObject. * This is required if the layer uses a Quadtree, otherwise it is optional * @type {PIXI.Rectangle} */ get bounds() { throw new Error("Each subclass of PlaceableObject must define its own bounds rectangle"); } /* -------------------------------------------- */ /** * The central coordinate pair of the placeable object based on it's own width and height * @type {PIXI.Point} */ get center() { const d = this.document; if ( ("width" in d) && ("height" in d) ) { return new PIXI.Point(d.x + (d.width / 2), d.y + (d.height / 2)); } return new PIXI.Point(d.x, d.y); } /* -------------------------------------------- */ /** * The id of the corresponding Document which this PlaceableObject represents. * @type {string} */ get id() { return this.document.id; } /* -------------------------------------------- */ /** * A unique identifier which is used to uniquely identify elements on the canvas related to this object. * @type {string} */ get objectId() { let id = `${this.document.documentName}.${this.document.id}`; if ( this.isPreview ) id += ".preview"; return id; } /* -------------------------------------------- */ /** * The named identified for the source object associated with this PlaceableObject. * This differs from the objectId because the sourceId is the same for preview objects as for the original. * @type {string} */ get sourceId() { return `${this.document.documentName}.${this._original?.id ?? this.document.id ?? "preview"}`; } /* -------------------------------------------- */ /** * Is this placeable object a temporary preview? * @type {boolean} */ get isPreview() { return !!this._original || !this.document.id; } /* -------------------------------------------- */ /** * Does there exist a temporary preview of this placeable object? * @type {boolean} */ get hasPreview() { return !!this._preview; } /* -------------------------------------------- */ /** * Provide a reference to the CanvasLayer which contains this PlaceableObject. * @type {PlaceablesLayer} */ get layer() { return this.document.layer; } /* -------------------------------------------- */ /** * A Form Application which is used to configure the properties of this Placeable Object or the Document it * represents. * @type {FormApplication} */ get sheet() { return this.document.sheet; } /** * An indicator for whether the object is currently controlled * @type {boolean} */ get controlled() { return this.#controlled; } #controlled = false; /* -------------------------------------------- */ /** * An indicator for whether the object is currently a hover target * @type {boolean} */ get hover() { return this.#hover; } set hover(state) { this.#hover = typeof state === "boolean" ? state : false; } #hover = false; /* -------------------------------------------- */ /** * Is the HUD display active for this Placeable? * @returns {boolean} */ get hasActiveHUD() { return this.layer.hud?.object === this; } /* -------------------------------------------- */ /** * Get the snapped position for a given position or the current position. * @param {Point} [position] The position to be used instead of the current position * @returns {Point} The snapped position */ getSnappedPosition(position) { return this.layer.getSnappedPoint(position ?? this.document); } /* -------------------------------------------- */ /* Rendering */ /* -------------------------------------------- */ /** @override */ applyRenderFlags() { if ( !this.renderFlags.size || this._destroyed ) return; const flags = this.renderFlags.clear(); // Full re-draw if ( flags.redraw ) { this.draw(); return; } // Don't refresh until the object is drawn if ( !this.#drawn ) return; // Incremental refresh this._applyRenderFlags(flags); Hooks.callAll(`refresh${this.document.documentName}`, this, flags); } /* -------------------------------------------- */ /** * Apply render flags before a render occurs. * @param {Record} flags The render flags which must be applied * @protected */ _applyRenderFlags(flags) {} /* -------------------------------------------- */ /** * Clear the display of the existing object. * @returns {PlaceableObject} The cleared object */ clear() { this.removeChildren().forEach(c => c.destroy({children: true})); return this; } /* -------------------------------------------- */ /** @inheritdoc */ destroy(options) { this.mouseInteractionManager?.cancel(); MouseInteractionManager.emulateMoveEvent(); if ( this._original ) this._original._preview = undefined; this.document._object = null; this.document._destroyed = true; if ( this.controlIcon ) this.controlIcon.destroy(); this.renderFlags.clear(); Hooks.callAll(`destroy${this.document.documentName}`, this); this._destroy(options); return super.destroy(options); } /** * The inner _destroy method which may optionally be defined by each PlaceableObject subclass. * @param {object} [options] Options passed to the initial destroy call * @protected */ _destroy(options) {} /* -------------------------------------------- */ /** * Draw the placeable object into its parent container * @param {object} [options] Options which may modify the draw and refresh workflow * @returns {Promise} The drawn object */ async draw(options={}) { return this.#drawing = this.#drawing.finally(async () => { this.#drawn = false; const wasVisible = this.visible; const wasRenderable = this.renderable; this.visible = false; this.renderable = false; this.clear(); this.mouseInteractionManager?.cancel(); MouseInteractionManager.emulateMoveEvent(); await this._draw(options); Hooks.callAll(`draw${this.document.documentName}`, this); this.renderFlags.set({refresh: true}); // Refresh all flags if ( this.id ) this.activateListeners(); this.visible = wasVisible; this.renderable = wasRenderable; this.#drawn = true; MouseInteractionManager.emulateMoveEvent(); }); } /** * The inner _draw method which must be defined by each PlaceableObject subclass. * @param {object} options Options which may modify the draw workflow * @abstract * @protected */ async _draw(options) { throw new Error(`The ${this.constructor.name} subclass of PlaceableObject must define the _draw method`); } /* -------------------------------------------- */ /** * Execute a partial draw. * @param {() => Promise} fn The draw function * @returns {Promise} The drawn object * @internal */ async _partialDraw(fn) { return this.#drawing = this.#drawing.finally(async () => { if ( !this.#drawn ) return; await fn(); }); } /* -------------------------------------------- */ /** * Refresh all incremental render flags for the PlaceableObject. * This method is no longer used by the core software but provided for backwards compatibility. * @param {object} [options] Options which may modify the refresh workflow * @returns {PlaceableObject} The refreshed object */ refresh(options={}) { this.renderFlags.set({refresh: true}); return this; } /* -------------------------------------------- */ /** * Update the quadtree. * @internal */ _updateQuadtree() { const layer = this.layer; if ( !layer.quadtree || this.isPreview ) return; if ( this.destroyed || this.parent !== layer.objects ) { this.#lastQuadtreeBounds = undefined; layer.quadtree.remove(this); return; } const bounds = this.bounds; if ( !this.#lastQuadtreeBounds || bounds.x !== this.#lastQuadtreeBounds.x || bounds.y !== this.#lastQuadtreeBounds.y || bounds.width !== this.#lastQuadtreeBounds.width || bounds.height !== this.#lastQuadtreeBounds.height ) { this.#lastQuadtreeBounds = bounds; layer.quadtree.update({r: bounds, t: this}); } } /* -------------------------------------------- */ /** * Is this PlaceableObject within the selection rectangle? * @param {PIXI.Rectangle} rectangle The selection rectangle * @protected * @internal */ _overlapsSelection(rectangle) { const {x, y} = this.center; return rectangle.contains(x, y); } /* -------------------------------------------- */ /** * Get the target opacity that should be used for a Placeable Object depending on its preview state. * @returns {number} * @protected */ _getTargetAlpha() { const isDragging = this._original?.mouseInteractionManager?.isDragging ?? this.mouseInteractionManager?.isDragging; return isDragging ? (this.isPreview ? 0.8 : (this.hasPreview ? 0.4 : 1)) : 1; } /* -------------------------------------------- */ /** * Register pending canvas operations which should occur after a new PlaceableObject of this type is created * @param {object} data * @param {object} options * @param {string} userId * @protected */ _onCreate(data, options, userId) {} /* -------------------------------------------- */ /** * Define additional steps taken when an existing placeable object of this type is updated with new data * @param {object} changed * @param {object} options * @param {string} userId * @protected */ _onUpdate(changed, options, userId) { this._updateQuadtree(); if ( this.parent && (("elevation" in changed) || ("sort" in changed)) ) this.parent.sortDirty = true; } /* -------------------------------------------- */ /** * Define additional steps taken when an existing placeable object of this type is deleted * @param {object} options * @param {string} userId * @protected */ _onDelete(options, userId) { this.release({trigger: false}); const layer = this.layer; if ( layer.hover === this ) layer.hover = null; this.destroy({children: true}); } /* -------------------------------------------- */ /* Methods */ /* -------------------------------------------- */ /** * Assume control over a PlaceableObject, flagging it as controlled and enabling downstream behaviors * @param {Object} options Additional options which modify the control request * @param {boolean} options.releaseOthers Release any other controlled objects first * @returns {boolean} A flag denoting whether control was successful */ control(options={}) { if ( !this.layer.options.controllableObjects ) return false; // Release other controlled objects if ( options.releaseOthers !== false ) { for ( let o of this.layer.controlled ) { if ( o !== this ) o.release(); } } // Bail out if this object is already controlled, or not controllable if ( this.#controlled || !this.id ) return true; if ( !this.can(game.user, "control") ) return false; // Toggle control status this.#controlled = true; this.layer.controlledObjects.set(this.id, this); // Trigger follow-up events and fire an on-control Hook this._onControl(options); Hooks.callAll(`control${this.constructor.embeddedName}`, this, this.#controlled); return true; } /* -------------------------------------------- */ /** * Additional events which trigger once control of the object is established * @param {Object} options Optional parameters which apply for specific implementations * @protected */ _onControl(options) { this.renderFlags.set({refreshState: true}); } /* -------------------------------------------- */ /** * Release control over a PlaceableObject, removing it from the controlled set * @param {object} options Options which modify the releasing workflow * @returns {boolean} A Boolean flag confirming the object was released. */ release(options={}) { this.layer.controlledObjects.delete(this.id); if ( !this.#controlled ) return true; this.#controlled = false; // Trigger follow-up events this._onRelease(options); // Fire an on-release Hook Hooks.callAll(`control${this.constructor.embeddedName}`, this, this.#controlled); return true; } /* -------------------------------------------- */ /** * Additional events which trigger once control of the object is released * @param {object} options Options which modify the releasing workflow * @protected */ _onRelease(options) { const layer = this.layer; this.hover = false; if ( this === layer.hover ) layer.hover = null; if ( this.hasActiveHUD ) layer.hud.clear(); this.renderFlags.set({refreshState: true}); } /* -------------------------------------------- */ /** * Clone the placeable object, returning a new object with identical attributes. * The returned object is non-interactive, and has no assigned ID. * If you plan to use it permanently you should call the create method. * @returns {PlaceableObject} A new object with identical data */ clone() { const cloneDoc = this.document.clone({}, {keepId: true}); const clone = new this.constructor(cloneDoc); cloneDoc._object = clone; clone.#original = this; clone.eventMode = "none"; clone.#controlled = this.#controlled; this._preview = clone; return clone; } /* -------------------------------------------- */ /** * Rotate the PlaceableObject to a certain angle of facing * @param {number} angle The desired angle of rotation * @param {number} snap Snap the angle of rotation to a certain target degree increment * @returns {Promise} The rotated object */ async rotate(angle, snap) { if ( !this.document.schema.has("rotation") ) return this; if ( game.paused && !game.user.isGM ) { ui.notifications.warn("GAME.PausedWarning", {localize: true}); return this; } const rotation = this._updateRotation({angle, snap}); await this.document.update({rotation}); return this; } /* -------------------------------------------- */ /** * Determine a new angle of rotation for a PlaceableObject either from an explicit angle or from a delta offset. * @param {object} options An object which defines the rotation update parameters * @param {number} [options.angle] An explicit angle, either this or delta must be provided * @param {number} [options.delta=0] A relative angle delta, either this or the angle must be provided * @param {number} [options.snap=0] A precision (in degrees) to which the resulting angle should snap. Default is 0. * @returns {number} The new rotation angle for the object * @internal */ _updateRotation({angle, delta=0, snap=0}={}) { let degrees = Number.isNumeric(angle) ? angle : this.document.rotation + delta; if ( snap > 0 ) degrees = degrees.toNearest(snap); return Math.normalizeDegrees(degrees); } /* -------------------------------------------- */ /** * Obtain a shifted position for the Placeable Object * @param {-1|0|1} dx The number of grid units to shift along the X-axis * @param {-1|0|1} dy The number of grid units to shift along the Y-axis * @returns {Point} The shifted target coordinates * @internal */ _getShiftedPosition(dx, dy) { const {x, y} = this.document; const snapped = this.getSnappedPosition(); const D = CONST.MOVEMENT_DIRECTIONS; let direction = 0; if ( dx < 0 ) { if ( x <= snapped.x + 0.5 ) direction |= D.LEFT; } else if ( dx > 0 ) { if ( x >= snapped.x - 0.5 ) direction |= D.RIGHT; } if ( dy < 0 ) { if ( y <= snapped.y + 0.5 ) direction |= D.UP; } else if ( dy > 0 ) { if ( y >= snapped.y - 0.5 ) direction |= D.DOWN; } const grid = this.scene.grid; let biasX = 0; let biasY = 0; if ( grid.isHexagonal ) { if ( grid.columns ) biasY = 1; else biasX = 1; } snapped.x += biasX; snapped.y += biasY; const shifted = grid.getShiftedPoint(snapped, direction); shifted.x -= biasX; shifted.y -= biasY; return shifted; } /* -------------------------------------------- */ /* Interactivity */ /* -------------------------------------------- */ /** * Activate interactivity for the Placeable Object */ activateListeners() { const mgr = this._createInteractionManager(); this.mouseInteractionManager = mgr.activate(); } /* -------------------------------------------- */ /** * Create a standard MouseInteractionManager for the PlaceableObject * @protected */ _createInteractionManager() { // Handle permissions to perform various actions const permissions = { hoverIn: this._canHover, clickLeft: this._canControl, clickLeft2: this._canView, clickRight: this._canHUD, clickRight2: this._canConfigure, dragStart: this._canDrag, dragLeftStart: this._canDragLeftStart }; // Define callback functions for each workflow step const callbacks = { hoverIn: this._onHoverIn, hoverOut: this._onHoverOut, clickLeft: this._onClickLeft, clickLeft2: this._onClickLeft2, clickRight: this._onClickRight, clickRight2: this._onClickRight2, unclickLeft: this._onUnclickLeft, unclickRight: this._onUnclickRight, dragLeftStart: this._onDragLeftStart, dragLeftMove: this._onDragLeftMove, dragLeftDrop: this._onDragLeftDrop, dragLeftCancel: this._onDragLeftCancel, dragRightStart: this._onDragRightStart, dragRightMove: this._onDragRightMove, dragRightDrop: this._onDragRightDrop, dragRightCancel: this._onDragRightCancel, longPress: this._onLongPress }; // Define options const options = { target: this.controlIcon ? "controlIcon" : null }; // Create the interaction manager return new MouseInteractionManager(this, canvas.stage, permissions, callbacks, options); } /* -------------------------------------------- */ /** * Test whether a user can perform a certain interaction regarding a Placeable Object * @param {User} user The User performing the action * @param {string} action The named action being attempted * @returns {boolean} Does the User have rights to perform the action? */ can(user, action) { const fn = this[`_can${action.titleCase()}`]; return fn ? fn.call(this, user) : false; } /* -------------------------------------------- */ /** * Can the User access the HUD for this Placeable Object? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canHUD(user, event) { return this.isOwner; } /* -------------------------------------------- */ /** * Does the User have permission to configure the Placeable Object? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canConfigure(user, event) { return this.document.canUserModify(user, "update"); } /* -------------------------------------------- */ /** * Does the User have permission to control the Placeable Object? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canControl(user, event) { if ( !this.layer.active || this.isPreview ) return false; return this.document.canUserModify(user, "update"); } /* -------------------------------------------- */ /** * Does the User have permission to view details of the Placeable Object? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canView(user, event) { return this.document.testUserPermission(user, "LIMITED"); } /* -------------------------------------------- */ /** * Does the User have permission to create the underlying Document? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canCreate(user, event) { return user.isGM; } /* -------------------------------------------- */ /** * Does the User have permission to drag this Placeable Object? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canDrag(user, event) { return this._canControl(user, event); } /* -------------------------------------------- */ /** * Does the User have permission to left-click drag this Placeable Object? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canDragLeftStart(user, event) { if ( game.paused && !game.user.isGM ) { ui.notifications.warn("GAME.PausedWarning", {localize: true}); return false; } if ( this.document.schema.has("locked") && this.document.locked ) { ui.notifications.warn(game.i18n.format("CONTROLS.ObjectIsLocked", { type: game.i18n.localize(this.document.constructor.metadata.label)})); return false; } return true; } /* -------------------------------------------- */ /** * Does the User have permission to hover on this Placeable Object? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canHover(user, event) { return this._canControl(user, event); } /* -------------------------------------------- */ /** * Does the User have permission to update the underlying Document? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canUpdate(user, event) { return this._canControl(user, event); } /* -------------------------------------------- */ /** * Does the User have permission to delete the underlying Document? * @param {User} user The User performing the action. * @param {object} event The event object. * @returns {boolean} The returned status. * @protected */ _canDelete(user, event) { return this._canControl(user, event); } /* -------------------------------------------- */ /** * Actions that should be taken for this Placeable Object when a mouseover event occurs. * Hover events on PlaceableObject instances allow event propagation by default. * @see MouseInteractionManager##handlePointerOver * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @param {object} options Options which customize event handling * @param {boolean} [options.hoverOutOthers=false] Trigger hover-out behavior on sibling objects * @protected */ _onHoverIn(event, {hoverOutOthers=false}={}) { if ( this.hover ) return; if ( event.buttons & 0x03 ) return; // Returning if hovering is happening with pressed left or right button // Handle the event const layer = this.layer; layer.hover = this; if ( hoverOutOthers ) { for ( const o of layer.placeables ) { if ( o !== this ) o._onHoverOut(event); } } this.hover = true; // Set render flags this.renderFlags.set({refreshState: true}); Hooks.callAll(`hover${this.constructor.embeddedName}`, this, this.hover); } /* -------------------------------------------- */ /** * Actions that should be taken for this Placeable Object when a mouseout event occurs * @see MouseInteractionManager##handlePointerOut * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onHoverOut(event) { if ( !this.hover ) return; // Handle the event const layer = this.layer; layer.hover = null; this.hover = false; // Set render flags this.renderFlags.set({refreshState: true}); Hooks.callAll(`hover${this.constructor.embeddedName}`, this, this.hover); } /* -------------------------------------------- */ /** * Should the placeable propagate left click downstream? * @param {PIXI.FederatedEvent} event * @returns {boolean} * @protected */ _propagateLeftClick(event) { return false; } /* -------------------------------------------- */ /** * Callback actions which occur on a single left-click event to assume control of the object * @see MouseInteractionManager##handleClickLeft * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onClickLeft(event) { this.layer.hud?.clear(); // Add or remove the Placeable Object from the currently controlled set if ( !this.#controlled ) this.control({releaseOthers: !event.shiftKey}); else if ( event.shiftKey ) event.interactionData.release = true; // Release on unclick // Propagate left click to the underlying canvas? if ( !this._propagateLeftClick(event) ) event.stopPropagation(); } /* -------------------------------------------- */ /** * Callback actions which occur on a single left-unclick event to assume control of the object * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onUnclickLeft(event) { // Remove Placeable Object from the currently controlled set if ( event.interactionData.release === true ) this.release(); // Propagate left click to the underlying canvas? if ( !this._propagateLeftClick(event) ) event.stopPropagation(); } /* -------------------------------------------- */ /** * Callback actions which occur on a double left-click event to activate * @see MouseInteractionManager##handleClickLeft2 * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onClickLeft2(event) { const sheet = this.sheet; if ( sheet ) sheet.render(true); if ( !this._propagateLeftClick(event) ) event.stopPropagation(); } /* -------------------------------------------- */ /** * Should the placeable propagate right click downstream? * @param {PIXI.FederatedEvent} event * @returns {boolean} * @protected */ _propagateRightClick(event) { return false; } /* -------------------------------------------- */ /** * Callback actions which occur on a single right-click event to configure properties of the object * @see MouseInteractionManager##handleClickRight * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onClickRight(event) { if ( this.layer.hud ) { const releaseOthers = !this.#controlled && !event.shiftKey; this.control({releaseOthers}); if ( this.hasActiveHUD ) this.layer.hud.clear(); else this.layer.hud.bind(this); } // Propagate the right-click to the underlying canvas? if ( !this._propagateRightClick(event) ) event.stopPropagation(); } /* -------------------------------------------- */ /** * Callback actions which occur on a single right-unclick event * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onUnclickRight(event) { // Propagate right-click to the underlying canvas? if ( !this._propagateRightClick(event) ) event.stopPropagation(); } /* -------------------------------------------- */ /** * Callback actions which occur on a double right-click event to configure properties of the object * @see MouseInteractionManager##handleClickRight2 * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onClickRight2(event) { const sheet = this.sheet; if ( sheet ) sheet.render(true); if ( !this._propagateRightClick(event) ) event.stopPropagation(); } /* -------------------------------------------- */ /** * Callback actions which occur when a mouse-drag action is first begun. * @see MouseInteractionManager##handleDragStart * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onDragLeftStart(event) { const objects = this.layer.options.controllableObjects ? this.layer.controlled : [this]; const clones = []; for ( const o of objects ) { if ( !o._canDrag(game.user, event) ) continue; // FIXME: Find a better solution such that any object for which _canDragLeftStart // would return false is included in the drag operation. The locked state might not // be the only condition that prevents dragging that is checked in _canDragLeftStart. if ( o.document.locked ) continue; // Clone the object const c = o.clone(); clones.push(c); // Draw the clone c._onDragStart(); c.visible = false; this.layer.preview.addChild(c); c.draw().then(c => c.visible = true); } event.interactionData.clones = clones; } /* -------------------------------------------- */ /** * Begin a drag operation from the perspective of the preview clone. * Modify the appearance of both the clone (this) and the original (_original) object. * @protected */ _onDragStart() { const o = this._original; o.document.locked = true; o.renderFlags.set({refreshState: true}); } /* -------------------------------------------- */ /** * Conclude a drag operation from the perspective of the preview clone. * Modify the appearance of both the clone (this) and the original (_original) object. * @protected */ _onDragEnd() { const o = this._original; if ( o ) { o.document.locked = o.document._source.locked; o.renderFlags.set({refreshState: true}); } } /* -------------------------------------------- */ /** * Callback actions which occur on a mouse-move operation. * @see MouseInteractionManager##handleDragMove * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onDragLeftMove(event) { canvas._onDragCanvasPan(event); const {clones, destination, origin} = event.interactionData; // Calculate the (snapped) position of the dragged object let position = { x: this.document.x + (destination.x - origin.x), y: this.document.y + (destination.y - origin.y) }; if ( !event.shiftKey ) position = this.getSnappedPosition(position); // Move all other objects in the selection relative to the the dragged object. // We want to avoid that the dragged object doesn't move when the cursor is moved, // because it snaps to the same position, but other objects in the selection do. const dx = position.x - this.document.x; const dy = position.y - this.document.y; for ( const c of clones || [] ) { const o = c._original; let position = {x: o.document.x + dx, y: o.document.y + dy}; if ( !event.shiftKey ) position = this.getSnappedPosition(position); c.document.x = position.x; c.document.y = position.y; c.renderFlags.set({refreshPosition: true}); } } /* -------------------------------------------- */ /** * Callback actions which occur on a mouse-move operation. * @see MouseInteractionManager##handleDragDrop * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onDragLeftDrop(event) { // Ensure that we landed in bounds const {clones, destination} = event.interactionData; if ( !clones || !canvas.dimensions.rect.contains(destination.x, destination.y) ) return false; event.interactionData.clearPreviewContainer = false; // Perform database updates using dropped data const updates = this._prepareDragLeftDropUpdates(event); // noinspection ES6MissingAwait if ( updates ) this.#commitDragLeftDropUpdates(updates); } /* -------------------------------------------- */ /** * Perform the database updates that should occur as the result of a drag-left-drop operation. * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @returns {object[]|null} An array of database updates to perform for documents in this collection */ _prepareDragLeftDropUpdates(event) { const updates = []; for ( const clone of event.interactionData.clones ) { let dest = {x: clone.document.x, y: clone.document.y}; if ( !event.shiftKey ) dest = this.getSnappedPosition(dest); updates.push({_id: clone._original.id, x: dest.x, y: dest.y, rotation: clone.document.rotation}); } return updates; } /* -------------------------------------------- */ /** * Perform database updates using the result of a drag-left-drop operation. * @param {object[]} updates The database updates for documents in this collection * @returns {Promise} */ async #commitDragLeftDropUpdates(updates) { for ( const u of updates ) { const d = this.document.collection.get(u._id); if ( d ) d.locked = d._source.locked; // Unlock original documents } await canvas.scene.updateEmbeddedDocuments(this.document.documentName, updates); this.layer.clearPreviewContainer(); } /* -------------------------------------------- */ /** * Callback actions which occur on a mouse-move operation. * @see MouseInteractionManager##handleDragCancel * @param {PIXI.FederatedEvent} event The triggering mouse click event * @protected */ _onDragLeftCancel(event) { if ( event.interactionData.clearPreviewContainer !== false ) { this.layer.clearPreviewContainer(); } } /* -------------------------------------------- */ /** * Callback actions which occur on a right mouse-drag operation. * @see MouseInteractionManager##handleDragStart * @param {PIXI.FederatedEvent} event The triggering mouse click event * @protected */ _onDragRightStart(event) { return canvas._onDragRightStart(event); } /* -------------------------------------------- */ /** * Callback actions which occur on a right mouse-drag operation. * @see MouseInteractionManager##handleDragMove * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @protected */ _onDragRightMove(event) { return canvas._onDragRightMove(event); } /* -------------------------------------------- */ /** * Callback actions which occur on a right mouse-drag operation. * @see MouseInteractionManager##handleDragDrop * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @returns {Promise<*>} * @protected */ _onDragRightDrop(event) { return canvas._onDragRightDrop(event); } /* -------------------------------------------- */ /** * Callback actions which occur on a right mouse-drag operation. * @see MouseInteractionManager##handleDragCancel * @param {PIXI.FederatedEvent} event The triggering mouse click event * @protected */ _onDragRightCancel(event) { return canvas._onDragRightCancel(event); } /* -------------------------------------------- */ /** * Callback action which occurs on a long press. * @see MouseInteractionManager##handleLongPress * @param {PIXI.FederatedEvent} event The triggering canvas interaction event * @param {PIXI.Point} origin The local canvas coordinates of the mousepress. * @protected */ _onLongPress(event, origin) { return canvas.controls._onLongPress(event, origin); } }