Skip to content

Commit 4358b23

Browse files
committed
refactor: enhance graph layer visibility management; implement camera viewport threshold and improve hit test stability checks
1 parent a8ea93e commit 4358b23

File tree

13 files changed

+102
-57
lines changed

13 files changed

+102
-57
lines changed

docs/system/graph-settings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ Constants control the sizing, spacing, and other numerical values used throughou
140140
| Constant | Default | Description |
141141
|----------|---------|-------------|
142142
| `GRID_SIZE` | `16` | Base grid size for layout calculations |
143-
| `PIXEL_RATIO` | `window.devicePixelRatio \|\| 1` | Device pixel ratio for rendering |
144143
| `USABLE_RECT_GAP` | `400` | Gap around the usable rect area |
144+
| `CAMERA_VIEWPORT_TRESHOLD` | `0.5` | Threshold for camera viewport |
145145

146146
### Camera Constants
147147

src/api/PublicGraphApi.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,23 @@ export class PublicGraphApi {
3939
return new Promise((resolve) => {
4040
const currentRect = this.getUsableRect();
4141

42-
// Check if usableRect is ready (not empty default state)
43-
if (currentRect.width > 0 || currentRect.height > 0 || currentRect.x !== 0 || currentRect.y !== 0) {
44-
this.zoomToRect(currentRect, zoomConfig);
45-
resolve();
42+
if (this.graph.hitTest.isUnstable) {
43+
const unsubscribe = this.graph.hitTest.onUsableRectUpdate((usableRect) => {
44+
if (this.graph.hitTest.isUnstable) {
45+
return;
46+
}
47+
48+
this.zoomToRect(usableRect, zoomConfig);
49+
resolve();
50+
setTimeout(() => {
51+
unsubscribe();
52+
}, 0);
53+
});
4654
return;
4755
}
4856

49-
// Wait for usableRect to become ready
50-
const unsubscribe = this.graph.hitTest.onUsableRectUpdate((usableRect) => {
51-
if (usableRect.height === 0 && usableRect.width === 0 && usableRect.x === 0 && usableRect.y === 0) {
52-
return;
53-
}
54-
55-
this.zoomToRect(usableRect, zoomConfig);
56-
unsubscribe();
57-
resolve();
58-
});
57+
this.zoomToRect(currentRect, zoomConfig);
58+
resolve();
5959
});
6060
}
6161

src/components/canvas/connections/BaseConnection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,13 @@ export class BaseConnection<
9696
this.connectionPoints[1].x,
9797
this.anchorsPoints?.[0].x || Infinity,
9898
this.anchorsPoints?.[1].x || Infinity,
99-
];
99+
].filter(Number.isFinite);
100100
const y = [
101101
this.connectionPoints[0].y,
102102
this.connectionPoints[1].y,
103103
this.anchorsPoints?.[0].y || Infinity,
104104
this.anchorsPoints?.[1].y || Infinity,
105-
];
105+
].filter(Number.isFinite);
106106

107107
this.bBox = [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)];
108108

src/components/canvas/connections/BatchPath2D/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,16 @@ export class BatchPath2DRenderer {
145145
}, [] satisfies Path2DGroup[]);
146146
});
147147

148-
protected requestRender = debounce(
148+
protected requestRender = () => {
149+
this.onChange?.();
150+
}; /* debounce(
149151
() => {
150152
this.onChange?.();
151153
},
152154
{
153155
priority: ESchedulerPriority.HIGHEST,
154156
}
155-
);
157+
); */
156158

157159
protected getGroup(zIndex: number, group: string) {
158160
if (!this.indexes.has(zIndex)) {

src/components/canvas/layers/graphLayer/GraphLayer.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ export class GraphLayer extends Layer<TGraphLayerProps, TGraphLayerContext> {
9595
this.camera = this.props.camera;
9696

9797
this.performRender = this.performRender.bind(this);
98-
99-
canvas.style.visibility = "hidden";
10098
}
10199

102100
protected afterInit(): void {
@@ -107,16 +105,6 @@ export class GraphLayer extends Layer<TGraphLayerProps, TGraphLayerContext> {
107105

108106
// Subscribe to graph events here instead of in the constructor
109107
this.onGraphEvent("camera-change", this.performRender);
110-
schedule(
111-
() => {
112-
this.getCanvas().style.visibility = "visible";
113-
},
114-
{
115-
priority: ESchedulerPriority.LOWEST,
116-
frameInterval: 15,
117-
once: true,
118-
}
119-
);
120108
super.afterInit();
121109
}
122110

src/components/canvas/layers/selectionLayer/SelectionLayer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class SelectionLayer extends Layer<
8181
this.selection.y,
8282
this.selection.width,
8383
this.selection.height,
84-
Number(window.devicePixelRatio)
84+
Number(this.context.graph.layers.getDPR())
8585
);
8686
ctx.closePath();
8787

src/graph.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { GraphLayer } from "./components/canvas/layers/graphLayer/GraphLayer";
99
import { SelectionLayer } from "./components/canvas/layers/selectionLayer/SelectionLayer";
1010
import { TGraphColors, TGraphConstants, initGraphColors, initGraphConstants } from "./graphConfig";
1111
import { GraphEventParams, GraphEventsDefinitions } from "./graphEvents";
12-
import { ESchedulerPriority, scheduler } from "./lib/Scheduler";
12+
import { scheduler } from "./lib/Scheduler";
1313
import { HitTest } from "./services/HitTest";
1414
import { Layer } from "./services/Layer";
1515
import { Layers } from "./services/LayersService";
@@ -22,7 +22,6 @@ import { getXY } from "./utils/functions";
2222
import { clearTextCache } from "./utils/renderers/text";
2323
import { RecursivePartial } from "./utils/types/helpers";
2424
import { IPoint, IRect, Point, TPoint, TRect, isTRect } from "./utils/types/shapes";
25-
import { schedule } from "./utils/utils/schedule";
2625

2726
export type LayerConfig<T extends Constructor<Layer> = Constructor<Layer>> = [
2827
T,
@@ -102,6 +101,10 @@ export class Graph {
102101
this.graphLayer = this.addLayer(GraphLayer, {});
103102
this.selectionLayer = this.addLayer(SelectionLayer, {});
104103

104+
this.selectionLayer.hide();
105+
this.graphLayer.hide();
106+
this.belowLayer.hide();
107+
105108
if (rootEl) {
106109
this.attach(rootEl);
107110
}
@@ -119,19 +122,6 @@ export class Graph {
119122
this.setupGraph(config);
120123
}
121124

122-
public scheduleTask(cb: () => void) {
123-
schedule(
124-
() => {
125-
cb();
126-
},
127-
{
128-
priority: ESchedulerPriority.LOWEST,
129-
frameInterval: 10,
130-
once: true,
131-
}
132-
);
133-
}
134-
135125
public getGraphLayer() {
136126
return this.graphLayer;
137127
}
@@ -173,7 +163,7 @@ export class Graph {
173163
}
174164

175165
public getElementsOverPoint<T extends Constructor<GraphComponent>>(point: IPoint, filter?: T[]): InstanceType<T>[] {
176-
const items = this.hitTest.testPoint(point, this.graphConstants.system.PIXEL_RATIO);
166+
const items = this.hitTest.testPoint(point, this.layers.getDPR());
177167
if (filter && items.length > 0) {
178168
return items.filter((item) => filter.some((Component) => item instanceof Component)) as InstanceType<T>[];
179169
}
@@ -362,6 +352,31 @@ export class Graph {
362352
this.layers.start();
363353
this.scheduler.start();
364354
this.setGraphState(GraphState.READY);
355+
this.runAfterGraphReady(() => {
356+
this.selectionLayer.show();
357+
this.graphLayer.show();
358+
this.belowLayer.show();
359+
});
360+
}
361+
362+
/**
363+
* Graph is ready when the hitboxes are stable.
364+
* In order to initialize hitboxes we need to start scheduler and wait untils every component registered in hitTest service
365+
* Immediatelly after registering startign a rendering process.
366+
* @param cb - Callback to run after graph is ready
367+
*/
368+
public runAfterGraphReady(cb: () => void) {
369+
if (this.hitTest.isUnstable) {
370+
this.hitTest.on("update", () => {
371+
if (this.hitTest.isUnstable) {
372+
this.runAfterGraphReady(cb);
373+
return;
374+
}
375+
cb();
376+
});
377+
} else {
378+
cb();
379+
}
365380
}
366381

367382
public stop(full = false) {

src/graphConfig.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export const initGraphColors: TGraphColors = {
8383
export type TGraphConstants = {
8484
system: {
8585
GRID_SIZE: number;
86+
/* @deprecated this config is not used anymore, Layers checks devicePixelRatio internally */
8687
PIXEL_RATIO: number;
8788
USABLE_RECT_GAP: number;
8889
/** For preload blocks on the html layer (camera dimensions * (1 + this value)) */
@@ -131,6 +132,7 @@ export type TGraphConstants = {
131132
export const initGraphConstants: TGraphConstants = {
132133
system: {
133134
GRID_SIZE: 16,
135+
/* @deprecated this config is not used anymore, Layers checks devicePixelRatio internally */
134136
PIXEL_RATIO: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
135137
USABLE_RECT_GAP: 400,
136138
CAMERA_VIEWPORT_TRESHOLD: 0.5,
@@ -157,7 +159,7 @@ export const initGraphConstants: TGraphConstants = {
157159
DEFAULT_Z_INDEX: 0,
158160
THRESHOLD_LINE_HIT: 8,
159161
MIN_ZOOM_FOR_CONNECTION_ARROW_AND_LABEL: 0.25,
160-
PATH2D_CHUNK_SIZE: 9,
162+
PATH2D_CHUNK_SIZE: 100,
161163
LABEL: {
162164
INNER_PADDINGS: [0, 0, 0, 0],
163165
},

src/services/HitTest.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export class HitTest extends Emitter {
4040
// Single queue replaces all complex state tracking
4141
protected queue = new Map<HitBox, HitBoxData | null>();
4242

43+
public get isUnstable() {
44+
return (
45+
this.queue.size > 0 ||
46+
(this.$usableRect.value.height === 0 &&
47+
this.$usableRect.value.width === 0 &&
48+
this.$usableRect.value.x === 0 &&
49+
this.$usableRect.value.y === 0)
50+
);
51+
}
52+
4353
public load(items: HitBox[]) {
4454
this.tree.load(items);
4555
}
@@ -74,9 +84,12 @@ export class HitTest extends Emitter {
7484
}
7585
);
7686

77-
public update(item: HitBox, bbox: HitBoxData) {
87+
public update(item: HitBox, bbox: HitBoxData, force = false) {
7888
this.queue.set(item, bbox);
7989
this.processQueue();
90+
if (force) {
91+
this.processQueue.flush();
92+
}
8093
}
8194

8295
public clear() {
@@ -118,12 +131,12 @@ export class HitTest extends Emitter {
118131
}
119132

120133
protected updateUsableRect() {
121-
const { minX, minY, maxX, maxY } = this.getBBox(this.tree.all());
134+
const rect = this.tree.toJSON();
122135
this.$usableRect.value = {
123-
x: minX,
124-
y: minY,
125-
width: maxX - minX,
126-
height: maxY - minY,
136+
x: rect.minX,
137+
y: rect.minY,
138+
width: rect.maxX - rect.minX,
139+
height: rect.maxY - rect.minY,
127140
};
128141
}
129142

@@ -227,7 +240,7 @@ export class HitBox implements IHitBox {
227240
public update = (minX: number, minY: number, maxX: number, maxY: number, force?: boolean): void => {
228241
if (this.destroyed) return;
229242
if (minX === this.minX && minY === this.minY && maxX === this.maxX && maxY === this.maxY && !force) return;
230-
this.hitTest.update(this, { minX, minY, maxX, maxY, x: this.x, y: this.y });
243+
this.hitTest.update(this, { minX, minY, maxX, maxY, x: this.x, y: this.y }, force);
231244
};
232245

233246
public getRect(): [number, number, number, number] {

src/services/Layer.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,8 @@
2525
transform-origin: 0 0;
2626
transform-style: preserve-3d;
2727
will-change: transform;
28+
}
29+
30+
.hidden {
31+
visibility: hidden;
2832
}

0 commit comments

Comments
 (0)