import { DisplayObjectWithCulling, AABB } from './types' export interface SimpleOptions { visible?: string dirtyTest?: boolean } const defaultSimpleOptions = { visible: 'visible', dirtyTest: false, } export interface SimpleStats { total: number visible: number culled: number } type DisplayObjectWithCullingArray = DisplayObjectWithCulling[] & { staticObject?: boolean } export class Simple { public options: SimpleOptions public dirtyTest: boolean protected lists: DisplayObjectWithCullingArray[] /** * Creates a simple cull * Note, options.dirtyTest defaults to false. Set to true for much better performance--this requires * additional work to ensure displayObject.dirty is set when objects change) * * @param {object} [options] * @param {string} [options.dirtyTest=false] - only update the AABB box for objects with object[options.dirtyTest]=true; this has a HUGE impact on performance */ constructor(options: SimpleOptions = {}) { options = { ...defaultSimpleOptions, ...options } this.dirtyTest = typeof options.dirtyTest !== 'undefined' ? options.dirtyTest : true this.lists = [[]] } /** * add an array of objects to be culled, eg: `simple.addList(container.children)` * @param {Array} array * @param {boolean} [staticObject] - set to true if the object's position/size does not change * @return {Array} array */ addList(array: DisplayObjectWithCullingArray, staticObject?: boolean): object[] { this.lists.push(array) if (staticObject) { array.staticObject = true } const length = array.length for (let i = 0; i < length; i++) { this.updateObject(array[i]) } return array } /** * remove an array added by addList() * @param {Array} array * @return {Array} array */ removeList(array: DisplayObjectWithCullingArray): DisplayObjectWithCullingArray { const index = this.lists.indexOf(array) if(index === -1){ return array } this.lists.splice(index, 1) return array } /** * add an object to be culled * NOTE: for implementation, add and remove uses this.lists[0] * * @param {DisplayObjectWithCulling} object * @param {boolean} [staticObject] - set to true if the object's position/size does not change * @return {DisplayObjectWithCulling} object */ add(object: DisplayObjectWithCulling, staticObject?: boolean): DisplayObjectWithCulling { if (staticObject) { object.staticObject = true } if (this.dirtyTest || staticObject) { this.updateObject(object) } this.lists[0].push(object) return object } /** * remove an object added by add() * NOTE: for implementation, add and remove uses this.lists[0] * * @param {DisplayObjectWithCulling} object * @return {DisplayObjectWithCulling} object */ remove(object: DisplayObjectWithCulling): DisplayObjectWithCulling { const index = this.lists[0].indexOf(object) if(index === -1){ return object } this.lists[0].splice(index, 1) return object } /** * cull the items in the list by changing the object.visible * @param {AABB} bounds * @param {boolean} [skipUpdate] - skip updating the AABB bounding box of all objects */ cull(bounds: AABB, skipUpdate?: boolean) { if (!skipUpdate) { this.updateObjects() } for (const list of this.lists) { const length = list.length for (let i = 0; i < length; i++) { const object = list[i] const box = object.AABB object.visible = box.x + box.width > bounds.x && box.x < bounds.x + bounds.width && box.y + box.height > bounds.y && box.y < bounds.y + bounds.height } } } /** * update the AABB for all objects * automatically called from update() when calculatePIXI=true and skipUpdate=false */ updateObjects() { if (this.dirtyTest) { for (const list of this.lists) { if (!list.staticObject) { const length = list.length for (let i = 0; i < length; i++) { const object = list[i] if (!object.staticObject && object.dirty) { this.updateObject(object) object.dirty = false } } } } } else { for (const list of this.lists) { if (!list.staticObject) { const length = list.length for (let i = 0; i < length; i++) { const object = list[i] if (!object.staticObject) { this.updateObject(object) } } } } } } /** * update the has of an object * automatically called from updateObjects() * @param {DisplayObjectWithCulling} object */ updateObject(object: DisplayObjectWithCulling) { const box = object.getLocalBounds() object.AABB = object.AABB || { x: 0, y: 0, width: 0, height: 0 } object.AABB.x = object.x + (box.x - object.pivot.x) * Math.abs(object.scale.x) object.AABB.y = object.y + (box.y - object.pivot.y) * Math.abs(object.scale.y) object.AABB.width = box.width * Math.abs(object.scale.x) object.AABB.height = box.height * Math.abs(object.scale.y) } /** * returns an array of objects contained within bounding box * @param {AABB} bounds - bounding box to search * @return {DisplayObjectWithCulling[]} - search results */ query(bounds: AABB): DisplayObjectWithCulling[] { let results = [] for (let list of this.lists) { for (let object of list) { const box = object.AABB if (box && box.x + box.width > bounds.x && box.x - box.width < bounds.x + bounds.width && box.y + box.height > bounds.y && box.y - box.height < bounds.y + bounds.height) { results.push(object) } } } return results } /** * iterates through objects contained within bounding box * stops iterating if the callback returns true * @param {AABB} bounds - bounding box to search * @param {function} callback * @return {boolean} - true if callback returned early */ queryCallback(bounds: AABB, callback: (object: DisplayObjectWithCulling) => boolean): boolean { for (let list of this.lists) { for (let object of list) { const box = object.AABB if (box && box.x + box.width > bounds.x && box.x - box.width < bounds.x + bounds.width && box.y + box.height > bounds.y && box.y - box.height < bounds.y + bounds.height) { if (callback(object)) { return true } } } } return false } /** * get stats (only updated after update() is called) * @return {SimpleStats} */ stats(): SimpleStats { let visible = 0, count = 0 for (let list of this.lists) { list.forEach(object => { visible += object.visible ? 1 : 0 count++ }) } return { total: count, visible, culled: count - visible } } }