Files
Foundry-VTT-Docker/resources/app/client/pixi/placeables/primary-canvas-objects/primary-canvas-object.js
2025-01-04 00:34:03 +01:00

456 lines
13 KiB
JavaScript

/**
* A mixin which decorates a DisplayObject with additional properties expected for rendering in the PrimaryCanvasGroup.
* @category - Mixins
* @param {typeof PIXI.DisplayObject} DisplayObject The parent DisplayObject class being mixed
* @returns {typeof PrimaryCanvasObject} A DisplayObject subclass mixed with PrimaryCanvasObject features
* @mixin
*/
function PrimaryCanvasObjectMixin(DisplayObject) {
/**
* A display object rendered in the PrimaryCanvasGroup.
* @param {...*} args The arguments passed to the base class constructor
*/
return class PrimaryCanvasObject extends CanvasTransformMixin(DisplayObject) {
constructor(...args) {
super(...args);
// Activate culling and initialize handlers
this.cullable = true;
this.on("added", this._onAdded);
this.on("removed", this._onRemoved);
}
/**
* An optional reference to the object that owns this PCO.
* This property does not affect the behavior of the PCO itself.
* @type {*}
* @default null
*/
object = null;
/**
* The entry in the quadtree.
* @type {QuadtreeObject|null}
*/
#quadtreeEntry = null;
/**
* Update the quadtree entry?
* @type {boolean}
*/
#quadtreeDirty = false;
/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */
/**
* The elevation of this object.
* @type {number}
*/
get elevation() {
return this.#elevation;
}
set elevation(value) {
if ( (typeof value !== "number") || Number.isNaN(value) ) {
throw new Error("PrimaryCanvasObject#elevation must be a numeric value.");
}
if ( value === this.#elevation ) return;
this.#elevation = value;
if ( this.parent ) {
this.parent.sortDirty = true;
if ( this.shouldRenderDepth ) canvas.masks.depth._elevationDirty = true;
}
}
#elevation = 0;
/* -------------------------------------------- */
/**
* A key which resolves ties amongst objects at the same elevation within the same layer.
* @type {number}
*/
get sort() {
return this.#sort;
}
set sort(value) {
if ( (typeof value !== "number") || Number.isNaN(value) ) {
throw new Error("PrimaryCanvasObject#sort must be a numeric value.");
}
if ( value === this.#sort ) return;
this.#sort = value;
if ( this.parent ) this.parent.sortDirty = true;
}
#sort = 0;
/* -------------------------------------------- */
/**
* A key which resolves ties amongst objects at the same elevation of different layers.
* @type {number}
*/
get sortLayer() {
return this.#sortLayer;
}
set sortLayer(value) {
if ( (typeof value !== "number") || Number.isNaN(value) ) {
throw new Error("PrimaryCanvasObject#sortLayer must be a numeric value.");
}
if ( value === this.#sortLayer ) return;
this.#sortLayer = value;
if ( this.parent ) this.parent.sortDirty = true;
}
#sortLayer = 0;
/* -------------------------------------------- */
/**
* A key which resolves ties amongst objects at the same elevation within the same layer and same sort.
* @type {number}
*/
get zIndex() {
return this._zIndex;
}
set zIndex(value) {
if ( (typeof value !== "number") || Number.isNaN(value) ) {
throw new Error("PrimaryCanvasObject#zIndex must be a numeric value.");
}
if ( value === this._zIndex ) return;
this._zIndex = value;
if ( this.parent ) this.parent.sortDirty = true;
}
/* -------------------------------------------- */
/* PIXI Events */
/* -------------------------------------------- */
/**
* Event fired when this display object is added to a parent.
* @param {PIXI.Container} parent The new parent container.
* @protected
*/
_onAdded(parent) {
if ( parent !== canvas.primary ) {
throw new Error("PrimaryCanvasObject instances may only be direct children of the PrimaryCanvasGroup");
}
}
/* -------------------------------------------- */
/**
* Event fired when this display object is removed from its parent.
* @param {PIXI.Container} parent Parent from which the PCO is removed.
* @protected
*/
_onRemoved(parent) {
this.#updateQuadtree(true);
}
/* -------------------------------------------- */
/* Canvas Transform & Quadtree */
/* -------------------------------------------- */
/** @inheritdoc */
updateCanvasTransform() {
super.updateCanvasTransform();
this.#updateQuadtree();
this.#updateDepth();
}
/* -------------------------------------------- */
/** @inheritdoc */
_onCanvasBoundsUpdate() {
super._onCanvasBoundsUpdate();
this.#quadtreeDirty = true;
}
/* -------------------------------------------- */
/**
* Update the quadtree.
* @param {boolean} [remove=false] Remove the quadtree entry?
*/
#updateQuadtree(remove=false) {
if ( !this.#quadtreeDirty && !remove ) return;
this.#quadtreeDirty = false;
if ( !remove && (this.canvasBounds.width > 0) && (this.canvasBounds.height > 0) ) {
this.#quadtreeEntry ??= {r: this.canvasBounds, t: this};
canvas.primary.quadtree.update(this.#quadtreeEntry);
} else if ( this.#quadtreeEntry ) {
this.#quadtreeEntry = null;
canvas.primary.quadtree.remove(this);
}
}
/* -------------------------------------------- */
/* PCO Properties */
/* -------------------------------------------- */
/**
* Does this object render to the depth buffer?
* @type {boolean}
*/
get shouldRenderDepth() {
return this.#shouldRenderDepth;
}
/** @type {boolean} */
#shouldRenderDepth = false;
/* -------------------------------------------- */
/* Depth Rendering */
/* -------------------------------------------- */
/**
* Flag the depth as dirty if necessary.
*/
#updateDepth() {
const shouldRenderDepth = this._shouldRenderDepth();
if ( this.#shouldRenderDepth === shouldRenderDepth ) return;
this.#shouldRenderDepth = shouldRenderDepth;
canvas.masks.depth._elevationDirty = true;
}
/* -------------------------------------------- */
/**
* Does this object render to the depth buffer?
* @returns {boolean}
* @protected
*/
_shouldRenderDepth() {
return false;
}
/* -------------------------------------------- */
/**
* Render the depth of this object.
* @param {PIXI.Renderer} renderer
*/
renderDepthData(renderer) {}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v11
* @ignore
*/
renderOcclusion(renderer) {
const msg = "PrimaryCanvasObject#renderOcclusion is deprecated in favor of PrimaryCanvasObject#renderDepthData";
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
this.renderDepthData(renderer);
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
get document() {
foundry.utils.logCompatibilityWarning("PrimaryCanvasObject#document is deprecated.", {since: 12, until: 14});
if ( !(this.object instanceof PlaceableObject) ) return null;
return this.object.document || null;
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
updateBounds() {
const msg = "PrimaryCanvasObject#updateBounds is deprecated and has no effect.";
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
}
};
}
/**
* A mixin which decorates a DisplayObject with additional properties for canvas transforms and bounds.
* @category - Mixins
* @param {typeof PIXI.Container} DisplayObject The parent DisplayObject class being mixed
* @returns {typeof CanvasTransformMixin} A DisplayObject subclass mixed with CanvasTransformMixin features
* @mixin
*/
function CanvasTransformMixin(DisplayObject) {
return class CanvasTransformMixin extends DisplayObject {
constructor(...args) {
super(...args);
this.on("added", this.#resetCanvasTransformParentID);
this.on("removed", this.#resetCanvasTransformParentID);
}
/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */
/**
* The transform matrix from local space to canvas space.
* @type {PIXI.Matrix}
*/
canvasTransform = new PIXI.Matrix();
/* -------------------------------------------- */
/**
* The update ID of canvas transform matrix.
* @type {number}
* @internal
*/
_canvasTransformID = -1;
/* -------------------------------------------- */
/**
* The update ID of the local transform of this object.
* @type {number}
*/
#canvasTransformLocalID = -1;
/* -------------------------------------------- */
/**
* The update ID of the canvas transform of the parent.
* @type {number}
*/
#canvasTransformParentID = -1;
/* -------------------------------------------- */
/**
* The canvas bounds of this object.
* @type {PIXI.Rectangle}
*/
canvasBounds = new PIXI.Rectangle();
/* -------------------------------------------- */
/**
* The canvas bounds of this object.
* @type {PIXI.Bounds}
* @protected
*/
_canvasBounds = new PIXI.Bounds();
/* -------------------------------------------- */
/**
* The update ID of the canvas bounds.
* Increment to force recalculation.
* @type {number}
* @protected
*/
_canvasBoundsID = 0;
/* -------------------------------------------- */
/**
* Reset the parent ID of the canvas transform.
*/
#resetCanvasTransformParentID() {
this.#canvasTransformParentID = -1;
}
/* -------------------------------------------- */
/* Methods */
/* -------------------------------------------- */
/**
* Calculate the canvas bounds of this object.
* @protected
*/
_calculateCanvasBounds() {}
/* -------------------------------------------- */
/**
* Recalculate the canvas transform and bounds of this object and its children, if necessary.
*/
updateCanvasTransform() {
this.transform.updateLocalTransform();
// If the local transform or the parent canvas transform has changed,
// recalculate the canvas transform of this object
if ( (this.#canvasTransformLocalID !== this.transform._localID)
|| (this.#canvasTransformParentID !== (this.parent._canvasTransformID ?? 0)) ) {
this.#canvasTransformLocalID = this.transform._localID;
this.#canvasTransformParentID = this.parent._canvasTransformID ?? 0;
this._canvasTransformID++;
this.canvasTransform.copyFrom(this.transform.localTransform);
// Prepend the parent canvas transform matrix (if exists)
if ( this.parent.canvasTransform ) this.canvasTransform.prepend(this.parent.canvasTransform);
this._canvasBoundsID++;
this._onCanvasTransformUpdate();
}
// Recalculate the canvas bounds of this object if necessary
if ( this._canvasBounds.updateID !== this._canvasBoundsID ) {
this._canvasBounds.updateID = this._canvasBoundsID;
this._canvasBounds.clear();
this._calculateCanvasBounds();
// Set the width and height of the canvas bounds rectangle to 0
// if the bounds are empty. PIXI.Bounds#getRectangle does not
// change the rectangle passed to it if the bounds are empty:
// so we need to handle the empty case here.
if ( this._canvasBounds.isEmpty() ) {
this.canvasBounds.x = this.x;
this.canvasBounds.y = this.y;
this.canvasBounds.width = 0;
this.canvasBounds.height = 0;
}
// Update the canvas bounds rectangle
else this._canvasBounds.getRectangle(this.canvasBounds);
this._onCanvasBoundsUpdate();
}
// Recursively update child canvas transforms
const children = this.children;
for ( let i = 0, n = children.length; i < n; i++ ) {
children[i].updateCanvasTransform?.();
}
}
/* -------------------------------------------- */
/**
* Called when the canvas transform changed.
* @protected
*/
_onCanvasTransformUpdate() {}
/* -------------------------------------------- */
/**
* Called when the canvas bounds changed.
* @protected
*/
_onCanvasBoundsUpdate() {}
/* -------------------------------------------- */
/**
* Is the given point in canvas space contained in this object?
* @param {PIXI.IPointData} point The point in canvas space.
* @returns {boolean}
*/
containsCanvasPoint(point) {
return false;
}
};
}