From b292a177a672b88aa0902984da5bd4cc86800403 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Sat, 9 Mar 2024 17:16:46 -0800 Subject: [PATCH 1/2] [#893] Add system for selecting summoning placement The `TokenPlacement` class right now is small and only delegates to the `TokenPlacementTemplate` which is responsible for rendering and returning placement information. With the V12 improvements to placeables hopefully we'll be able to shift more logic out of the `MeasuredTemplate` subclass and into the `TokenPlacement` class. The `TokenPlacementConfiguration` data structure currently just includes the prototype token information, but will eventually have quantity, origin, and range values to handle multiple summons and restricting range from the summoner. Works with tokens of any size or scale on the square grid. On hex grids, it handles 1x1 tokens pretty good with only a bit of offset on the final token placement. Stranger token sizes lead to some issues with positioning, but I'm not sure how important those are to fix before the other grid improvements in V12. --- module/canvas/_module.mjs | 1 + module/canvas/token-placement.mjs | 410 ++++++++++++++++++++++ module/data/item/fields/summons-field.mjs | 18 +- 3 files changed, 416 insertions(+), 13 deletions(-) create mode 100644 module/canvas/token-placement.mjs diff --git a/module/canvas/_module.mjs b/module/canvas/_module.mjs index 97ff4bef7e..9665121d10 100644 --- a/module/canvas/_module.mjs +++ b/module/canvas/_module.mjs @@ -3,6 +3,7 @@ export * as detectionModes from "./detection-modes/_module.mjs"; export {measureDistances} from "./grid.mjs"; export {default as Note5e} from "./note.mjs"; export {default as Token5e} from "./token.mjs"; +export {default as TokenPlacement} from "./token-placement.mjs"; export {default as TokenRing} from "./token-ring.mjs"; export {default as TokenRingSamplerShaderV11} from "./shaders/token-ring-shader-v11.mjs"; export {default as TokenRingSamplerShader} from "./shaders/token-ring-shader.mjs"; diff --git a/module/canvas/token-placement.mjs b/module/canvas/token-placement.mjs new file mode 100644 index 0000000000..581c22f8d5 --- /dev/null +++ b/module/canvas/token-placement.mjs @@ -0,0 +1,410 @@ +/** + * Configuration information for a token placement operation. + * + * @typedef {object} TokenPlacementConfiguration + * @property {PrototypeToken[]} tokens Prototype token information for rendering. + */ + +/** + * Data for token placement on the scene. + * + * @typedef {object} PlacementData + * @property {number} x + * @property {number} y + * @property {number} rotation + */ + +/** + * Class responsible for placing one or more tokens onto the scene. + * @param {TokenPlacementConfiguration} config Configuration information for placement. + */ +export default class TokenPlacement { + constructor(config) { + this.config = config; + } + + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /** + * Configuration information for the placements. + * @type {TokenPlacementConfiguration} + */ + config; + + /* -------------------------------------------- */ + /* Placement */ + /* -------------------------------------------- */ + + /** + * Perform the placement, asking player guidance when necessary. + * @returns {PlacementData[]} + */ + async place() { + const template = TokenPlacementTemplate.create(this.config); + const placement = await template.place(); + return placement; + } +} + + +/** + * A MeasuredTemplate used to visualize token placement in the scene. + */ +export class TokenPlacementTemplate extends MeasuredTemplate { + + /** + * Configuration information for this placement. + * @type {TokenPlacementConfiguration} + */ + config; + + /* -------------------------------------------- */ + + /** + * Track the bound event handlers so they can be properly canceled later. + * @type {object} + */ + #events; + + /* -------------------------------------------- */ + + /** + * The initially active CanvasLayer to re-activate after the workflow is complete. + * @type {CanvasLayer} + */ + #initialLayer; + + /** + * Placements that have been generated. + * @type {PlacementData[]} + */ + placements; + + /* -------------------------------------------- */ + + /** + * Track the timestamp when the last mouse move event was captured. + * @type {number} + */ + #moveTime = 0; + + /* -------------------------------------------- */ + + /** + * Determine whether the throttling period has passed. Set to `true` to reset period. + * @type {boolean} + */ + get throttle() { + return Date.now() - this.#moveTime <= 20; + } + + set throttle(value) { + this.#moveTime = Date.now(); + } + + /* -------------------------------------------- */ + + /** + * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance + * @param {TokenPlacementConfiguration} config Configuration data for the token placement. + * @param {object} [options={}] Options to modify the created template. + * @returns {TokenPlacementTemplate} The template object. + */ + static create(config, options={}) { + const templateData = foundry.utils.mergeObject({ + t: "ray", user: game.user.id, distance: config.tokens[0].width, width: config.tokens[0].height + }, options); + + const cls = getDocumentClass("MeasuredTemplate"); + const template = new cls(templateData, {parent: canvas.scene}); + const object = new this(template); + object.config = config; + object.placements = [{x: 0, y: 0, rotation: 0}]; + + return object; + } + + /* -------------------------------------------- */ + + /** + * Creates a preview of the token placement template. + * @returns {Promise} A promise that resolves with the final token position if completed. + */ + place() { + const initialLayer = canvas.activeLayer; + + // Draw the template and switch to the template layer + this.draw(); + this.layer.activate(); + this.layer.preview.addChild(this); + + // Activate interactivity + return this.activatePreviewListeners(initialLayer); + } + + /* -------------------------------------------- */ + /* Drawing */ + /* -------------------------------------------- */ + + /** @override */ + async _draw() { + + // Load Fill Texture + if ( this.document.texture ) await loadTexture(this.document.texture, {fallback: "icons/svg/hazard.svg"}); + this.texture = null; + + // Template Shape + this.template = this.addChild(new PIXI.Graphics()); + + // Token Icon + this.controlIcon = this.addChild(this.#createTokenIcon()); + await this.controlIcon.draw(); + + // Ruler Text + this.ruler = this.addChild(this.#drawRulerText()); + + // Enable highlighting for this template + canvas.grid.addHighlightLayer(this.highlightId); + } + + /* -------------------------------------------- */ + + /** + * Draw the ControlIcon for the MeasuredTemplate + * @returns {ControlIcon} + */ + #createTokenIcon() { + let icon = new TokenIcon({ + texture: this.config.tokens[0].randomImg ? this.config.tokens[0].actor.img : this.config.tokens[0].texture.src, + angle: this.document.direction, + scale: { x: this.config.tokens[0].texture.scaleX, y: this.config.tokens[0].texture.scaleY }, + width: this.config.tokens[0].width * canvas.dimensions.size, + height: this.config.tokens[0].height * canvas.dimensions.size + }); + icon.x -= this.config.tokens[0].width * 0.5; + icon.y -= this.config.tokens[0].height * 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; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + _applyRenderFlags(flags) { + this.controlIcon.angle = this.document.direction; + super._applyRenderFlags(flags); + + // Hide unnecessary elements + const highlightLayer = canvas.grid.getHighlightLayer(this.highlightId); + highlightLayer.visible = false; + this.ruler.visible = false; + this.template.visible = false; + } + + /* -------------------------------------------- */ + /* Event Handlers */ + /* -------------------------------------------- */ + + /** + * Pixel offset to ensure snapping occurs in middle of grid space. + * @type {{x: number, y: number}} + */ + get snapAdjustment() { + const size = canvas.dimensions.size; + switch ( canvas.grid.type ) { + case CONST.GRID_TYPES.SQUARE: + return { + x: this.config.tokens[0].width % 2 === 0 ? Math.round(size * 0.5) : 0, + y: this.config.tokens[0].height % 2 === 0 ? Math.round(size * 0.5) : 0 + }; + default: + return { x: 0, y: 0 }; + } + } + + /* -------------------------------------------- */ + + /** + * Interval for snapping token to the grid. + * @type {number} + */ + get snapInterval() { + // TODO: Figure out proper snapping sized based on token size + return canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ? 0 + : Math.max(this.config.tokens[0].width, this.config.tokens[0].height) > 0.5 ? 1 : 2; + } + + /* -------------------------------------------- */ + + /** + * Activate listeners for the template preview + * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete + * @returns {Promise} A promise that resolves with the final measured template if created. + */ + activatePreviewListeners(initialLayer) { + return new Promise((resolve, reject) => { + this.#initialLayer = initialLayer; + this.#events = { + cancel: this._onCancelPlacement.bind(this), + confirm: this._onConfirmPlacement.bind(this), + move: this._onMovePlacement.bind(this), + resolve, + reject, + rotate: this._onRotatePlacement.bind(this) + }; + + // Activate listeners + canvas.stage.on("mousemove", this.#events.move); + canvas.stage.on("mousedown", this.#events.confirm); + canvas.app.view.oncontextmenu = this.#events.cancel; + canvas.app.view.onwheel = this.#events.rotate; + }); + } + + /* -------------------------------------------- */ + + /** + * Shared code for when template placement ends by being confirmed or canceled. + * @param {Event} event Triggering event that ended the placement. + */ + async _finishPlacement(event) { + this.layer._onDragLeftCancel(event); + canvas.stage.off("mousemove", this.#events.move); + canvas.stage.off("mousedown", this.#events.confirm); + canvas.app.view.oncontextmenu = null; + canvas.app.view.onwheel = null; + this.#initialLayer.activate(); + } + + /* -------------------------------------------- */ + + /** + * Move the template preview when the mouse moves. + * @param {Event} event Triggering mouse event. + */ + _onMovePlacement(event) { + event.stopPropagation(); + if ( this.throttle ) return; + const adjustment = this.snapAdjustment; + const point = event.data.getLocalPosition(this.layer); + const center = canvas.grid.getCenter(point.x - adjustment.x, point.y - adjustment.y); + this.document.updateSource({ x: center[0] + adjustment.x, y: center[1] + adjustment.y }); + this.placements[0].x = this.document.x - Math.round((this.config.tokens[0].width * canvas.dimensions.size) / 2); + this.placements[0].y = this.document.y - Math.round((this.config.tokens[0].height * canvas.dimensions.size) / 2); + this.refresh(); + this.throttle = true; + } + + /* -------------------------------------------- */ + + /** + * Rotate the template preview by 3˚ increments when the mouse wheel is rotated. + * @param {Event} event Triggering mouse event. + */ + _onRotatePlacement(event) { + if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window + event.stopPropagation(); + const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15; + const snap = event.shiftKey ? delta : 5; + this.placements[0].rotation += snap * Math.sign(event.deltaY); + this.controlIcon.icon.angle = this.placements[0].rotation; + this.refresh(); + } + + /* -------------------------------------------- */ + + /** + * Confirm placement when the left mouse button is clicked. + * @param {Event} event Triggering mouse event. + */ + async _onConfirmPlacement(event) { + await this._finishPlacement(event); + this.#events.resolve(this.placements); + } + + /* -------------------------------------------- */ + + /** + * Cancel placement when the right mouse button is clicked. + * @param {Event} event Triggering mouse event. + */ + async _onCancelPlacement(event) { + await this._finishPlacement(event); + this.#events.reject(); + } +} + +/** + * A PIXI element for rendering a token preview. + */ +class TokenIcon extends PIXI.Container { + constructor({texture, width, height, angle, scale}={}, ...args) { + super(...args); + const size = Math.min(width, height); + this.textureSrc = texture; + this.width = this.height = size; + this.pivot.set(width * 0.5, height * 0.5); + + this.eventMode = "static"; + this.interactiveChildren = false; + + this.bg = this.addChild(new PIXI.Graphics()); + this.bg.clear().lineStyle(2, 0x000000, 1.0).drawRoundedRect(0, 0, width, height, 5).endFill(); + + const halfSize = Math.round(size * 0.5); + this.icon = this.addChild(new PIXI.Container()); + this.icon.width = size; + this.icon.height = size; + this.icon.pivot.set(halfSize, halfSize); + this.icon.x += halfSize; + this.icon.y += halfSize; + if ( width < height ) this.icon.y += Math.round((height - width) / 2); + else if ( height < width ) this.icon.x += Math.round((width - height) / 2); + + this.iconSprite = this.icon.addChild(new PIXI.Sprite()); + this.iconSprite.width = size * scale.x; + this.iconSprite.height = size * scale.y; + this.iconSprite.x -= (scale.x - 1) * halfSize; + this.iconSprite.y -= (scale.y - 1) * halfSize; + + this.draw(); + } + + /* -------------------------------------------- */ + + /** + * Initial drawing of the TokenIcon. + * @returns {Promise} + */ + async draw() { + if ( this.destroyed ) return this; + this.texture = this.texture ?? await loadTexture(this.textureSrc); + this.iconSprite.texture = this.texture; + return this.refresh(); + } + + /* -------------------------------------------- */ + + /** + * Incremental refresh for TokenIcon appearance. + * @returns {TokenIcon} + */ + refresh() { + return this; + } +} diff --git a/module/data/item/fields/summons-field.mjs b/module/data/item/fields/summons-field.mjs index 66e57198c8..622cd4ccb8 100644 --- a/module/data/item/fields/summons-field.mjs +++ b/module/data/item/fields/summons-field.mjs @@ -1,3 +1,4 @@ +import TokenPlacement from "../../../canvas/token-placement.mjs"; import { FormulaField } from "../../fields.mjs"; const { @@ -355,24 +356,15 @@ export class SummonsData extends foundry.abstract.DataModel { /* -------------------------------------------- */ - /** - * Data for token placement on the scene. - * - * @typedef {object} PlacementData - * @property {number} x - * @property {number} y - * @property {number} rotation - */ - /** * Determine where the summons should be placed on the scene. * @param {TokenDocument5e} token Token to be placed. * @param {SummonsProfile} profile Profile used for summoning. - * @returns {PlacementData[]} + * @returns {Promise} */ - async getPlacement(token, profile) { - // TODO: Query use for placement - return [{x: 1000, y: 1000, rotation: 0}]; + getPlacement(token, profile) { + const placement = new TokenPlacement({ tokens: [token] }); + return placement.place(); } /* -------------------------------------------- */ From ad057a7821108b7b36b8080c4081122f35ddfe6e Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Wed, 20 Mar 2024 15:42:26 -0700 Subject: [PATCH 2/2] [#893] Swap to using token previews directly --- module/canvas/token-placement.mjs | 328 ++++++---------------- module/data/item/fields/summons-field.mjs | 5 +- 2 files changed, 88 insertions(+), 245 deletions(-) diff --git a/module/canvas/token-placement.mjs b/module/canvas/token-placement.mjs index 581c22f8d5..6914a5ecff 100644 --- a/module/canvas/token-placement.mjs +++ b/module/canvas/token-placement.mjs @@ -33,33 +33,6 @@ export default class TokenPlacement { */ config; - /* -------------------------------------------- */ - /* Placement */ - /* -------------------------------------------- */ - - /** - * Perform the placement, asking player guidance when necessary. - * @returns {PlacementData[]} - */ - async place() { - const template = TokenPlacementTemplate.create(this.config); - const placement = await template.place(); - return placement; - } -} - - -/** - * A MeasuredTemplate used to visualize token placement in the scene. - */ -export class TokenPlacementTemplate extends MeasuredTemplate { - - /** - * Configuration information for this placement. - * @type {TokenPlacementConfiguration} - */ - config; - /* -------------------------------------------- */ /** @@ -71,166 +44,104 @@ export class TokenPlacementTemplate extends MeasuredTemplate { /* -------------------------------------------- */ /** - * The initially active CanvasLayer to re-activate after the workflow is complete. - * @type {CanvasLayer} + * Track the timestamp when the last mouse move event was captured. + * @type {number} */ - #initialLayer; + #moveTime = 0; + + /* -------------------------------------------- */ /** * Placements that have been generated. * @type {PlacementData[]} */ - placements; + #placements; /* -------------------------------------------- */ /** - * Track the timestamp when the last mouse move event was captured. - * @type {number} + * Preview tokens. Should match 1-to-1 with placements. + * @type {Token[]} */ - #moveTime = 0; + #previews; /* -------------------------------------------- */ /** - * Determine whether the throttling period has passed. Set to `true` to reset period. + * Is the system currently being throttled to the next animation frame? * @type {boolean} */ - get throttle() { - return Date.now() - this.#moveTime <= 20; - } - - set throttle(value) { - this.#moveTime = Date.now(); - } + #throttle = false; + /* -------------------------------------------- */ + /* Placement */ /* -------------------------------------------- */ /** - * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance - * @param {TokenPlacementConfiguration} config Configuration data for the token placement. - * @param {object} [options={}] Options to modify the created template. - * @returns {TokenPlacementTemplate} The template object. + * Perform the placement, asking player guidance when necessary. + * @param {TokenPlacementConfiguration} config + * @returns {Promise} */ - static create(config, options={}) { - const templateData = foundry.utils.mergeObject({ - t: "ray", user: game.user.id, distance: config.tokens[0].width, width: config.tokens[0].height - }, options); - - const cls = getDocumentClass("MeasuredTemplate"); - const template = new cls(templateData, {parent: canvas.scene}); - const object = new this(template); - object.config = config; - object.placements = [{x: 0, y: 0, rotation: 0}]; - - return object; + static place(config) { + const placement = new this(config); + return placement.place(); } - /* -------------------------------------------- */ - /** - * Creates a preview of the token placement template. - * @returns {Promise} A promise that resolves with the final token position if completed. + * Perform the placement, asking player guidance when necessary. + * @returns {Promise} */ - place() { - const initialLayer = canvas.activeLayer; - - // Draw the template and switch to the template layer - this.draw(); - this.layer.activate(); - this.layer.preview.addChild(this); - - // Activate interactivity - return this.activatePreviewListeners(initialLayer); - } - - /* -------------------------------------------- */ - /* Drawing */ - /* -------------------------------------------- */ - - /** @override */ - async _draw() { - - // Load Fill Texture - if ( this.document.texture ) await loadTexture(this.document.texture, {fallback: "icons/svg/hazard.svg"}); - this.texture = null; - - // Template Shape - this.template = this.addChild(new PIXI.Graphics()); - - // Token Icon - this.controlIcon = this.addChild(this.#createTokenIcon()); - await this.controlIcon.draw(); - - // Ruler Text - this.ruler = this.addChild(this.#drawRulerText()); - - // Enable highlighting for this template - canvas.grid.addHighlightLayer(this.highlightId); + async place() { + this.#createPreviews(); + try { + return await this.#activatePreviewListeners(); + } finally { + this.#destroyPreviews(); + } } /* -------------------------------------------- */ /** - * Draw the ControlIcon for the MeasuredTemplate - * @returns {ControlIcon} + * Create token previews based on the prototype tokens in config. */ - #createTokenIcon() { - let icon = new TokenIcon({ - texture: this.config.tokens[0].randomImg ? this.config.tokens[0].actor.img : this.config.tokens[0].texture.src, - angle: this.document.direction, - scale: { x: this.config.tokens[0].texture.scaleX, y: this.config.tokens[0].texture.scaleY }, - width: this.config.tokens[0].width * canvas.dimensions.size, - height: this.config.tokens[0].height * canvas.dimensions.size - }); - icon.x -= this.config.tokens[0].width * 0.5; - icon.y -= this.config.tokens[0].height * 0.5; - return icon; + #createPreviews() { + this.#placements = []; + this.#previews = []; + for ( const prototypeToken of this.config.tokens ) { + const tokenData = prototypeToken.toObject(); + if ( tokenData.randomImg ) tokenData.texture.src = prototypeToken.actor.img; + const cls = getDocumentClass("Token"); + const doc = new cls(tokenData, { parent: canvas.scene }); + this.#placements.push({ x: 0, y: 0, rotation: 0 }); + this.#previews.push(doc); + doc.object.draw(); + } } /* -------------------------------------------- */ /** - * Draw the Text label used for the MeasuredTemplate - * @returns {PreciseText} + * Clear any previews from the scene. */ - #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; + #destroyPreviews() { + this.#previews.forEach(p => p.object.destroy()); } /* -------------------------------------------- */ - /** @inheritDoc */ - _applyRenderFlags(flags) { - this.controlIcon.angle = this.document.direction; - super._applyRenderFlags(flags); - - // Hide unnecessary elements - const highlightLayer = canvas.grid.getHighlightLayer(this.highlightId); - highlightLayer.visible = false; - this.ruler.visible = false; - this.template.visible = false; - } - - /* -------------------------------------------- */ - /* Event Handlers */ - /* -------------------------------------------- */ - /** * Pixel offset to ensure snapping occurs in middle of grid space. - * @type {{x: number, y: number}} + * @param {PrototypeToken} token Token for which to calculate the adjustment. + * @returns {{x: number, y: number}} */ - get snapAdjustment() { + #getSnapAdjustment(token) { const size = canvas.dimensions.size; switch ( canvas.grid.type ) { case CONST.GRID_TYPES.SQUARE: return { - x: this.config.tokens[0].width % 2 === 0 ? Math.round(size * 0.5) : 0, - y: this.config.tokens[0].height % 2 === 0 ? Math.round(size * 0.5) : 0 + x: token.width % 2 === 0 ? Math.round(size * 0.5) : 0, + y: token.height % 2 === 0 ? Math.round(size * 0.5) : 0 }; default: return { x: 0, y: 0 }; @@ -238,34 +149,22 @@ export class TokenPlacementTemplate extends MeasuredTemplate { } /* -------------------------------------------- */ - - /** - * Interval for snapping token to the grid. - * @type {number} - */ - get snapInterval() { - // TODO: Figure out proper snapping sized based on token size - return canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ? 0 - : Math.max(this.config.tokens[0].width, this.config.tokens[0].height) > 0.5 ? 1 : 2; - } - + /* Event Handlers */ /* -------------------------------------------- */ /** - * Activate listeners for the template preview - * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete - * @returns {Promise} A promise that resolves with the final measured template if created. + * Activate listeners for the placement preview. + * @returns {Promise} A promise that resolves with the final placement if created. */ - activatePreviewListeners(initialLayer) { + #activatePreviewListeners() { return new Promise((resolve, reject) => { - this.#initialLayer = initialLayer; this.#events = { - cancel: this._onCancelPlacement.bind(this), - confirm: this._onConfirmPlacement.bind(this), - move: this._onMovePlacement.bind(this), + cancel: this.#onCancelPlacement.bind(this), + confirm: this.#onConfirmPlacement.bind(this), + move: this.#onMovePlacement.bind(this), resolve, reject, - rotate: this._onRotatePlacement.bind(this) + rotate: this.#onRotatePlacement.bind(this) }; // Activate listeners @@ -279,51 +178,56 @@ export class TokenPlacementTemplate extends MeasuredTemplate { /* -------------------------------------------- */ /** - * Shared code for when template placement ends by being confirmed or canceled. + * Shared code for when token placement ends by being confirmed or canceled. * @param {Event} event Triggering event that ended the placement. */ - async _finishPlacement(event) { - this.layer._onDragLeftCancel(event); + async #finishPlacement(event) { + canvas.tokens._onDragLeftCancel(event); canvas.stage.off("mousemove", this.#events.move); canvas.stage.off("mousedown", this.#events.confirm); canvas.app.view.oncontextmenu = null; canvas.app.view.onwheel = null; - this.#initialLayer.activate(); } /* -------------------------------------------- */ /** - * Move the template preview when the mouse moves. + * Move the token preview when the mouse moves. * @param {Event} event Triggering mouse event. */ - _onMovePlacement(event) { + #onMovePlacement(event) { event.stopPropagation(); - if ( this.throttle ) return; - const adjustment = this.snapAdjustment; - const point = event.data.getLocalPosition(this.layer); + if ( this.#throttle ) return; + this.#throttle = true; + const preview = this.#previews[0]; + const adjustment = this.#getSnapAdjustment(preview); + const point = event.data.getLocalPosition(canvas.tokens); const center = canvas.grid.getCenter(point.x - adjustment.x, point.y - adjustment.y); - this.document.updateSource({ x: center[0] + adjustment.x, y: center[1] + adjustment.y }); - this.placements[0].x = this.document.x - Math.round((this.config.tokens[0].width * canvas.dimensions.size) / 2); - this.placements[0].y = this.document.y - Math.round((this.config.tokens[0].height * canvas.dimensions.size) / 2); - this.refresh(); - this.throttle = true; + preview.updateSource({ + x: center[0] + adjustment.x - Math.round((this.config.tokens[0].width * canvas.dimensions.size) / 2), + y: center[1] + adjustment.y - Math.round((this.config.tokens[0].height * canvas.dimensions.size) / 2) + }); + this.#placements[0].x = preview.x; + this.#placements[0].y = preview.y; + preview.object.refresh(); + requestAnimationFrame(() => this.#throttle = false); } /* -------------------------------------------- */ /** - * Rotate the template preview by 3˚ increments when the mouse wheel is rotated. + * Rotate the token preview by 3˚ increments when the mouse wheel is rotated. * @param {Event} event Triggering mouse event. */ - _onRotatePlacement(event) { + #onRotatePlacement(event) { if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window event.stopPropagation(); const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15; const snap = event.shiftKey ? delta : 5; - this.placements[0].rotation += snap * Math.sign(event.deltaY); - this.controlIcon.icon.angle = this.placements[0].rotation; - this.refresh(); + const preview = this.#previews[0]; + this.#placements[0].rotation += snap * Math.sign(event.deltaY); + preview.updateSource({ rotation: this.#placements[0].rotation }); + preview.object.refresh(); } /* -------------------------------------------- */ @@ -332,9 +236,9 @@ export class TokenPlacementTemplate extends MeasuredTemplate { * Confirm placement when the left mouse button is clicked. * @param {Event} event Triggering mouse event. */ - async _onConfirmPlacement(event) { - await this._finishPlacement(event); - this.#events.resolve(this.placements); + async #onConfirmPlacement(event) { + await this.#finishPlacement(event); + this.#events.resolve(this.#placements); } /* -------------------------------------------- */ @@ -343,68 +247,8 @@ export class TokenPlacementTemplate extends MeasuredTemplate { * Cancel placement when the right mouse button is clicked. * @param {Event} event Triggering mouse event. */ - async _onCancelPlacement(event) { - await this._finishPlacement(event); + async #onCancelPlacement(event) { + await this.#finishPlacement(event); this.#events.reject(); } } - -/** - * A PIXI element for rendering a token preview. - */ -class TokenIcon extends PIXI.Container { - constructor({texture, width, height, angle, scale}={}, ...args) { - super(...args); - const size = Math.min(width, height); - this.textureSrc = texture; - this.width = this.height = size; - this.pivot.set(width * 0.5, height * 0.5); - - this.eventMode = "static"; - this.interactiveChildren = false; - - this.bg = this.addChild(new PIXI.Graphics()); - this.bg.clear().lineStyle(2, 0x000000, 1.0).drawRoundedRect(0, 0, width, height, 5).endFill(); - - const halfSize = Math.round(size * 0.5); - this.icon = this.addChild(new PIXI.Container()); - this.icon.width = size; - this.icon.height = size; - this.icon.pivot.set(halfSize, halfSize); - this.icon.x += halfSize; - this.icon.y += halfSize; - if ( width < height ) this.icon.y += Math.round((height - width) / 2); - else if ( height < width ) this.icon.x += Math.round((width - height) / 2); - - this.iconSprite = this.icon.addChild(new PIXI.Sprite()); - this.iconSprite.width = size * scale.x; - this.iconSprite.height = size * scale.y; - this.iconSprite.x -= (scale.x - 1) * halfSize; - this.iconSprite.y -= (scale.y - 1) * halfSize; - - this.draw(); - } - - /* -------------------------------------------- */ - - /** - * Initial drawing of the TokenIcon. - * @returns {Promise} - */ - async draw() { - if ( this.destroyed ) return this; - this.texture = this.texture ?? await loadTexture(this.textureSrc); - this.iconSprite.texture = this.texture; - return this.refresh(); - } - - /* -------------------------------------------- */ - - /** - * Incremental refresh for TokenIcon appearance. - * @returns {TokenIcon} - */ - refresh() { - return this; - } -} diff --git a/module/data/item/fields/summons-field.mjs b/module/data/item/fields/summons-field.mjs index 622cd4ccb8..5708ce27fe 100644 --- a/module/data/item/fields/summons-field.mjs +++ b/module/data/item/fields/summons-field.mjs @@ -358,13 +358,12 @@ export class SummonsData extends foundry.abstract.DataModel { /** * Determine where the summons should be placed on the scene. - * @param {TokenDocument5e} token Token to be placed. + * @param {PrototypeToken} token Token to be placed. * @param {SummonsProfile} profile Profile used for summoning. * @returns {Promise} */ getPlacement(token, profile) { - const placement = new TokenPlacement({ tokens: [token] }); - return placement.place(); + return TokenPlacement.place({ tokens: [token] }); } /* -------------------------------------------- */