From 8c563295069df3965748ec83937bef2a3dff7b18 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Thu, 4 Jun 2026 18:15:07 -0400 Subject: [PATCH] refactor(core)!: remove the interactive/interactiveNodes hit-test feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: removes the node `interactive` prop and the Stage pointer hit-test API (`getNodeFromPosition`, `findNodesAtPoint`). `Stage.interactiveNodes` was a Set that nodes added themselves to (via the `interactive` setter) but were never removed from on destroy — a strong-ref leak that pinned every destroyed interactive node for the life of the session. Rather than patch the leak, this removes the whole feature, which existed only to back the rarely-used built-in pointer hit-test. Removed: - `Stage.interactiveNodes`, `Stage.findNodesAtPoint`, `Stage.getNodeFromPosition` - the exported `Point` type (only used by those hit-test signatures) - `CoreNode` `interactive` getter/setter, the `interactive` prop on `CoreNodeProps` (and thus the public `INodeProps`), its constructor handling and its default in `resolveNodeDefaults` - `pointInBound` in core/lib/utils (dead once the hit-test was removed) - the `detect-touch` example (a demo of this feature) Apps that relied on the built-in hit-test should implement point-in-bounds testing against their own node references in application code. Co-Authored-By: Claude Opus 4.8 --- examples/tests/detect-touch.ts | 112 --------------------------------- src/core/CoreNode.ts | 24 +------ src/core/Stage.ts | 47 -------------- src/core/lib/utils.ts | 4 -- 4 files changed, 1 insertion(+), 186 deletions(-) delete mode 100644 examples/tests/detect-touch.ts diff --git a/examples/tests/detect-touch.ts b/examples/tests/detect-touch.ts deleted file mode 100644 index bf1a97a..0000000 --- a/examples/tests/detect-touch.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { Point } from '@lightningjs/renderer'; -import type { ExampleSettings } from '../common/ExampleSettings.js'; -import type { CoreNode } from '../../dist/src/core/CoreNode.js'; - -const getRandomValue = (min: number, max: number) => { - return Math.random() * (max - min) + min; -}; - -const getRandomColor = () => { - const randomInt = Math.floor(Math.random() * Math.pow(2, 24)); // Use 24 bits for RGB - const hexString = randomInt.toString(16).padStart(6, '0'); // RGB hex without alpha - return parseInt(hexString + 'FF', 16); // Append 'FF' for full alpha -}; - -const getRandomBezierCurve = () => { - // Generate random values for control points within specified ranges - const x1 = Math.random(); // 0 to 1 - const y1 = Math.random() * 2; // Allow values above 1 - const x2 = Math.random(); // 0 to 1 - const y2 = Math.random() * 2 - 1; // Allow values between -1 and 1 - - // Return the Bezier curve in the required format - return `cubic-bezier(${x1.toFixed(2)}, ${y1.toFixed(2)}, ${x2.toFixed( - 2, - )}, ${y2.toFixed(2)})`; -}; - -export default async function ({ renderer, testRoot }: ExampleSettings) { - const holder = renderer.createNode({ - x: 0, - y: 0, - w: 1920, - h: 1080, - color: 0x000000ff, - parent: testRoot, - }); - - // Copy source texture from rootRenderToTextureNode - for (let i = 0; i < 50; i++) { - const dimension = getRandomValue(30, 150); - const node = renderer.createNode({ - parent: holder, - x: getRandomValue(0, 1820), - y: getRandomValue(0, 980), - w: dimension, - h: dimension, - color: getRandomColor(), - interactive: true, - zIndex: getRandomValue(0, 100), - }); - - node - .animate( - { - x: getRandomValue(0, 1820), - y: getRandomValue(0, 980), - }, - { - duration: getRandomValue(8000, 12000), - delay: getRandomValue(0, 5000), - stopMethod: 'reverse', - loop: true, - easing: getRandomBezierCurve(), - }, - ) - .start(); - } - - document.addEventListener('touchstart', (e: TouchEvent) => { - const { changedTouches } = e; - if (changedTouches.length) { - const touch = changedTouches.item(0); - - const x = touch?.clientX ?? 0; - const y = touch?.clientY ?? 0; - - const eventData: Point = { - x, - y, - }; - // const nodes: CoreNode[] = renderer.stage.findNodesAtPoint(eventData); - const topNode: CoreNode | null = - renderer.stage.getNodeFromPosition(eventData); - - if (topNode) { - topNode.scale = 1.5; - setTimeout(() => { - topNode.scale = 1; - }, 150); - } - } - }); - - document.addEventListener('mousemove', (e: MouseEvent) => { - const x = e?.clientX ?? 0; - const y = e?.clientY ?? 0; - - const eventData: Point = { - x, - y, - }; - - const topNode: CoreNode | null = - renderer.stage.getNodeFromPosition(eventData); - if (topNode) { - topNode.scale = 1.5; - setTimeout(() => { - topNode.scale = 1; - }, 150); - } - }); -} diff --git a/src/core/CoreNode.ts b/src/core/CoreNode.ts index d96ccd0..856547a 100644 --- a/src/core/CoreNode.ts +++ b/src/core/CoreNode.ts @@ -688,12 +688,6 @@ export interface CoreNodeProps { * are provided. Only works when createImageBitmap is supported on the browser. */ srcY?: number; - /** - * Mark the node as interactive so we can perform hit tests on it - * when pointer events are registered. - * @default false - */ - interactive?: boolean; /** * preventDestroy flag prevents the node and its children from being destroyed * when the parent is destroyed. @@ -882,8 +876,7 @@ export class CoreNode extends EventEmitter { // creates a fresh object with a consistent shape. Save fields that are // re-applied through setters, then null them on props so the setters // detect the change. - const { texture, shader, src, rtt, boundsMargin, interactive, parent } = - props; + const { texture, shader, src, rtt, boundsMargin, parent } = props; const p = (this.props = props); p.texture = null; p.shader = null; @@ -940,9 +933,6 @@ export class CoreNode extends EventEmitter { if (boundsMargin !== null) { this.boundsMargin = boundsMargin; } - if (interactive !== undefined) { - this.interactive = interactive; - } // Initialize autosize if enabled if (p.autosize === true) { @@ -2965,18 +2955,6 @@ export class CoreNode extends EventEmitter { return this.props.textureOptions; } - set interactive(value: boolean | undefined) { - this.props.interactive = value; - // Update Stage's interactive Set - if (value === true) { - this.stage.interactiveNodes.add(this); - } - } - - get interactive(): boolean | undefined { - return this.props.interactive; - } - get componentName(): string | undefined { return this.props.componentName; } diff --git a/src/core/Stage.ts b/src/core/Stage.ts index 016eee7..525db22 100644 --- a/src/core/Stage.ts +++ b/src/core/Stage.ts @@ -37,7 +37,6 @@ import { import { CoreRenderer } from './renderers/CoreRenderer.js'; import { CoreTextNode, type CoreTextNodeProps } from './CoreTextNode.js'; import { santizeCustomDataMap } from '../main-api/utils.js'; -import { pointInBound } from './lib/utils.js'; import type { CoreShaderNode } from './renderers/CoreShaderNode.js'; import { Matrix3d } from './lib/Matrix3d.js'; import { createBound, createPreloadBounds, type Bound } from './lib/utils.js'; @@ -71,11 +70,6 @@ export type StageFrameTickHandler = ( frameTickData: FrameTickPayload, ) => void; -export interface Point { - x: number; - y: number; -} - const autoStart = true; export class Stage { @@ -88,7 +82,6 @@ export class Stage { public readonly shManager: CoreShaderManager; public readonly renderer: CoreRenderer; public readonly root: CoreNode; - public readonly interactiveNodes: Set = new Set(); public boundsMargin: [number, number, number, number]; public readonly defShaderNode: CoreShaderNode | null = null; public strictBound: Bound; @@ -897,45 +890,6 @@ export class Stage { this.root.childUpdateType |= UpdateType.RenderBounds; } - /** Find all nodes at a given point - * @param data - */ - findNodesAtPoint(data: Point): CoreNode[] { - const x = data.x / this.options.deviceLogicalPixelRatio; - const y = data.y / this.options.deviceLogicalPixelRatio; - const nodes: CoreNode[] = []; - for (const node of this.interactiveNodes) { - if (node.isRenderable === false) { - continue; - } - if (pointInBound(x, y, node.renderBound!) === true) { - nodes.push(node); - } - } - return nodes; - } - - /** - * Find the top node at a given point - * @param data - * @returns - */ - getNodeFromPosition(data: Point): CoreNode | null { - const nodes: CoreNode[] = this.findNodesAtPoint(data); - if (nodes.length === 0) { - return null; - } - - //get last node in array (as top node) - let topNode = nodes[nodes.length - 1] as CoreNode; - for (let i = 0; i < nodes.length; i++) { - if (nodes[i]!.zIndex > topNode.zIndex) { - topNode = nodes[i]!; - } - } - return topNode || null; - } - /** * add node to timeNodes arrays * @param node @@ -1062,7 +1016,6 @@ export class Stage { rtt: props.rtt ?? false, data, imageType: props.imageType, - interactive: props.interactive ?? false, preventDestroy: props.preventDestroy, componentName: props.componentName, componentLocation: props.componentLocation, diff --git a/src/core/lib/utils.ts b/src/core/lib/utils.ts index 2954d33..0d91594 100644 --- a/src/core/lib/utils.ts +++ b/src/core/lib/utils.ts @@ -251,10 +251,6 @@ export function boundLargeThanBound(bound1: Bound, bound2: Bound) { ); } -export function pointInBound(x: number, y: number, bound: Bound) { - return !(x < bound.x1 || x > bound.x2 || y < bound.y1 || y > bound.y2); -} - export function isBoundPositive(bound: Bound): boolean { return bound.x1 < bound.x2 && bound.y1 < bound.y2; }