@gravity-ui/graph
Version:
Modern graph editor component
187 lines (186 loc) • 6.06 kB
TypeScript
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;
}