UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

187 lines (186 loc) 6.06 kB
import { Graph } from "../graph"; import { Component } from "../lib/Component"; import { Emitter } from "../utils/Emitter"; import { IPoint, TRect } from "../utils/types/shapes"; export interface IWithHitTest { hitBox: IHitBox; zIndex: number; setHitBox(minX: number, minY: number, maxX: number, maxY: number, force?: boolean): void; onHitBox(shape: HitBoxData): boolean; removeHitBox(): void; } export type HitBoxData = { minX: number; minY: number; maxX: number; maxY: number; x: number; y: number; }; export interface IHitBox extends HitBoxData { update(minX: number, minY: number, maxX: number, maxY: number): void; destroy(): void; getRect(): [number, number, number, number]; } /** * Hit Testing system with dual architecture for performance optimization: * * 1. interactiveTree (RBush) - for fast spatial search of elements on click/hover * 2. usableRectTracker (IncrementalBoundingBoxTracker) - for optimized calculation of overall bbox * * This solution allows: * - Avoid circular dependencies with boundary elements (affectsUsableRect: false) * - Optimize performance for different types of operations * - Efficiently handle high-frequency updates (drag operations up to 120fps) * * Performance benchmark results: * - Multiple blocks drag: +62% faster than naive approach 🏆 * - Single block drag: equal performance * - Solution to the problem of recalculating usableRect on every change */ export declare class HitTest extends Emitter { protected graph: Graph; private interactiveTree; private usableRectTracker; readonly $usableRect: import("@preact/signals-core").Signal<TRect>; protected queue: Map<HitBox, HitBoxData>; constructor(graph: Graph); /** * Check if graph has any elements (blocks or connections) * @returns true if graph has elements, false if empty */ private hasGraphElements; get isUnstable(): boolean; /** * Load array of HitBox items * @param items Array of HitBox items to load * @returns void */ load(items: HitBox[]): void; protected processQueue: (() => void) & { cancel: () => void; flush: () => void; isScheduled: () => boolean; }; /** * Update HitBox item with new bounds * @param item HitBox item to update * @param bbox New bounds data * @param _force Force update flag * @returns void */ update(item: HitBox, bbox: HitBoxData, _force?: boolean): void; /** * Clear all HitBox items and reset state */ clear(): void; /** * Add new HitBox item * @param item HitBox item to add */ add(item: HitBox): void; /** * Wait for usableRect to become stable and then call callback * @param callback Function to call when usableRect becomes stable * @returns Unsubscribe function */ waitUsableRectUpdate(callback: (rect: TRect) => void): () => void; protected updateUsableRect(): void; /** * Remove HitBox item * @param item HitBox item to remove */ remove(item: HitBox): void; /** * Test hit at specific point * @param point Point to test * @param pixelRatio Pixel ratio for coordinate conversion * @returns Array of hit components */ testPoint(point: IPoint, pixelRatio: number): Component[]; /** * Test hit box intersection with interactive elements * @param item Hit box data to test * @returns Array of hit components */ testBox(item: Omit<HitBoxData, "x" | "y">): Component[]; /** * Subscribe to usableRect updates * @param callback Function to call when usableRect changes * @returns Unsubscribe function */ onUsableRectUpdate(callback: (rect: TRect) => void): () => void; /** * Get current usableRect value * @returns Current usableRect */ getUsableRect(): TRect; destroy(): void; /** * Test hit box intersection with interactive elements and sort by z-index * @param item Hit box data to test * @returns Array of hit components sorted by z-index */ /** * Test hit box intersection with interactive elements and sort by z-index * @param item Hit box data to test * @returns Array of hit components sorted by z-index */ testHitBox(item: HitBoxData): Component[]; } export declare class HitBox implements IHitBox { item: { affectsUsableRect?: boolean; } & { zIndex: number; } & Component & IWithHitTest; protected hitTest: HitTest; destroyed: boolean; maxX: number; maxY: number; minX: number; minY: number; x: number; y: number; /** * AffectsUsableRect flag uses to determine if the element affects the usableRect * If true, the element will be added to the usableRect tracker * if false, the element wont affect the usableRect */ affectsUsableRect: boolean; private rect; protected unstable: boolean; constructor(item: { affectsUsableRect?: boolean; } & { zIndex: number; } & Component & IWithHitTest, hitTest: HitTest); /** * Update HitBox rectangle data * @param rect New rectangle data */ updateRect(rect: HitBoxData): void; /** * Update HitBox bounds * @param minX Minimum X coordinate * @param minY Minimum Y coordinate * @param maxX Maximum X coordinate * @param maxY Maximum Y coordinate * @param force Force update even if bounds haven't changed */ update: (minX: number, minY: number, maxX: number, maxY: number, force?: boolean) => void; /** * Get HitBox rectangle as array [x, y, width, height] * @returns Rectangle array [x, y, width, height] */ getRect(): [number, number, number, number]; /** * Remove HitBox from hit testing */ remove(): void; /** * Destroy HitBox and remove from hit testing */ destroy(): void; setAffectsUsableRect(affectsUsableRect: boolean): void; }