diff --git a/examples/tests/zIndex.ts b/examples/tests/zIndex.ts index bc59d404..e25ced9b 100644 --- a/examples/tests/zIndex.ts +++ b/examples/tests/zIndex.ts @@ -66,7 +66,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore color: Colors[color], - shader: renderer.createShader('RoundedRectangle', { + shader: renderer.createShader('Rounded', { radius: 2, }), zIndex: 10 + (i + 1), @@ -81,12 +81,11 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 600, h: 600, color: Colors.Gray, - // shader: renderer.createShader('RoundedRectangle', { + // shader: renderer.createShader('Rounded', { // radius: 40, // }), zIndex: 2, parent: testRoot, - zIndexLocked: 1, }); const childRectWhite = renderer.createNode({ @@ -95,7 +94,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 200, h: 200, color: Colors.White, - // shader: renderer.createShader('RoundedRectangle', { + // shader: renderer.createShader('Rounded', { // radius: 40, // }), zIndex: 4, @@ -108,7 +107,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 200, h: 200, color: Colors.Red, - // shader: renderer.createShader('RoundedRectangle', { + // shader: renderer.createShader('Rounded', { // radius: 40, // }), zIndex: 5, @@ -136,7 +135,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 400, h: 100, color: Colors.Green, - // shader: renderer.createShader('RoundedRectangle', { + // shader: renderer.createShader('Rounded', { // radius: 40, // }), zIndex: 2, @@ -164,13 +163,11 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 10, h: 10, color: 0x00ffffff, - shader: renderer.createShader('RoundedRectangle', { + shader: renderer.createShader('Rounded', { radius: 2, }), zIndex: 148901482921849101841290481, - - zIndexLocked: 148901482921849101841290481, parent: testRoot, }); @@ -180,13 +177,12 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 10, h: 10, color: 0x00ffffff, - shader: renderer.createShader('RoundedRectangle', { + shader: renderer.createShader('Rounded', { radius: 2, }), zIndex: -148901482921849101841290481, - zIndexLocked: -148901482921849101841290481, parent: testRoot, }); @@ -196,13 +192,11 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 10, h: 10, color: 0x00ffffff, - shader: renderer.createShader('RoundedRectangle', { + shader: renderer.createShader('Rounded', { radius: 2, }), // @ts-expect-error Invalid prop test zIndex: 'boop', - // @ts-expect-error Invalid prop test - zIndexLocked: 'boop', parent: testRoot, }); @@ -212,13 +206,11 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 10, h: 10, color: 0x00ffffff, - shader: renderer.createShader('RoundedRectangle', { + shader: renderer.createShader('Rounded', { radius: 2, }), // @ts-expect-error Invalid prop test zIndex: true, - // @ts-expect-error Invalid prop test - zIndexLocked: true, parent: testRoot, }); @@ -228,13 +220,11 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 10, h: 10, color: 0x00ffffff, - shader: renderer.createShader('RoundedRectangle', { + shader: renderer.createShader('Rounded', { radius: 2, }), // @ts-expect-error Invalid prop test zIndex: null, - // @ts-expect-error Invalid prop test - zIndexLocked: null, parent: testRoot, }); @@ -244,11 +234,10 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 10, h: 10, color: 0x00ffffff, - shader: renderer.createShader('RoundedRectangle', { + shader: renderer.createShader('Rounded', { radius: 2, }), zIndex: undefined, - zIndexLocked: undefined, parent: testRoot, }); @@ -258,15 +247,12 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 10, h: 10, color: 0x00ffffff, - shader: renderer.createShader('RoundedRectangle', { + shader: renderer.createShader('Rounded', { radius: 2, }), // @ts-expect-error Invalid prop test zIndex: () => {}, - // @ts-expect-error Invalid prop test - - zIndexLocked: () => {}, parent: testRoot, }); @@ -276,13 +262,11 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { w: 10, h: 10, color: 0x00ffffff, - shader: renderer.createShader('RoundedRectangle', { + shader: renderer.createShader('Rounded', { radius: 2, }), // @ts-expect-error Invalid prop test zIndex: {}, - // @ts-expect-error Invalid prop test - zIndexLocked: {}, parent: testRoot, }); } diff --git a/src/core/CoreNode.test.ts b/src/core/CoreNode.test.ts index 4e519d70..45339e6e 100644 --- a/src/core/CoreNode.test.ts +++ b/src/core/CoreNode.test.ts @@ -62,7 +62,6 @@ describe('set color()', () => { x: 0, y: 0, zIndex: 0, - zIndexLocked: 0, }; const clippingRect = { diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index fb844f61..a8226f7f 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -56,6 +56,11 @@ import type { IAnimationController } from '../common/IAnimationController.js'; import { CoreAnimation } from './animations/CoreAnimation.js'; import { CoreAnimationController } from './animations/CoreAnimationController.js'; import type { CoreShaderNode } from './renderers/CoreShaderNode.js'; +import { + bucketSortByZIndex, + incrementalRepositionByZIndex, + removeChild, +} from './lib/collectionUtils.js'; export enum CoreNodeRenderState { Init = 0, @@ -106,22 +111,13 @@ export enum UpdateType { Clipping = 8, /** - * Calculated ZIndex update - * - * @remarks - * CoreNode Properties Updated: - * - `calcZIndex` - */ - CalculatedZIndex = 16, - - /** - * Z-Index Sorted Children update + * Sort Z-Index Children update * * @remarks * CoreNode Properties Updated: * - `children` (sorts children by their `calcZIndex`) */ - ZIndexSortedChildren = 32, + SortZIndexChildren = 16, /** * Premultiplied Colors update @@ -133,7 +129,7 @@ export enum UpdateType { * - `premultipliedColorBl` * - `premultipliedColorBr` */ - PremultipliedColors = 64, + PremultipliedColors = 32, /** * World Alpha update @@ -142,7 +138,7 @@ export enum UpdateType { * CoreNode Properties Updated: * - `worldAlpha` = `parent.worldAlpha` * `alpha` */ - WorldAlpha = 128, + WorldAlpha = 64, /** * Render State update @@ -151,7 +147,7 @@ export enum UpdateType { * CoreNode Properties Updated: * - `renderState` */ - RenderState = 256, + RenderState = 128, /** * Is Renderable update @@ -160,27 +156,27 @@ export enum UpdateType { * CoreNode Properties Updated: * - `isRenderable` */ - IsRenderable = 512, + IsRenderable = 256, /** * Render Texture update */ - RenderTexture = 1024, + RenderTexture = 512, /** * Track if parent has render texture */ - ParentRenderTexture = 2048, + ParentRenderTexture = 1024, /** * Render Bounds update */ - RenderBounds = 4096, + RenderBounds = 2048, /** * RecalcUniforms */ - RecalcUniforms = 8192, + RecalcUniforms = 4096, /** * None @@ -190,7 +186,7 @@ export enum UpdateType { /** * All */ - All = 14335, + All = 7167, } /** @@ -389,7 +385,11 @@ export interface CoreNodeProps { * The Node's z-index. * * @remarks - * TBD + * Max z-index of children under the same parent determines which child + * is rendered on top. Higher z-index means the Node is rendered on top of + * children with lower z-index. + * + * Max value is 1000 and min value is -1000. Values outside of this range will be clamped. */ zIndex: number; /** @@ -446,7 +446,6 @@ export interface CoreNodeProps { * settings being defaults) */ src: string | null; - zIndexLocked: number; /** * Scale to render the Node at * @@ -707,6 +706,11 @@ export class CoreNode extends EventEmitter { private hasShaderUpdater = false; private hasColorProps = false; + private zIndexMin = 0; + private zIndexMax = 0; + + public previousZIndex = -1; + public zIndexSortList: CoreNode[] = []; public updateType = UpdateType.All; public childUpdateType = UpdateType.None; @@ -750,7 +754,6 @@ export class CoreNode extends EventEmitter { constructor(readonly stage: Stage, props: CoreNodeProps) { super(); - const p = (this.props = {} as CoreNodeProps); // Fast-path assign only known keys @@ -783,7 +786,6 @@ export class CoreNode extends EventEmitter { p.pivot = props.pivot; p.zIndex = props.zIndex; - p.zIndexLocked = props.zIndexLocked; p.textureOptions = props.textureOptions; p.data = props.data; @@ -793,15 +795,23 @@ export class CoreNode extends EventEmitter { p.srcWidth = props.srcWidth; p.srcHeight = props.srcHeight; - p.parent = null; + p.parent = props.parent; p.texture = null; p.shader = null; p.src = null; p.rtt = false; p.boundsMargin = null; + // Only set non-default values + if (props.zIndex !== 0) { + this.zIndex = props.zIndex; + } + + if (props.parent !== null) { + props.parent.addChild(this); + } + // Assign props to instances - this.parent = props.parent; this.texture = props.texture; this.shader = props.shader; this.src = props.src; @@ -957,10 +967,6 @@ export class CoreNode extends EventEmitter { parent.setUpdateType(UpdateType.Children); } - sortChildren() { - this.children.sort((a, b) => a.calcZIndex - b.calcZIndex); - } - updateLocalTransform() { const p = this.props; const { x, y, w, h } = p; @@ -1196,12 +1202,6 @@ export class CoreNode extends EventEmitter { if (updateParent === true) { parent!.setUpdateType(UpdateType.Children); } - // No need to update zIndex if there is no parent - if (updateType & UpdateType.CalculatedZIndex && parent !== null) { - this.calculateZIndex(); - // Tell parent to re-sort children - parent.setUpdateType(UpdateType.ZIndexSortedChildren); - } if (this.renderState === CoreNodeRenderState.OutOfBounds) { updateType &= ~UpdateType.RenderBounds; // remove render bounds update @@ -1224,8 +1224,7 @@ export class CoreNode extends EventEmitter { if (updateType & UpdateType.Children && this.children.length > 0) { for (let i = 0, length = this.children.length; i < length; i++) { const child = this.children[i] as CoreNode; - - child.setUpdateType(childUpdateType); + child.updateType |= childUpdateType; if (child.updateType === 0) { continue; @@ -1253,9 +1252,8 @@ export class CoreNode extends EventEmitter { this.notifyParentRTTOfUpdate(); } - // Sorting children MUST happen after children have been updated so - // that they have the oppotunity to update their calculated zIndex. - if (updateType & UpdateType.ZIndexSortedChildren) { + //Resort children if needed + if (updateType & UpdateType.SortZIndexChildren) { // reorder z-index this.sortChildren(); } @@ -1634,18 +1632,6 @@ export class CoreNode extends EventEmitter { } } - calculateZIndex(): void { - const props = this.props; - const z = props.zIndex || 0; - const p = props.parent?.zIndex || 0; - - let zIndex = z; - if (props.parent?.zIndexLocked) { - zIndex = z < p ? z : p; - } - this.calcZIndex = zIndex; - } - /** * Destroy the node and cleanup all resources */ @@ -1667,11 +1653,7 @@ export class CoreNode extends EventEmitter { const parent = this.parent; if (parent !== null) { - const index = parent.children.indexOf(this); - parent.children.splice(index, 1); - parent.setUpdateType( - UpdateType.Children | UpdateType.ZIndexSortedChildren, - ); + parent.removeChild(this); } this.props.parent = null; @@ -1680,6 +1662,7 @@ export class CoreNode extends EventEmitter { if (this.rtt === true) { this.stage.renderer.removeRTTNode(this); } + this.stage.requestRender(); } renderQuads(renderer: CoreRenderer): void { @@ -1729,6 +1712,91 @@ export class CoreNode extends EventEmitter { }); } + sortChildren() { + const changedCount = this.zIndexSortList.length; + if (changedCount === 0) { + return; + } + const children = this.children; + let min = Infinity; + let max = -Infinity; + // find min and max zIndex + for (let i = 0; i < children.length; i++) { + const zIndex = children[i]!.props.zIndex; + if (zIndex < min) { + min = zIndex; + } + if (zIndex > max) { + max = zIndex; + } + } + + // update min and max zIndex + this.zIndexMin = min; + this.zIndexMax = max; + + // if min and max are the same, no need to sort + if (min === max) { + return; + } + + const n = children.length; + // decide whether to use incremental sort or bucket sort + const useIncremental = changedCount <= 2 && changedCount < n * 0.05; + + // when changed count is less than 5% of total children, use incremental sort + if (useIncremental === true) { + incrementalRepositionByZIndex(this.zIndexSortList, children); + } else { + bucketSortByZIndex(children, min); + } + + this.zIndexSortList.length = 0; + this.zIndexSortList = []; + } + + removeChild(node: CoreNode, targetParent: CoreNode | null = null) { + if ( + targetParent === null && + this.props.rtt === true && + this.parentHasRenderTexture === true + ) { + node.clearRTTInheritance(); + } + removeChild(node, this.children); + } + + addChild(node: CoreNode, previousParent: CoreNode | null = null) { + const inRttCluster = + this.props.rtt === true || this.parentHasRenderTexture === true; + const children = this.children; + const min = this.zIndexMin; + const max = this.zIndexMax; + const zIndex = node.zIndex; + + node.parentHasRenderTexture = inRttCluster; + if (previousParent !== null) { + const previousParentInRttCluster = + previousParent.props.rtt === true || + previousParent.parentHasRenderTexture === true; + if (inRttCluster === false && previousParentInRttCluster === true) { + // update child RTT status + node.clearRTTInheritance(); + } + } + + if (inRttCluster === true) { + node.markChildrenWithRTT(this); + } + + children.push(node); + + if (min !== max || (zIndex !== min && zIndex !== max)) { + this.zIndexSortList.push(node); + this.setUpdateType(UpdateType.SortZIndexChildren); + } + } + //#region Properties get id(): number { return this._id; @@ -2126,33 +2194,39 @@ export class CoreNode extends EventEmitter { this.setUpdateType(UpdateType.PremultipliedColors); } - // we're only interested in parent zIndex to test - // if we should use node zIndex is higher then parent zIndex - get zIndexLocked(): number { - return this.props.zIndexLocked || 0; - } - - set zIndexLocked(value: number) { - this.props.zIndexLocked = value; - this.setUpdateType(UpdateType.CalculatedZIndex | UpdateType.Children); - for (let i = 0, length = this.children.length; i < length; i++) { - this.children[i]!.setUpdateType(UpdateType.CalculatedZIndex); - } - } - get zIndex(): number { return this.props.zIndex; } set zIndex(value: number) { - if (this.props.zIndex === value) { - return; + let sanitizedValue = value; + if (isNaN(sanitizedValue) || Number.isFinite(sanitizedValue) === false) { + console.warn( + `zIndex was set to an invalid value: ${value}, defaulting to 0`, + ); + sanitizedValue = 0; + } + + //Clamp to safe integer range + if (sanitizedValue > Number.MAX_SAFE_INTEGER) { + sanitizedValue = 1000; + } else if (sanitizedValue < Number.MIN_SAFE_INTEGER) { + sanitizedValue = -1000; } - this.props.zIndex = value; - this.setUpdateType(UpdateType.CalculatedZIndex | UpdateType.Children); - for (let i = 0, length = this.children.length; i < length; i++) { - this.children[i]!.setUpdateType(UpdateType.CalculatedZIndex); + if (this.props.zIndex === sanitizedValue) { + return; + } + this.previousZIndex = this.props.zIndex; + this.props.zIndex = sanitizedValue; + const parent = this.parent; + if (parent !== null) { + const min = parent.zIndexMin; + const max = parent.zIndexMax; + if (min !== max || sanitizedValue < min || sanitizedValue > max) { + parent.zIndexSortList.push(this); + parent.setUpdateType(UpdateType.SortZIndexChildren); + } } } @@ -2167,29 +2241,13 @@ export class CoreNode extends EventEmitter { } this.props.parent = newParent; if (oldParent) { - const index = oldParent.children.indexOf(this); - oldParent.children.splice(index, 1); - oldParent.setUpdateType( - UpdateType.Children | UpdateType.ZIndexSortedChildren, - ); + oldParent.removeChild(this, newParent); } - if (newParent) { - newParent.children.push(this); - // Since this node has a new parent, to be safe, have it do a full update. - this.setUpdateType(UpdateType.All); - // Tell parent that it's children need to be updated and sorted. - newParent.setUpdateType( - UpdateType.Children | UpdateType.ZIndexSortedChildren, - ); - - // If the new parent has an RTT enabled, apply RTT inheritance - if (newParent.rtt || newParent.parentHasRenderTexture) { - this.applyRTTInheritance(newParent); - } + if (newParent !== null) { + newParent.addChild(this, oldParent); } - - // fetch render bounds from parent - this.setUpdateType(UpdateType.RenderBounds | UpdateType.Children); + // Since this node has a new parent, to be safe, have it do a full update. + this.setUpdateType(UpdateType.All); } get rtt(): boolean { diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 57ec1a57..004bcd30 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -335,7 +335,6 @@ export class Stage { colorBl: 0x00000000, colorBr: 0x00000000, zIndex: 0, - zIndexLocked: 0, scaleX: 1, scaleY: 1, mountX: 0, @@ -818,7 +817,6 @@ export class Stage { colorBl, colorBr, zIndex: props.zIndex ?? 0, - zIndexLocked: props.zIndexLocked ?? 0, parent: props.parent ?? null, texture: props.texture ?? null, textureOptions: props.textureOptions ?? {}, diff --git a/src/core/lib/collectionUtils.ts b/src/core/lib/collectionUtils.ts new file mode 100644 index 00000000..b9f2c21d --- /dev/null +++ b/src/core/lib/collectionUtils.ts @@ -0,0 +1,111 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2023 Comcast Cable Communications Management, LLC. + * + * Licensed under the Apache License, Version 2.0 (the License); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { CoreNode } from '../CoreNode.js'; + +//Bucket sort implementation for sorting CoreNode arrays by zIndex +export const bucketSortByZIndex = (nodes: CoreNode[], min: number): void => { + const buckets: CoreNode[][] = []; + const bucketIndices: number[] = []; + //distribute nodes into buckets + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]!; + const index = node.props.zIndex - min; + //create bucket if it doesn't exist + if (buckets[index] === undefined) { + buckets[index] = []; + bucketIndices.push(index); + } + buckets[index]!.push(node); + } + + //sort each bucket using insertion sort + for (let i = 1; i < bucketIndices.length; i++) { + const key = bucketIndices[i]!; + let j = i - 1; + while (j >= 0 && bucketIndices[j]! > key) { + bucketIndices[j + 1] = bucketIndices[j]!; + j--; + } + bucketIndices[j + 1] = key; + } + + //flatten buckets + let idx = 0; + for (let i = 0; i < bucketIndices.length; i++) { + const bucket = buckets[bucketIndices[i]!]!; + for (let j = 0; j < bucket.length; j++) { + nodes[idx++] = bucket[j]!; + } + } + + //clean up + buckets.length = 0; + bucketIndices.length = 0; +}; + +export const incrementalRepositionByZIndex = ( + changedNodes: CoreNode[], + nodes: CoreNode[], +): void => { + for (let i = 0; i < changedNodes.length; i++) { + const node = changedNodes[i]!; + const currentIndex = findChildIndexById(node, nodes); + if (currentIndex === -1) continue; + + //remove node from current position + nodes.splice(currentIndex, 1); + + let left = 0; + let right = nodes.length - 1; + const targetZIndex = node.props.zIndex; + + while (left < right) { + const mid = (left + right) >>> 1; + if (nodes[mid]!.props.zIndex < targetZIndex) { + left = mid + 1; + } else { + right = mid; + } + } + nodes.splice(left, 0, node); + } +}; + +export const findChildIndexById = ( + node: CoreNode, + children: CoreNode[], +): number => { + for (let i = 0; i < children.length; i++) { + const child = children[i]!; + + // @ts-ignore - accessing protected property + if (child._id === node._id) { + return i; + } + } + return -1; +}; + +export const removeChild = (node: CoreNode, children: CoreNode[]): void => { + const index = findChildIndexById(node, children); + if (index !== -1) { + children.splice(index, 1); + } +};