diff --git a/packages/interaction/src/InteractionManager.js b/packages/interaction/src/InteractionManager.js index 2b38ac2..43a076c 100644 --- a/packages/interaction/src/InteractionManager.js +++ b/packages/interaction/src/InteractionManager.js @@ -1,9 +1,9 @@ import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; -import { Point } from '@pixi/math'; import { DisplayObject } from '@pixi/display'; import { InteractionData } from './InteractionData'; import { InteractionEvent } from './InteractionEvent'; import { InteractionTrackingData } from './InteractionTrackingData'; +import { TreeSearch } from './TreeSearch'; import { EventEmitter } from '@pixi/utils'; import { interactiveTarget } from './interactiveTarget'; @@ -237,14 +237,6 @@ this.cursor = null; /** - * Internal cached let. - * - * @private - * @member {PIXI.Point} - */ - this._tempPoint = new Point(); - - /** * The current resolution / device pixel ratio. * * @member {number} @@ -261,6 +253,14 @@ this.delayedEvents = []; /** + * TreeSearch component that is used to hitTest stage tree + * + * @private + * @member {PIXI.interaction.TreeSearch} + */ + this.search = new TreeSearch(); + + /** * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * @@ -1010,176 +1010,38 @@ * 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 - * @param {boolean} [skipDelayed] - Whether to process delayed events before returning. This is - * used to avoid processing them too early during recursive calls. - * @return {boolean} returns true if the displayObject hit the point + * @param {boolean} [hitTest] - indicates whether we want to calculate hits + * or just iterate through all interactive objects */ - processInteractive(interactionEvent, displayObject, func, hitTest, interactive, skipDelayed) + processInteractive(interactionEvent, displayObject, func, hitTest) { - 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.processInteractive(interactionEvent, child, func, hitTest, interactiveParent, true); - - 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); - } - } - } + const hit = this.search.findHit(interactionEvent, displayObject, func, hitTest); const delayedEvents = this.delayedEvents; - if (delayedEvents.length && !skipDelayed) + if (!delayedEvents.length) { - // Reset the propagation hint, because we start deeper in the tree again. - interactionEvent.stopPropagationHint = false; + return hit; + } + // Reset the propagation hint, because we start deeper in the tree again. + interactionEvent.stopPropagationHint = false; - const delayedLen = delayedEvents.length; + const delayedLen = delayedEvents.length; - this.delayedEvents = []; + this.delayedEvents = []; - for (let i = 0; i < delayedLen; i++) + for (let i = 0; i < delayedLen; i++) + { + const { displayObject, eventString, eventData } = delayedEvents[i]; + + // When we reach the object we wanted to stop propagating at, + // set the propagation hint. + if (eventData.stopsPropagatingAt === displayObject) { - const { displayObject, eventString, eventData } = delayedEvents[i]; - - // When we reach the object we wanted to stop propagating at, - // set the propagation hint. - if (eventData.stopsPropagatingAt === displayObject) - { - eventData.stopPropagationHint = true; - } - - this.dispatchEvent(displayObject, eventString, eventData); + eventData.stopPropagationHint = true; } + + this.dispatchEvent(displayObject, eventString, eventData); } return hit; @@ -1885,6 +1747,6 @@ this.onPointerOver = null; - this._tempPoint = null; + this.search = null; } } diff --git a/packages/interaction/src/InteractionManager.js b/packages/interaction/src/InteractionManager.js index 2b38ac2..43a076c 100644 --- a/packages/interaction/src/InteractionManager.js +++ b/packages/interaction/src/InteractionManager.js @@ -1,9 +1,9 @@ import { Ticker, UPDATE_PRIORITY } from '@pixi/ticker'; -import { Point } from '@pixi/math'; import { DisplayObject } from '@pixi/display'; import { InteractionData } from './InteractionData'; import { InteractionEvent } from './InteractionEvent'; import { InteractionTrackingData } from './InteractionTrackingData'; +import { TreeSearch } from './TreeSearch'; import { EventEmitter } from '@pixi/utils'; import { interactiveTarget } from './interactiveTarget'; @@ -237,14 +237,6 @@ this.cursor = null; /** - * Internal cached let. - * - * @private - * @member {PIXI.Point} - */ - this._tempPoint = new Point(); - - /** * The current resolution / device pixel ratio. * * @member {number} @@ -261,6 +253,14 @@ this.delayedEvents = []; /** + * TreeSearch component that is used to hitTest stage tree + * + * @private + * @member {PIXI.interaction.TreeSearch} + */ + this.search = new TreeSearch(); + + /** * Fired when a pointer device button (usually a mouse left-button) is pressed on the display * object. * @@ -1010,176 +1010,38 @@ * 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 - * @param {boolean} [skipDelayed] - Whether to process delayed events before returning. This is - * used to avoid processing them too early during recursive calls. - * @return {boolean} returns true if the displayObject hit the point + * @param {boolean} [hitTest] - indicates whether we want to calculate hits + * or just iterate through all interactive objects */ - processInteractive(interactionEvent, displayObject, func, hitTest, interactive, skipDelayed) + processInteractive(interactionEvent, displayObject, func, hitTest) { - 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.processInteractive(interactionEvent, child, func, hitTest, interactiveParent, true); - - 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); - } - } - } + const hit = this.search.findHit(interactionEvent, displayObject, func, hitTest); const delayedEvents = this.delayedEvents; - if (delayedEvents.length && !skipDelayed) + if (!delayedEvents.length) { - // Reset the propagation hint, because we start deeper in the tree again. - interactionEvent.stopPropagationHint = false; + return hit; + } + // Reset the propagation hint, because we start deeper in the tree again. + interactionEvent.stopPropagationHint = false; - const delayedLen = delayedEvents.length; + const delayedLen = delayedEvents.length; - this.delayedEvents = []; + this.delayedEvents = []; - for (let i = 0; i < delayedLen; i++) + for (let i = 0; i < delayedLen; i++) + { + const { displayObject, eventString, eventData } = delayedEvents[i]; + + // When we reach the object we wanted to stop propagating at, + // set the propagation hint. + if (eventData.stopsPropagatingAt === displayObject) { - const { displayObject, eventString, eventData } = delayedEvents[i]; - - // When we reach the object we wanted to stop propagating at, - // set the propagation hint. - if (eventData.stopsPropagatingAt === displayObject) - { - eventData.stopPropagationHint = true; - } - - this.dispatchEvent(displayObject, eventString, eventData); + eventData.stopPropagationHint = true; } + + this.dispatchEvent(displayObject, eventString, eventData); } return hit; @@ -1885,6 +1747,6 @@ this.onPointerOver = null; - this._tempPoint = null; + this.search = null; } } diff --git a/packages/interaction/src/TreeSearch.js b/packages/interaction/src/TreeSearch.js new file mode 100644 index 0000000..6aa6575 --- /dev/null +++ b/packages/interaction/src/TreeSearch.js @@ -0,0 +1,194 @@ +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); + } +}