diff --git a/src/core/CoreNode.test.ts b/src/core/CoreNode.test.ts index dfce10a..90751b0 100644 --- a/src/core/CoreNode.test.ts +++ b/src/core/CoreNode.test.ts @@ -1113,5 +1113,71 @@ describe('set color()', () => { expect(node.clippingRect.valid).toBe(false); }); + + it('shares the invalid default rect when neither node nor ancestor clips', () => { + const parent = new CoreNode(stage, defaultProps()); + parent.globalTransform = Matrix3d.identity(); + parent.worldAlpha = 1; + + const node = new CoreNode(stage, defaultProps({ parent })); + node.alpha = 1; + node.setUpdateType(UpdateType.Clipping); + + node.update(0, { x: 0, y: 0, w: 0, h: 0, valid: false }); + + // No own allocation: the field still points at the shared default that + // every freshly-constructed node starts with. + const fresh = new CoreNode(stage, defaultProps()); + expect(node.clippingRect).toBe(fresh.clippingRect); + expect(node.clippingRect.valid).toBe(false); + }); + + it('never mutates the shared default when a sibling node clips', () => { + const clipParent = new CoreNode(stage, defaultProps()); + clipParent.globalTransform = Matrix3d.identity(); + clipParent.worldAlpha = 1; + + const clippingNode = new CoreNode( + stage, + defaultProps({ parent: clipParent }), + ); + clippingNode.worldAlpha = 1; + clippingNode.alpha = 1; + clippingNode.x = 10; + clippingNode.y = 20; + clippingNode.w = 30; + clippingNode.h = 40; + clippingNode.clipping = true; + clippingNode.update(0, { x: 0, y: 0, w: 1000, h: 1000, valid: true }); + + // A separate non-clipping node must still see a pristine invalid default. + const plain = new CoreNode(stage, defaultProps()); + expect(plain.clippingRect.valid).toBe(false); + expect(plain.clippingRect.x).toBe(0); + expect(plain.clippingRect.y).toBe(0); + expect(plain.clippingRect.w).toBe(0); + expect(plain.clippingRect.h).toBe(0); + }); + + it('allocates its own rect to inherit a valid ancestor clip rect', () => { + const parent = new CoreNode(stage, defaultProps()); + parent.globalTransform = Matrix3d.identity(); + parent.worldAlpha = 1; + + const node = new CoreNode(stage, defaultProps({ parent })); + node.alpha = 1; + node.setUpdateType(UpdateType.Clipping); + + const fresh = new CoreNode(stage, defaultProps()); + node.update(0, { x: 5, y: 6, w: 20, h: 30, valid: true }); + + // Now owns a private rect carrying the parent's clip values. + expect(node.clippingRect).not.toBe(fresh.clippingRect); + expect(node.clippingRect.valid).toBe(true); + expect(node.clippingRect.x).toBe(5); + expect(node.clippingRect.y).toBe(6); + expect(node.clippingRect.w).toBe(20); + expect(node.clippingRect.h).toBe(30); + }); }); }); diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index 2ddc40b..d96ccd0 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -808,13 +808,14 @@ export class CoreNode extends EventEmitter { public renderBound?: Bound; public strictBound?: Bound; public preloadBound?: Bound; - public clippingRect: RectWithValid = { - x: 0, - y: 0, - w: 0, - h: 0, - valid: false, - }; + /** + * Points at the shared `NO_CLIPPING_RECT` until this node actually + * participates in clipping (either it clips, or an ancestor's clip rect + * propagates down). Clipping is rare across the scene graph, so most nodes + * never allocate their own rect — `calculateClippingRect` swaps in a private + * object lazily the first time one is needed. + */ + public clippingRect: RectWithValid = NO_CLIPPING_RECT; public textureCoords?: TextureCoords; public updateShaderUniforms: boolean = false; public isRenderable = false; @@ -1933,11 +1934,33 @@ export class CoreNode extends EventEmitter { * Finally, the node's parentClippingRect and clippingRect properties are updated. */ calculateClippingRect(parentClippingRect: RectWithValid) { - const { clippingRect, props, globalTransform: gt } = this; + const { props, globalTransform: gt } = this; const { clipping } = props; const isRotated = gt!.tb !== 0 || gt!.tc !== 0; + const nodeClips = clipping !== false && isRotated === false; + + // Common case: this node doesn't clip and no ancestor clip rect needs to + // propagate. No node-owned rect is required, so point at the shared + // invalid default and skip the allocation entirely. + if (nodeClips === false && parentClippingRect.valid === false) { + this.clippingRect = NO_CLIPPING_RECT; + return; + } + + // A node-owned, mutable rect is needed. Allocate one lazily the first time + // (the default shares NO_CLIPPING_RECT, which must never be written to). + let clippingRect = this.clippingRect; + if (clippingRect === NO_CLIPPING_RECT) { + clippingRect = this.clippingRect = { + x: 0, + y: 0, + w: 0, + h: 0, + valid: false, + }; + } - if (clipping !== false && isRotated === false) { + if (nodeClips === true) { let mT = 0; let mR = 0; let mB = 0;