From 945a64fbb6b3b416352816fe720c4c84406132a6 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Tue, 11 Nov 2025 12:03:48 +0100 Subject: [PATCH 1/9] initial check in --- src/core/CoreNode.ts | 123 +++++++++++++++++++++---------------------- src/core/Stage.ts | 2 - 2 files changed, 61 insertions(+), 64 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index fb844f61..122bb0fe 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -446,7 +446,6 @@ export interface CoreNodeProps { * settings being defaults) */ src: string | null; - zIndexLocked: number; /** * Scale to render the Node at * @@ -702,11 +701,13 @@ export interface CoreNodeAnimateProps extends NumberProps { */ export class CoreNode extends EventEmitter { readonly children: CoreNode[] = []; - protected _id: number = getNewId(); + readonly id: number = getNewId(); readonly props: CoreNodeProps; private hasShaderUpdater = false; private hasColorProps = false; + private zIndexMin = 0; + private zIndexMax = 0; public updateType = UpdateType.All; public childUpdateType = UpdateType.None; @@ -783,7 +784,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 +793,18 @@ 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; + 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; @@ -1196,12 +1199,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 @@ -1634,18 +1631,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 +1652,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; @@ -1729,11 +1710,55 @@ export class CoreNode extends EventEmitter { }); } - //#region Properties - get id(): number { - return this._id; + removeChild(node: CoreNode) { + const children = this.children; + let index = -1; + for (let i = 0; i < children.length; i++) { + if (children[i]!.id === node.id) { + index = i; + } + } + if (index === -1) { + return; + } + children.splice(index, 1); } + addChild(node: CoreNode) { + const children = this.children; + const min = this.zIndexMin; + const max = this.zIndexMax; + const zIndex = node.zIndex; + if (max === zIndex && min === zIndex) { + children.push(node); + return; + } + if (children.length === 0) { + this.zIndexMin = zIndex; + this.zIndexMax = zIndex; + children.push(node); + return; + } + if (zIndex < min) { + this.zIndexMin = zIndex; + children.unshift(node); + return; + } + if (zIndex >= max) { + this.zIndexMax = zIndex; + children.push(node); + return; + } + let index = 0; + for (; index < children.length; index++) { + if (zIndex < children[index]!.zIndex) { + break; + } + } + children.splice(index, 0, node); + } + + //#region Properties get data(): CustomDataMap | undefined { return this.props.data; } @@ -2126,20 +2151,6 @@ 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; } @@ -2167,29 +2178,17 @@ 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); } - 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 (newParent !== null) { + newParent.addChild(this); // If the new parent has an RTT enabled, apply RTT inheritance if (newParent.rtt || newParent.parentHasRenderTexture) { this.applyRTTInheritance(newParent); } } - - // 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 ?? {}, From eff689f845166c3c34edcd93aefa6132277fa9d8 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Wed, 19 Nov 2025 14:41:15 +0100 Subject: [PATCH 2/9] changed UpdateType enum --- src/core/CoreNode.ts | 142 ++++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 34 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 122bb0fe..048798ac 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -106,22 +106,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 +124,7 @@ export enum UpdateType { * - `premultipliedColorBl` * - `premultipliedColorBr` */ - PremultipliedColors = 64, + PremultipliedColors = 32, /** * World Alpha update @@ -142,7 +133,7 @@ export enum UpdateType { * CoreNode Properties Updated: * - `worldAlpha` = `parent.worldAlpha` * `alpha` */ - WorldAlpha = 128, + WorldAlpha = 64, /** * Render State update @@ -151,7 +142,7 @@ export enum UpdateType { * CoreNode Properties Updated: * - `renderState` */ - RenderState = 256, + RenderState = 128, /** * Is Renderable update @@ -160,27 +151,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 +181,7 @@ export enum UpdateType { /** * All */ - All = 14335, + All = 7167, } /** @@ -709,6 +700,9 @@ export class CoreNode extends EventEmitter { private zIndexMin = 0; private zIndexMax = 0; + public previousZIndex = -1; + public zIndexSortList: CoreNode[] = []; + public updateType = UpdateType.All; public childUpdateType = UpdateType.None; @@ -961,7 +955,65 @@ export class CoreNode extends EventEmitter { } sortChildren() { - this.children.sort((a, b) => a.calcZIndex - b.calcZIndex); + const zIndexSortList = this.zIndexSortList; + const children = this.children; + if ( + children.length === 1 || + children.length === 0 || + zIndexSortList.length === 0 + ) { + return; + } + + for (let i = 0; i < zIndexSortList.length; i++) { + const childIndex = this.findChildById(zIndexSortList[i]!.id); + //child not found + if (childIndex === -1) { + continue; + } + } + + for (let i = 0; i < zIndexSortList.length; i++) { + const child = zIndexSortList[i]!; + const zIndex = child.props.zIndex; + const sortDir = zIndex >= child.previousZIndex ? 1 : -1; + + for (let j = 0; j < children.length; j++) { + if (children[j]!.id === child.id) { + if (sortDir === 1) { + for (let k = j + 1; k < children.length; k++) {} + } else { + } + + for ( + let k = (j += sortDir); + k >= 0 && k < children.length; + k += sortDir + ) { + const compareChild = children[k]!; + if ( + sortDir === 1 && + child.props.zIndex < compareChild.props.zIndex + ) { + continue; + } + } + } + } + } + + zIndexSortList.length = 0; + // this.children.sort((a, b) => a.calcZIndex - b.calcZIndex); + } + + findChildById(id: number): number { + const children = this.children; + for (let i = 0; i < children.length; i++) { + if (children[i]!.id === id) { + return i; + } + } + return -1; } updateLocalTransform() { @@ -1252,7 +1304,7 @@ export class CoreNode extends EventEmitter { // Sorting children MUST happen after children have been updated so // that they have the oppotunity to update their calculated zIndex. - if (updateType & UpdateType.ZIndexSortedChildren) { + if (updateType & UpdateType.SortZIndexChildren) { // reorder z-index this.sortChildren(); } @@ -1710,7 +1762,14 @@ export class CoreNode extends EventEmitter { }); } - removeChild(node: CoreNode) { + removeChild(node: CoreNode, targetParent: CoreNode | null = null) { + if ( + targetParent === null && + this.props.rtt === true && + this.parentHasRenderTexture === true + ) { + node.clearRTTInheritance(); + } const children = this.children; let index = -1; for (let i = 0; i < children.length; i++) { @@ -1724,11 +1783,29 @@ export class CoreNode extends EventEmitter { children.splice(index, 1); } - addChild(node: CoreNode) { + 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); + } + if (max === zIndex && min === zIndex) { children.push(node); return; @@ -2159,11 +2236,12 @@ export class CoreNode extends EventEmitter { if (this.props.zIndex === value) { return; } - + this.previousZIndex = this.props.zIndex; 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); + const parent = this.parent; + if (parent !== null) { + parent.zIndexSortList.push(this); + parent.setUpdateType(UpdateType.SortZIndexChildren); } } @@ -2178,14 +2256,10 @@ export class CoreNode extends EventEmitter { } this.props.parent = newParent; if (oldParent) { - oldParent.removeChild(this); + oldParent.removeChild(this, newParent); } if (newParent !== null) { - newParent.addChild(this); - // If the new parent has an RTT enabled, apply RTT inheritance - if (newParent.rtt || newParent.parentHasRenderTexture) { - this.applyRTTInheritance(newParent); - } + newParent.addChild(this, oldParent); } // Since this node has a new parent, to be safe, have it do a full update. this.setUpdateType(UpdateType.All); From 05d36bace973b3201ddcd5b5732b681a9ab104f7 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 20 Nov 2025 11:10:09 +0100 Subject: [PATCH 3/9] optimized zindex sorting --- examples/tests/zIndex.ts | 44 +++++---------- src/core/CoreNode.test.ts | 1 - src/core/CoreNode.ts | 109 ++++++++++++++++---------------------- 3 files changed, 59 insertions(+), 95 deletions(-) diff --git a/examples/tests/zIndex.ts b/examples/tests/zIndex.ts index bc59d404..4f6eaa94 100644 --- a/examples/tests/zIndex.ts +++ b/examples/tests/zIndex.ts @@ -66,8 +66,8 @@ 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', { - radius: 2, + shader: renderer.createShader('Rounded', { + radius: 30, }), zIndex: 10 + (i + 1), parent: testRoot, @@ -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 048798ac..dfe8c3b2 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -954,68 +954,6 @@ export class CoreNode extends EventEmitter { parent.setUpdateType(UpdateType.Children); } - sortChildren() { - const zIndexSortList = this.zIndexSortList; - const children = this.children; - if ( - children.length === 1 || - children.length === 0 || - zIndexSortList.length === 0 - ) { - return; - } - - for (let i = 0; i < zIndexSortList.length; i++) { - const childIndex = this.findChildById(zIndexSortList[i]!.id); - //child not found - if (childIndex === -1) { - continue; - } - } - - for (let i = 0; i < zIndexSortList.length; i++) { - const child = zIndexSortList[i]!; - const zIndex = child.props.zIndex; - const sortDir = zIndex >= child.previousZIndex ? 1 : -1; - - for (let j = 0; j < children.length; j++) { - if (children[j]!.id === child.id) { - if (sortDir === 1) { - for (let k = j + 1; k < children.length; k++) {} - } else { - } - - for ( - let k = (j += sortDir); - k >= 0 && k < children.length; - k += sortDir - ) { - const compareChild = children[k]!; - if ( - sortDir === 1 && - child.props.zIndex < compareChild.props.zIndex - ) { - continue; - } - } - } - } - } - - zIndexSortList.length = 0; - // this.children.sort((a, b) => a.calcZIndex - b.calcZIndex); - } - - findChildById(id: number): number { - const children = this.children; - for (let i = 0; i < children.length; i++) { - if (children[i]!.id === id) { - return i; - } - } - return -1; - } - updateLocalTransform() { const p = this.props; const { x, y, w, h } = p; @@ -1302,8 +1240,7 @@ 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. + //Resort children if needed if (updateType & UpdateType.SortZIndexChildren) { // reorder z-index this.sortChildren(); @@ -1762,6 +1699,50 @@ export class CoreNode extends EventEmitter { }); } + sortChildren() { + const zIndexSortList = this.zIndexSortList; + const children = this.children; + if ( + children.length === 1 || + children.length === 0 || + zIndexSortList.length === 0 + ) { + return; + } + + for (let i = 0; i < zIndexSortList.length; i++) { + let childIndex = -1; + const child = zIndexSortList[i]!; + for (let j = 0; j < children.length; j++) { + if (children[j]!.id === child.id) { + childIndex = j; + break; + } + } + + //child not found + if (childIndex === -1) { + continue; + } + + // remove child from current position + children.splice(childIndex, 1); + + // find new position + let newIndex = 0; + for (; newIndex < children.length; newIndex++) { + if (child.props.zIndex < children[newIndex]!.props.zIndex) { + break; + } + } + + // insert child at new position + children.splice(newIndex, 0, child); + } + + zIndexSortList.length = 0; + } + removeChild(node: CoreNode, targetParent: CoreNode | null = null) { if ( targetParent === null && From a5ce7607ce5eed9cd78738ff0736249554e62033 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Thu, 20 Nov 2025 15:04:43 +0100 Subject: [PATCH 4/9] revert get id back to previous setup --- src/core/CoreNode.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index dfe8c3b2..6cfc91a8 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -692,7 +692,7 @@ export interface CoreNodeAnimateProps extends NumberProps { */ export class CoreNode extends EventEmitter { readonly children: CoreNode[] = []; - readonly id: number = getNewId(); + protected _id: number = getNewId(); readonly props: CoreNodeProps; private hasShaderUpdater = false; @@ -745,7 +745,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 @@ -1714,7 +1713,7 @@ export class CoreNode extends EventEmitter { let childIndex = -1; const child = zIndexSortList[i]!; for (let j = 0; j < children.length; j++) { - if (children[j]!.id === child.id) { + if (children[j]!._id === child._id) { childIndex = j; break; } @@ -1754,7 +1753,7 @@ export class CoreNode extends EventEmitter { const children = this.children; let index = -1; for (let i = 0; i < children.length; i++) { - if (children[i]!.id === node.id) { + if (children[i]!._id === node._id) { index = i; } } @@ -1817,6 +1816,10 @@ export class CoreNode extends EventEmitter { } //#region Properties + get id(): number { + return this._id; + } + get data(): CustomDataMap | undefined { return this.props.data; } From 5ae5dd709c977ce386fa9d6f725a63d76588945e Mon Sep 17 00:00:00 2001 From: jfboeve Date: Fri, 21 Nov 2025 10:28:05 +0100 Subject: [PATCH 5/9] revert radius settings in test --- examples/tests/zIndex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tests/zIndex.ts b/examples/tests/zIndex.ts index 4f6eaa94..e25ced9b 100644 --- a/examples/tests/zIndex.ts +++ b/examples/tests/zIndex.ts @@ -67,7 +67,7 @@ export default async function test({ renderer, testRoot }: ExampleSettings) { //@ts-ignore color: Colors[color], shader: renderer.createShader('Rounded', { - radius: 30, + radius: 2, }), zIndex: 10 + (i + 1), parent: testRoot, From 1de7fe66ddd6ab0659a8cd0de95ae565ca816dfe Mon Sep 17 00:00:00 2001 From: jfboeve Date: Mon, 24 Nov 2025 14:24:58 +0100 Subject: [PATCH 6/9] improved sorting by zIndex --- src/core/CoreNode.ts | 122 +++++++++++++------------------- src/core/lib/collectionUtils.ts | 105 +++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 73 deletions(-) create mode 100644 src/core/lib/collectionUtils.ts diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 6cfc91a8..4237dfce 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, @@ -1210,8 +1215,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; @@ -1649,6 +1653,7 @@ export class CoreNode extends EventEmitter { if (this.rtt === true) { this.stage.renderer.removeRTTNode(this); } + this.stage.requestRender(); } renderQuads(renderer: CoreRenderer): void { @@ -1699,47 +1704,45 @@ export class CoreNode extends EventEmitter { } sortChildren() { - const zIndexSortList = this.zIndexSortList; - const children = this.children; - if ( - children.length === 1 || - children.length === 0 || - zIndexSortList.length === 0 - ) { + const changedCount = this.zIndexSortList.length; + if (changedCount === 0) { return; } - - for (let i = 0; i < zIndexSortList.length; i++) { - let childIndex = -1; - const child = zIndexSortList[i]!; - for (let j = 0; j < children.length; j++) { - if (children[j]!._id === child._id) { - childIndex = j; - break; - } + 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; } - - //child not found - if (childIndex === -1) { - continue; + if (zIndex > max) { + max = zIndex; } + } + // update min and max zIndex + this.zIndexMin = min; + this.zIndexMax = max; - // remove child from current position - children.splice(childIndex, 1); + // if min and max are the same, no need to sort + if (min === max) { + return; + } - // find new position - let newIndex = 0; - for (; newIndex < children.length; newIndex++) { - if (child.props.zIndex < children[newIndex]!.props.zIndex) { - break; - } - } + const n = children.length; + // decide whether to use incremental sort or bucket sort + const useIncremental = changedCount < n * 0.05; - // insert child at new position - children.splice(newIndex, 0, child); + // when changed count is less than 5% of total children, use incremental sort + if (useIncremental === true) { + incrementalRepositionByZIndex(this.zIndexSortList, children); + } else { + bucketSortByZIndex(children, min); } - zIndexSortList.length = 0; + this.zIndexSortList.length = 0; + this.zIndexSortList = []; } removeChild(node: CoreNode, targetParent: CoreNode | null = null) { @@ -1750,17 +1753,7 @@ export class CoreNode extends EventEmitter { ) { node.clearRTTInheritance(); } - const children = this.children; - let index = -1; - for (let i = 0; i < children.length; i++) { - if (children[i]!._id === node._id) { - index = i; - } - } - if (index === -1) { - return; - } - children.splice(index, 1); + removeChild(node, this.children); } addChild(node: CoreNode, previousParent: CoreNode | null = null) { @@ -1786,33 +1779,12 @@ export class CoreNode extends EventEmitter { node.markChildrenWithRTT(this); } - if (max === zIndex && min === zIndex) { - children.push(node); - return; - } - if (children.length === 0) { - this.zIndexMin = zIndex; - this.zIndexMax = zIndex; - children.push(node); - return; - } - if (zIndex < min) { - this.zIndexMin = zIndex; - children.unshift(node); - return; - } - if (zIndex >= max) { - this.zIndexMax = zIndex; - children.push(node); - return; - } - let index = 0; - for (; index < children.length; index++) { - if (zIndex < children[index]!.zIndex) { - break; - } + children.push(node); + + if (min !== max || zIndex < min || zIndex > max) { + this.zIndexSortList.push(node); + this.setUpdateType(UpdateType.SortZIndexChildren); } - children.splice(index, 0, node); } //#region Properties @@ -2224,8 +2196,12 @@ export class CoreNode extends EventEmitter { this.props.zIndex = value; const parent = this.parent; if (parent !== null) { - parent.zIndexSortList.push(this); - parent.setUpdateType(UpdateType.SortZIndexChildren); + const min = parent.zIndexMin; + const max = parent.zIndexMax; + if (min !== max || value < min || value > max) { + parent.zIndexSortList.push(this); + parent.setUpdateType(UpdateType.SortZIndexChildren); + } } } diff --git a/src/core/lib/collectionUtils.ts b/src/core/lib/collectionUtils.ts new file mode 100644 index 00000000..eadc7721 --- /dev/null +++ b/src/core/lib/collectionUtils.ts @@ -0,0 +1,105 @@ +/* + * 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 = 0, +): void => { + const buckets: CoreNode[][] = []; + const bucketIds: 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]) { + buckets[index] = []; + //keep track of created bucket ids + bucketIds.push(index); + } + buckets[index]!.push(node); + } + + //flatten buckets + let idx = 0; + for (let i = 0; i < bucketIds.length; i++) { + const bucket = buckets[bucketIds[i]!]!; + for (let j = 0; j < bucket.length; j++) { + nodes[idx++] = bucket[j]!; + } + } + + //clean up + buckets.length = 0; + bucketIds.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); + } +}; From 76f78d77c570133ae5ebee7b2e013ea61874f519 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Tue, 25 Nov 2025 11:39:18 +0100 Subject: [PATCH 7/9] fixed bucket sorting, introduced max zIndex 1000, min zIndex -1000 --- src/core/CoreNode.ts | 37 +++++++++++++++++++++++++++------ src/core/lib/collectionUtils.ts | 30 +++++++++++++++----------- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 4237dfce..a8226f7f 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -385,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; /** @@ -798,6 +802,11 @@ export class CoreNode extends EventEmitter { 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); } @@ -1721,6 +1730,7 @@ export class CoreNode extends EventEmitter { max = zIndex; } } + // update min and max zIndex this.zIndexMin = min; this.zIndexMax = max; @@ -1732,7 +1742,7 @@ export class CoreNode extends EventEmitter { const n = children.length; // decide whether to use incremental sort or bucket sort - const useIncremental = changedCount < n * 0.05; + const useIncremental = changedCount <= 2 && changedCount < n * 0.05; // when changed count is less than 5% of total children, use incremental sort if (useIncremental === true) { @@ -1781,7 +1791,7 @@ export class CoreNode extends EventEmitter { children.push(node); - if (min !== max || zIndex < min || zIndex > max) { + if (min !== max || (zIndex !== min && zIndex !== max)) { this.zIndexSortList.push(node); this.setUpdateType(UpdateType.SortZIndexChildren); } @@ -2189,16 +2199,31 @@ export class CoreNode extends EventEmitter { } set zIndex(value: number) { - if (this.props.zIndex === value) { + 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; + } + + if (this.props.zIndex === sanitizedValue) { return; } this.previousZIndex = this.props.zIndex; - this.props.zIndex = value; + this.props.zIndex = sanitizedValue; const parent = this.parent; if (parent !== null) { const min = parent.zIndexMin; const max = parent.zIndexMax; - if (min !== max || value < min || value > max) { + if (min !== max || sanitizedValue < min || sanitizedValue > max) { parent.zIndexSortList.push(this); parent.setUpdateType(UpdateType.SortZIndexChildren); } diff --git a/src/core/lib/collectionUtils.ts b/src/core/lib/collectionUtils.ts index eadc7721..b9f2c21d 100644 --- a/src/core/lib/collectionUtils.ts +++ b/src/core/lib/collectionUtils.ts @@ -20,30 +20,36 @@ import type { CoreNode } from '../CoreNode.js'; //Bucket sort implementation for sorting CoreNode arrays by zIndex -export const bucketSortByZIndex = ( - nodes: CoreNode[], - min: number = 0, -): void => { +export const bucketSortByZIndex = (nodes: CoreNode[], min: number): void => { const buckets: CoreNode[][] = []; - const bucketIds: number[] = []; - + 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]) { + if (buckets[index] === undefined) { buckets[index] = []; - //keep track of created bucket ids - bucketIds.push(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 < bucketIds.length; i++) { - const bucket = buckets[bucketIds[i]!]!; + 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]!; } @@ -51,7 +57,7 @@ export const bucketSortByZIndex = ( //clean up buckets.length = 0; - bucketIds.length = 0; + bucketIndices.length = 0; }; export const incrementalRepositionByZIndex = ( From 3bdab93e2ecba0c6201e7d0c597a2eb53df7d8b1 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Tue, 16 Dec 2025 10:03:51 +0100 Subject: [PATCH 8/9] improved incremental check, pruned updateType when changing parents. --- src/core/CoreNode.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index a8226f7f..c13eae4c 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -1742,9 +1742,9 @@ export class CoreNode extends EventEmitter { const n = children.length; // decide whether to use incremental sort or bucket sort - const useIncremental = changedCount <= 2 && changedCount < n * 0.05; + const useIncremental = changedCount <= 2 || changedCount < n * 0.05; - // when changed count is less than 5% of total children, use incremental sort + // when changed count is less than 2 or 5% of total children, use incremental sort if (useIncremental === true) { incrementalRepositionByZIndex(this.zIndexSortList, children); } else { @@ -2246,8 +2246,8 @@ export class CoreNode extends EventEmitter { if (newParent !== null) { newParent.addChild(this, oldParent); } - // Since this node has a new parent, to be safe, have it do a full update. - this.setUpdateType(UpdateType.All); + //since this node has a new parent, recalc global and render bounds + this.setUpdateType(UpdateType.Global | UpdateType.RenderBounds); } get rtt(): boolean { From 1cd68059d58872531759df869917765b61e34986 Mon Sep 17 00:00:00 2001 From: jfboeve Date: Tue, 16 Dec 2025 11:45:13 +0100 Subject: [PATCH 9/9] fixed / improved incrementalRepositionByZindex --- src/core/lib/collectionUtils.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/core/lib/collectionUtils.ts b/src/core/lib/collectionUtils.ts index b9f2c21d..d3be108a 100644 --- a/src/core/lib/collectionUtils.ts +++ b/src/core/lib/collectionUtils.ts @@ -69,22 +69,29 @@ export const incrementalRepositionByZIndex = ( const currentIndex = findChildIndexById(node, nodes); if (currentIndex === -1) continue; - //remove node from current position - nodes.splice(currentIndex, 1); + const targetZIndex = node.props.zIndex; + //binary search for correct insertion position let left = 0; - let right = nodes.length - 1; - const targetZIndex = node.props.zIndex; + let right = nodes.length; while (left < right) { const mid = (left + right) >>> 1; - if (nodes[mid]!.props.zIndex < targetZIndex) { + if (nodes[mid]!.props.zIndex <= targetZIndex) { left = mid + 1; } else { right = mid; } } - nodes.splice(left, 0, node); + + //adjust target position if it's after the current position + const targetIndex = left > currentIndex ? left - 1 : left; + + //only reposition if target is different from current + if (targetIndex !== currentIndex) { + nodes.splice(currentIndex, 1); + nodes.splice(targetIndex, 0, node); + } } };