import { Point } from '@pixi/math'; /** * Strategy how to search through stage tree for interactive objects * * @private * @class * @memberof PIXI.interaction */ export class TreeSearch { constructor() { this._tempPoint = new Point(); } /** * Recursive implementation for findHit * * @private * @param {PIXI.interaction.InteractionEvent} interactionEvent - event containing the point that * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @param {boolean} [interactive] - Whether the displayObject is interactive * @return {boolean} returns true if the displayObject hit the point */ recursiveFindHit(interactionEvent, displayObject, func, hitTest, interactive) { if (!displayObject || !displayObject.visible) { return false; } const point = interactionEvent.data.global; // Took a little while to rework this function correctly! But now it is done and nice and optimized! ^_^ // // This function will now loop through all objects and then only hit test the objects it HAS // to, not all of them. MUCH faster.. // An object will be hit test if the following is true: // // 1: It is interactive. // 2: It belongs to a parent that is interactive AND one of the parents children have not already been hit. // // As another little optimization once an interactive object has been hit we can carry on // through the scenegraph, but we know that there will be no more hits! So we can avoid extra hit tests // A final optimization is that an object is not hit test directly if a child has already been hit. interactive = displayObject.interactive || interactive; let hit = false; let interactiveParent = interactive; // Flag here can set to false if the event is outside the parents hitArea or mask let hitTestChildren = true; // If there is a hitArea, no need to test against anything else if the pointer is not within the hitArea // There is also no longer a need to hitTest children. if (displayObject.hitArea) { if (hitTest) { displayObject.worldTransform.applyInverse(point, this._tempPoint); if (!displayObject.hitArea.contains(this._tempPoint.x, this._tempPoint.y)) { hitTest = false; hitTestChildren = false; } else { hit = true; } } interactiveParent = false; } // If there is a mask, no need to hitTest against anything else if the pointer is not within the mask. // We still want to hitTestChildren, however, to ensure a mouseout can still be generated. // https://github.com/pixijs/pixi.js/issues/5135 else if (displayObject._mask) { if (hitTest) { if (!(displayObject._mask.containsPoint && displayObject._mask.containsPoint(point))) { hitTest = false; } } } // ** FREE TIP **! If an object is not interactive or has no buttons in it // (such as a game scene!) set interactiveChildren to false for that displayObject. // This will allow PixiJS to completely ignore and bypass checking the displayObjects children. if (hitTestChildren && displayObject.interactiveChildren && displayObject.children) { const children = displayObject.children; for (let i = children.length - 1; i >= 0; i--) { const child = children[i]; // time to get recursive.. if this function will return if something is hit.. const childHit = this.recursiveFindHit(interactionEvent, child, func, hitTest, interactiveParent); if (childHit) { // its a good idea to check if a child has lost its parent. // this means it has been removed whilst looping so its best if (!child.parent) { continue; } // we no longer need to hit test any more objects in this container as we we // now know the parent has been hit interactiveParent = false; // If the child is interactive , that means that the object hit was actually // interactive and not just the child of an interactive object. // This means we no longer need to hit test anything else. We still need to run // through all objects, but we don't need to perform any hit tests. if (childHit) { if (interactionEvent.target) { hitTest = false; } hit = true; } } } } // no point running this if the item is not interactive or does not have an interactive parent. if (interactive) { // if we are hit testing (as in we have no hit any objects yet) // We also don't need to worry about hit testing if once of the displayObjects children // has already been hit - but only if it was interactive, otherwise we need to keep // looking for an interactive child, just in case we hit one if (hitTest && !interactionEvent.target) { // already tested against hitArea if it is defined if (!displayObject.hitArea && displayObject.containsPoint) { if (displayObject.containsPoint(point)) { hit = true; } } } if (displayObject.interactive) { if (hit && !interactionEvent.target) { interactionEvent.target = displayObject; } if (func) { func(interactionEvent, displayObject, !!hit); } } } return hit; } /** * @private * * This function is provides a neat way of crawling through the scene graph and running a * specified function on all interactive objects it finds. It will also take care of hit * testing the interactive objects and passes the hit across in the function. * * @param {PIXI.interaction.InteractionEvent} interactionEvent - event containing the point that * is tested for collision * @param {PIXI.Container|PIXI.Sprite|PIXI.TilingSprite} displayObject - the displayObject * that will be hit test (recursively crawls its children) * @param {Function} [func] - the function that will be called on each interactive object. The * interactionEvent, displayObject and hit will be passed to the function * @param {boolean} [hitTest] - this indicates if the objects inside should be hit test against the point * @return {boolean} returns true if the displayObject hit the point */ findHit(interactionEvent, displayObject, func, hitTest) { this.recursiveFindHit(interactionEvent, displayObject, func, hitTest, false); } }