/** * A basic PCO sprite mesh which is handling occlusion and depth. * @extends {SpriteMesh} * @mixes PrimaryOccludableObjectMixin * @mixes PrimaryCanvasObjectMixin * * @property {PrimaryBaseSamplerShader} shader The shader bound to this mesh. * * @param {object} [options] The constructor options. * @param {PIXI.Texture} [options.texture] Texture passed to the SpriteMesh. * @param {typeof PrimaryBaseSamplerShader} [options.shaderClass] The shader class used to render this sprite. * @param {string|null} [options.name] The name of this sprite. * @param {*} [options.object] Any object that owns this sprite. */ class PrimarySpriteMesh extends PrimaryOccludableObjectMixin(SpriteMesh) { constructor(options, shaderClass) { let texture; if ( options instanceof PIXI.Texture ) { texture = options; options = {}; } else if ( options instanceof Object ) { texture = options.texture; shaderClass = options.shaderClass; } else { options = {}; } shaderClass ??= PrimaryBaseSamplerShader; if ( !foundry.utils.isSubclass(shaderClass, PrimaryBaseSamplerShader) ) { throw new Error(`${shaderClass.name} in not a subclass of PrimaryBaseSamplerShader`); } super(texture, shaderClass); this.name = options.name ?? null; this.object = options.object ?? null; } /* -------------------------------------------- */ /** * A temporary point used by this class. * @type {PIXI.Point} */ static #TEMP_POINT = new PIXI.Point(); /* -------------------------------------------- */ /** * The texture alpha data. * @type {TextureAlphaData|null} * @protected */ _textureAlphaData = null; /* -------------------------------------------- */ /** * The texture alpha threshold used for point containment tests. * If set to a value larger than 0, the texture alpha data is * extracted from the texture at 25% resolution. * @type {number} */ textureAlphaThreshold = 0; /* -------------------------------------------- */ /* PIXI Events */ /* -------------------------------------------- */ /** @inheritDoc */ _onTextureUpdate() { super._onTextureUpdate(); this._textureAlphaData = null; this._canvasBoundsID++; } /* -------------------------------------------- */ /* Helper Methods */ /* -------------------------------------------- */ /** @inheritdoc */ setShaderClass(shaderClass) { if ( !foundry.utils.isSubclass(shaderClass, PrimaryBaseSamplerShader) ) { throw new Error(`${shaderClass.name} in not a subclass of PrimaryBaseSamplerShader`); } super.setShaderClass(shaderClass); } /* -------------------------------------------- */ /** * An all-in-one helper method: Resizing the PCO according to desired dimensions and options. * This helper computes the width and height based on the following factors: * * - The ratio of texture width and base width. * - The ratio of texture height and base height. * * Additionally, It takes into account the desired fit options: * * - (default) "fill" computes the exact width and height ratio. * - "cover" takes the maximum ratio of width and height and applies it to both. * - "contain" takes the minimum ratio of width and height and applies it to both. * - "width" applies the width ratio to both width and height. * - "height" applies the height ratio to both width and height. * * You can also apply optional scaleX and scaleY options to both width and height. The scale is applied after fitting. * * **Important**: By using this helper, you don't need to set the height, width, and scale properties of the DisplayObject. * * **Note**: This is a helper method. Alternatively, you could assign properties as you would with a PIXI DisplayObject. * * @param {number} baseWidth The base width used for computations. * @param {number} baseHeight The base height used for computations. * @param {object} [options] The options. * @param {"fill"|"cover"|"contain"|"width"|"height"} [options.fit="fill"] The fit type. * @param {number} [options.scaleX=1] The scale on X axis. * @param {number} [options.scaleY=1] The scale on Y axis. */ resize(baseWidth, baseHeight, {fit="fill", scaleX=1, scaleY=1}={}) { if ( !((baseWidth >= 0) && (baseHeight >= 0)) ) { throw new Error(`Invalid baseWidth/baseHeight passed to ${this.constructor.name}#resize.`); } const {width: textureWidth, height: textureHeight} = this._texture; let sx; let sy; switch ( fit ) { case "fill": sx = baseWidth / textureWidth; sy = baseHeight / textureHeight; break; case "cover": sx = sy = Math.max(baseWidth / textureWidth, baseHeight / textureHeight); break; case "contain": sx = sy = Math.min(baseWidth / textureWidth, baseHeight / textureHeight); break; case "width": sx = sy = baseWidth / textureWidth; break; case "height": sx = sy = baseHeight / textureHeight; break; default: throw new Error(`Invalid fill type passed to ${this.constructor.name}#resize (fit=${fit}).`); } sx *= scaleX; sy *= scaleY; this.scale.set(sx, sy); this._width = Math.abs(sx * textureWidth); this._height = Math.abs(sy * textureHeight); } /* -------------------------------------------- */ /* Methods */ /* -------------------------------------------- */ /** @inheritdoc */ _updateBatchData() { super._updateBatchData(); const batchData = this._batchData; batchData.elevation = this.elevation; batchData.textureAlphaThreshold = this.textureAlphaThreshold; batchData.unoccludedAlpha = this.unoccludedAlpha; batchData.occludedAlpha = this.occludedAlpha; const occlusionState = this._occlusionState; batchData.fadeOcclusion = occlusionState.fade; batchData.radialOcclusion = occlusionState.radial; batchData.visionOcclusion = occlusionState.vision; batchData.restrictionState = this._restrictionState; } /* -------------------------------------------- */ /** @override */ _calculateCanvasBounds() { if ( !this._texture ) return; const {width, height} = this._texture; let minX = 0; let minY = 0; let maxX = width; let maxY = height; const alphaData = this._textureAlphaData; if ( alphaData ) { const scaleX = width / alphaData.width; const scaleY = height / alphaData.height; minX = alphaData.minX * scaleX; minY = alphaData.minY * scaleY; maxX = alphaData.maxX * scaleX; maxY = alphaData.maxY * scaleY; } let {x: anchorX, y: anchorY} = this.anchor; anchorX *= width; anchorY *= height; minX -= anchorX; minY -= anchorY; maxX -= anchorX; maxY -= anchorY; this._canvasBounds.addFrameMatrix(this.canvasTransform, minX, minY, maxX, maxY); } /* -------------------------------------------- */ /** * Is the given point in canvas space contained in this object? * @param {PIXI.IPointData} point The point in canvas space * @param {number} [textureAlphaThreshold] The minimum texture alpha required for containment * @returns {boolean} */ containsCanvasPoint(point, textureAlphaThreshold=this.textureAlphaThreshold) { if ( textureAlphaThreshold > 1 ) return false; if ( !this.canvasBounds.contains(point.x, point.y) ) return false; point = this.canvasTransform.applyInverse(point, PrimarySpriteMesh.#TEMP_POINT); return this.#containsLocalPoint(point, textureAlphaThreshold); } /* -------------------------------------------- */ /** * Is the given point in world space contained in this object? * @param {PIXI.IPointData} point The point in world space * @param {number} [textureAlphaThreshold] The minimum texture alpha required for containment * @returns {boolean} */ containsPoint(point, textureAlphaThreshold=this.textureAlphaThreshold) { if ( textureAlphaThreshold > 1 ) return false; point = this.worldTransform.applyInverse(point, PrimarySpriteMesh.#TEMP_POINT); return this.#containsLocalPoint(point, textureAlphaThreshold); } /* -------------------------------------------- */ /** * Is the given point in local space contained in this object? * @param {PIXI.IPointData} point The point in local space * @param {number} textureAlphaThreshold The minimum texture alpha required for containment * @returns {boolean} */ #containsLocalPoint(point, textureAlphaThreshold) { const {width, height} = this._texture; const {x: anchorX, y: anchorY} = this.anchor; let {x, y} = point; x += (width * anchorX); y += (height * anchorY); if ( textureAlphaThreshold > 0 ) return this.#getTextureAlpha(x, y) >= textureAlphaThreshold; return (x >= 0) && (x < width) && (y >= 0) && (y < height); } /* -------------------------------------------- */ /** * Get alpha value of texture at the given texture coordinates. * @param {number} x The x-coordinate * @param {number} y The y-coordinate * @returns {number} The alpha value (0-1) */ #getTextureAlpha(x, y) { if ( !this._texture ) return 0; if ( !this._textureAlphaData ) { this._textureAlphaData = TextureLoader.getTextureAlphaData(this._texture, 0.25); this._canvasBoundsID++; } // Transform the texture coordinates const {width, height} = this._texture; const alphaData = this._textureAlphaData; x *= (alphaData.width / width); y *= (alphaData.height / height); // First test against the bounding box const {minX, minY, maxX, maxY} = alphaData; if ( (x < minX) || (x >= maxX) || (y < minY) || (y >= maxY) ) return 0; // Get the alpha at the local coordinates return alphaData.data[((maxX - minX) * ((y | 0) - minY)) + ((x | 0) - minX)] / 255; } /* -------------------------------------------- */ /* Rendering Methods */ /* -------------------------------------------- */ /** @override */ renderDepthData(renderer) { if ( !this.shouldRenderDepth || !this.visible || !this.renderable ) return; const shader = this._shader; const blendMode = this.blendMode; this.blendMode = PIXI.BLEND_MODES.MAX_COLOR; this._shader = shader.depthShader; if ( this.cullable ) this._renderWithCulling(renderer); else this._render(renderer); this._shader = shader; this.blendMode = blendMode; } /* -------------------------------------------- */ /** * Render the sprite with ERASE blending. * Note: The sprite must not have visible/renderable children. * @param {PIXI.Renderer} renderer The renderer * @internal */ _renderVoid(renderer) { if ( !this.visible || (this.worldAlpha <= 0) || !this.renderable ) return; // Delegate to PrimarySpriteMesh#renderVoidAdvanced if the sprite has filter or mask if ( this._mask || this.filters?.length ) this.#renderVoidAdvanced(renderer); else { // Set the blend mode to ERASE before rendering const originalBlendMode = this.blendMode; this.blendMode = PIXI.BLEND_MODES.ERASE; // Render the sprite but not its children if ( this.cullable ) this._renderWithCulling(renderer); else this._render(renderer); // Restore the original blend mode after rendering this.blendMode = originalBlendMode; } } /* -------------------------------------------- */ /** * Render the sprite that has a filter or a mask with ERASE blending. * Note: The sprite must not have visible/renderable children. * @param {PIXI.Renderer} renderer The renderer */ #renderVoidAdvanced(renderer) { // Same code as in PIXI.Container#renderAdvanced const filters = this.filters; const mask = this._mask; if ( filters ) { this._enabledFilters ||= []; this._enabledFilters.length = 0; for ( let i = 0; i < filters.length; i++ ) { if ( filters[i].enabled ) this._enabledFilters.push(filters[i]); } } const flush = (filters && this._enabledFilters.length) || (mask && (!mask.isMaskData || (mask.enabled && (mask.autoDetect || mask.type !== MASK_TYPES.NONE)))); if ( flush ) renderer.batch.flush(); if ( filters && this._enabledFilters.length ) renderer.filter.push(this, this._enabledFilters); if ( mask ) renderer.mask.push(this, mask); // Set the blend mode to ERASE before rendering let filter; let originalBlendMode; const filterState = renderer.filter.defaultFilterStack.at(-1); if ( filterState.target === this ) { filter = filterState.filters.at(-1); originalBlendMode = filter.blendMode; filter.blendMode = PIXI.BLEND_MODES.ERASE; } else { originalBlendMode = this.blendMode; this.blendMode = PIXI.BLEND_MODES.ERASE; } // Same code as in PIXI.Container#renderAdvanced without the part that renders children if ( this.cullable ) this._renderWithCulling(renderer); else this._render(renderer); if ( flush ) renderer.batch.flush(); if ( mask ) renderer.mask.pop(this); if ( filters && this._enabledFilters.length ) renderer.filter.pop(); // Restore the original blend mode after rendering if ( filter ) filter.blendMode = originalBlendMode; else this.blendMode = originalBlendMode; } /* -------------------------------------------- */ /* Deprecations and Compatibility */ /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ getPixelAlpha(x, y) { const msg = `${this.constructor.name}#getPixelAlpha is deprecated without replacement.`; foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14}); if ( !this._textureAlphaData ) return null; if ( !this.canvasBounds.contains(x, y) ) return -1; const point = PrimarySpriteMesh.#TEMP_POINT.set(x, y); this.canvasTransform.applyInverse(point, point); const {width, height} = this._texture; const {x: anchorX, y: anchorY} = this.anchor; x = point.x + (width * anchorX); y = point.y + (height * anchorY); return this.#getTextureAlpha(x, y) * 255; } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ _getAlphaBounds() { const msg = `${this.constructor.name}#_getAlphaBounds is deprecated without replacement.`; foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14}); const m = this._textureAlphaData; const r = this.rotation; return PIXI.Rectangle.fromRotation(m.minX, m.minY, m.maxX - m.minX, m.maxY - m.minY, r).normalize(); } /* -------------------------------------------- */ /** * @deprecated since v12 * @ignore */ _getTextureCoordinate(testX, testY) { const msg = `${this.constructor.name}#_getTextureCoordinate is deprecated without replacement.`; foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14}); const point = {x: testX, y: testY}; let {x, y} = this.canvasTransform.applyInverse(point, point); point.x = ((x / this._texture.width) + this.anchor.x) * this._textureAlphaData.width; point.y = ((y / this._texture.height) + this.anchor.y) * this._textureAlphaData.height; return point; } }