Skip to content

Commit ec8a02f

Browse files
committed
refactor: rename event capturing in method in GraphLayer
fix(ReactBlock): fix performance issue fix(Layer): use current devicePixelRation in Layer fix(Camera): fix tracking trackpad on pan event with browser zoom
1 parent 0f33ee5 commit ec8a02f

File tree

12 files changed

+167
-113
lines changed

12 files changed

+167
-113
lines changed

src/components/canvas/GraphComponent/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class GraphComponent<
7676
startDragCoords = currentCoords;
7777
})
7878
.on(EVENTS.DRAG_END, (_event: MouseEvent) => {
79-
this.context.graph.getGraphLayer().releaseCapturing();
79+
this.context.graph.getGraphLayer().releaseCapture();
8080
startDragCoords = undefined;
8181
onDrop?.(_event);
8282
});

src/components/canvas/blocks/controllers/BlockController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export class BlockController {
6464
dispatchEvents(selectedBlocksComponents, createCustomDragEvent(EVENTS.DRAG_UPDATE, _event));
6565
})
6666
.on(EVENTS.DRAG_END, (_event: MouseEvent) => {
67-
block.context.graph.getGraphLayer().releaseCapturing();
67+
block.context.graph.getGraphLayer().releaseCapture();
6868
dispatchEvents(selectedBlocksComponents, createCustomDragEvent(EVENTS.DRAG_END, _event));
6969
});
7070
},

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,17 @@ export class GraphLayer extends Layer<TGraphLayerProps, TGraphLayerContext> {
123123
this.onRootEvent("mousemove", this);
124124
}
125125

126+
/*
127+
* Capture element for future events
128+
* When element is captured, it will be used as target for future events
129+
* until releaseCapture is called
130+
* @param component - element to capture
131+
*/
126132
public captureEvents(component: EventedComponent) {
127133
this.capturedTargetComponent = component;
128134
}
129135

130-
public releaseCapturing() {
136+
public releaseCapture() {
131137
this.capturedTargetComponent = undefined;
132138
}
133139

@@ -194,6 +200,9 @@ export class GraphLayer extends Layer<TGraphLayerProps, TGraphLayerContext> {
194200
}
195201

196202
private updateTargetComponent(event: MouseEvent, force = false) {
203+
// Check is event is too close to previous event
204+
// In case when previous event is too close to current event, we don't need to update target component
205+
// This is useful to prevent flickering when user is moving mouse fast
197206
if (!force && this.eventByTargetComponent && getEventDelta(event, this.eventByTargetComponent) < 3) return;
198207

199208
this.eventByTargetComponent = event;
@@ -203,10 +212,6 @@ export class GraphLayer extends Layer<TGraphLayerProps, TGraphLayerContext> {
203212
return;
204213
}
205214

206-
if (this.camera.isUnstable()) {
207-
return;
208-
}
209-
210215
this.prevTargetComponent = this.targetComponent;
211216

212217
const point = this.context.graph.getPointInCameraSpace(event);

src/graph.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ 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";
2526

2627
export type LayerConfig<T extends Constructor<Layer> = Constructor<Layer>> = [
2728
T,
@@ -118,14 +119,17 @@ export class Graph {
118119
this.setupGraph(config);
119120
}
120121

121-
public schedule(cb: () => void) {
122-
const scheduler = {
123-
performUpdate: () => {
122+
public scheduleTask(cb: () => void) {
123+
schedule(
124+
() => {
124125
cb();
125-
this.scheduler.removeScheduler(scheduler, ESchedulerPriority.LOWEST);
126126
},
127-
};
128-
this.scheduler.addScheduler(scheduler, ESchedulerPriority.LOWEST);
127+
{
128+
priority: ESchedulerPriority.LOWEST,
129+
frameInterval: 10,
130+
once: true,
131+
}
132+
);
129133
}
130134

131135
public getGraphLayer() {
@@ -157,7 +161,8 @@ export class Graph {
157161
}
158162

159163
public zoomTo(target: TGraphZoomTarget, config?: ZoomConfig) {
160-
this.schedule(() => {
164+
this.scheduleTask(() => {
165+
console.log("zoomTo", target, config);
161166
if (target === "center") {
162167
this.api.zoomToViewPort(config);
163168
return;

src/react-components/Block.css

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
top: 0;
55
left: 0;
66
box-sizing: content-box;
7-
transform: translate3d(var(--graph-block-geometry-x, 0), var(--graph-block-geometry-y, 0), 0);
8-
will-change: transform, width, height;
9-
width: var(--graph-block-geometry-width, 0);
10-
height: var(--graph-block-geometry-height, 0);
11-
--z-index: calc(var(--graph-block-z-index, 0) + var(--graph-block-order, 0));
12-
z-index: var(--z-index);
7+
8+
will-change: transform;
9+
contain: layout size;
10+
isolation: isolate;
11+
12+
/* Создаёт композитный слой */
13+
backface-visibility: hidden;
14+
transform-style: preserve-3d;
1315
}
1416

1517
.graph-block-wrapper {

src/react-components/Block.tsx

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React, { useEffect, useLayoutEffect, useRef } from "react";
22

33
import { TBlock } from "../components/canvas/blocks/Block";
44
import { Graph } from "../graph";
5-
import { setCssProps } from "../utils/functions/cssProp";
65

76
import { useBlockState, useBlockViewState } from "./hooks/useBlockState";
87

@@ -22,6 +21,7 @@ export const GraphBlock = <T extends TBlock>({
2221
containerClassName?: string;
2322
}) => {
2423
const containerRef = useRef<HTMLDivElement>(null);
24+
const lastStateRef = useRef({ x: 0, y: 0, width: 0, height: 0, zIndex: 0 });
2525
const viewState = useBlockViewState(graph, block);
2626
const state = useBlockState(graph, block);
2727

@@ -30,33 +30,56 @@ export const GraphBlock = <T extends TBlock>({
3030
return () => viewState?.setHiddenBlock(false);
3131
}, [viewState]);
3232

33-
useLayoutEffect(() => {
34-
setCssProps(containerRef.current, {
35-
"--graph-block-geometry-x": `${state.x}px`,
36-
"--graph-block-geometry-y": `${state.y}px`,
37-
"--graph-block-geometry-width": `${state.width}px`,
38-
"--graph-block-geometry-height": `${state.height}px`,
39-
});
40-
}, [state.x, state.y, state.width, state.height, containerRef]);
33+
// Оптимизированные обновления только при реальных изменениях
34+
useEffect(() => {
35+
if (!containerRef.current || !state) return;
36+
37+
const element = containerRef.current;
38+
const lastState = lastStateRef.current;
39+
40+
// Проверяем, что действительно изменилось
41+
const hasPositionChange = lastState.x !== state.x || lastState.y !== state.y;
42+
const hasSizeChange = lastState.width !== state.width || lastState.height !== state.height;
43+
44+
if (hasPositionChange) {
45+
// Используем transform для позиции - самый быстрый способ
46+
element.style.transform = `translate3d(${state.x}px, ${state.y}px, 0)`;
47+
lastState.x = state.x;
48+
lastState.y = state.y;
49+
}
50+
51+
if (hasSizeChange) {
52+
// Размеры устанавливаем напрямую
53+
element.style.width = `${state.width}px`;
54+
element.style.height = `${state.height}px`;
55+
lastState.width = state.width;
56+
lastState.height = state.height;
57+
}
58+
}, [state?.x, state?.y, state?.width, state?.height]);
4159

4260
useEffect(() => {
43-
if (viewState) {
61+
if (viewState && containerRef.current) {
4462
return viewState.$viewState.subscribe(({ zIndex, order }) => {
45-
setCssProps(containerRef.current, {
46-
"--graph-block-z-index": `${zIndex}`,
47-
"--graph-block-order": `${order}`,
48-
});
63+
const element = containerRef.current;
64+
const lastState = lastStateRef.current;
65+
const newZIndex = (zIndex || 0) + (order || 0);
66+
67+
if (element && lastState.zIndex !== newZIndex) {
68+
element.style.zIndex = `${newZIndex}`;
69+
lastState.zIndex = newZIndex;
70+
}
4971
});
5072
}
51-
}, [viewState, containerRef]);
73+
return undefined;
74+
}, [viewState]);
5275

5376
if (!viewState || !state) {
54-
return;
77+
return null;
5578
}
5679

5780
return (
58-
<div className={`graph-block-container ${containerClassName}`} ref={containerRef}>
59-
<div className={`graph-block-wrapper ${className} ${state.selected ? "selected" : ""}`}>{children}</div>
81+
<div className={`graph-block-container ${containerClassName || ""}`} ref={containerRef}>
82+
<div className={`graph-block-wrapper ${className || ""} ${state.selected ? "selected" : ""}`}>{children}</div>
6083
</div>
6184
);
6285
};

src/services/HitTest.ts

Lines changed: 45 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ESchedulerPriority, scheduler } from "../lib";
55
import { Component } from "../lib/Component";
66
import { Emitter } from "../utils/Emitter";
77
import { IPoint, TRect } from "../utils/types/shapes";
8+
import { schedule } from "../utils/utils/schedule";
89

910
export interface IWithHitTest {
1011
hitBox: IHitBox;
@@ -49,62 +50,55 @@ export class HitTest extends Emitter {
4950
this.tree.load(items);
5051
}
5152

52-
protected loadItemsScheduler = {
53-
performUpdate: () => {
54-
if (this.scheduledItems.size === 0) return;
55-
const items = Array.from(this.scheduledItems);
56-
items.forEach(([item, bbox]) => {
57-
this.tree.remove(item);
58-
item.updateRect(bbox);
59-
});
60-
const boxes = Array.from(this.scheduledItems.keys());
61-
this.tree.load(boxes);
62-
this.scheduledItems.clear();
63-
64-
const { minX, minY, maxX, maxY } = this.getBBox(boxes);
65-
if (
66-
minX !== this.usableRect.value.x ||
67-
minY !== this.usableRect.value.y ||
68-
maxX !== this.usableRect.value.width ||
69-
maxY !== this.usableRect.value.height
70-
) {
71-
this.updateUsableRect();
72-
}
73-
this.emitUpdate();
74-
},
75-
};
76-
77-
protected removeItemsScheduler = {
78-
performUpdate: () => {
79-
if (this.scheduledRemoveItems.size === 0) return;
80-
this.scheduledRemoveItems.forEach((item) => {
81-
this.tree.remove(item);
82-
});
83-
this.scheduledRemoveItems.clear();
84-
this.updateUsableRect();
85-
},
86-
};
87-
88-
protected emitUpdateScheduler = {
89-
performUpdate: () => {
90-
if (this.needEmitUpdate) {
91-
this.emit("update", this);
92-
this.needEmitUpdate = false;
93-
}
94-
},
95-
};
96-
9753
protected removeSchedulersFns: (() => void)[] = [];
9854

9955
constructor() {
10056
super();
10157
this.removeSchedulersFns = [
102-
// Schedule loading items to tree and recompute usableRect
103-
scheduler.addScheduler(this.loadItemsScheduler, ESchedulerPriority.LOWEST),
104-
scheduler.addScheduler(this.removeItemsScheduler, ESchedulerPriority.LOWEST),
105-
106-
// Recompute usableRect and emit update event
107-
scheduler.addScheduler(this.emitUpdateScheduler, ESchedulerPriority.MEDIUM),
58+
schedule(
59+
() => {
60+
let needUpdateUsableRect = false;
61+
let needEmitUpdate = false;
62+
if (this.scheduledRemoveItems.size > 0) {
63+
needEmitUpdate = true;
64+
this.scheduledRemoveItems.forEach((item) => {
65+
this.tree.remove(item);
66+
});
67+
this.scheduledRemoveItems.clear();
68+
}
69+
if (this.scheduledItems.size > 0) {
70+
needEmitUpdate = true;
71+
const items = Array.from(this.scheduledItems);
72+
items.forEach(([item, bbox]) => {
73+
this.tree.remove(item);
74+
item.updateRect(bbox);
75+
});
76+
const boxes = Array.from(this.scheduledItems.keys());
77+
this.tree.load(boxes);
78+
this.scheduledItems.clear();
79+
80+
const { minX, minY, maxX, maxY } = this.getBBox(boxes);
81+
if (
82+
minX !== this.usableRect.value.x ||
83+
minY !== this.usableRect.value.y ||
84+
maxX !== this.usableRect.value.width ||
85+
maxY !== this.usableRect.value.height
86+
) {
87+
needUpdateUsableRect = true;
88+
}
89+
}
90+
if (needUpdateUsableRect) {
91+
this.updateUsableRect();
92+
}
93+
if (needEmitUpdate) {
94+
this.emit("update", this);
95+
}
96+
},
97+
{
98+
priority: ESchedulerPriority.LOWEST,
99+
frameInterval: 10,
100+
}
101+
),
108102
];
109103
}
110104

src/services/Layer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export class Layer<
190190

191191
public updateSize(width: number, height: number) {
192192
if (this.canvas) {
193-
const dpr = this.props.canvas.respectPixelRatio === false ? 1 : this.context.constants.system?.PIXEL_RATIO;
193+
const dpr = this.props.canvas.respectPixelRatio === false ? 1 : devicePixelRatio;
194194
this.canvas.width = width * dpr;
195195
this.canvas.height = height * dpr;
196196
}
@@ -341,7 +341,7 @@ export class Layer<
341341

342342
public getDRP() {
343343
const respectPixelRatio = this.props.canvas?.respectPixelRatio ?? true;
344-
return respectPixelRatio ? this.context.constants.system?.PIXEL_RATIO ?? 1 : 1;
344+
return respectPixelRatio ? devicePixelRatio : 1;
345345
}
346346

347347
protected applyTransform(

src/services/LayersService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class Layers extends Emitter {
1414

1515
constructor(public $root?: HTMLDivElement) {
1616
super();
17+
window.addEventListener("resize", this.handleRootResize);
1718
}
1819

1920
public createLayer<T extends Constructor<Layer> = Constructor<Layer>>(
@@ -86,6 +87,7 @@ export class Layers extends Emitter {
8687
public unmount() {
8788
this.detach(true);
8889
this.destroy();
90+
window.removeEventListener("resize", this.handleRootResize);
8991
}
9092

9193
protected resizeObserver = new ResizeObserver(() => {

src/services/camera/CameraService.ts

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import intersects from "intersects";
2-
import debounce from "lodash/debounce";
32

43
import { Graph } from "../../graph";
54
import { Emitter } from "../../utils/Emitter";
@@ -57,22 +56,6 @@ export const getInitCameraState = (): TCameraState => {
5756
export type ICamera = Interface<CameraService>;
5857

5958
export class CameraService extends Emitter {
60-
/* State for unstable state of camera viewport, like moving viewport or zoom */
61-
protected unstable = false;
62-
63-
public isUnstable() {
64-
return this.unstable;
65-
}
66-
67-
protected makeUnstable() {
68-
this.unstable = true;
69-
this.unsetUnstable();
70-
}
71-
72-
protected unsetUnstable = debounce(() => {
73-
this.unstable = false;
74-
}, 50);
75-
7659
constructor(
7760
protected graph: Graph,
7861
protected state: TCameraState = getInitCameraState()
@@ -91,7 +74,6 @@ export class CameraService extends Emitter {
9174
this.graph.executеDefaultEventAction("camera-change", Object.assign({}, this.state, newState), () => {
9275
this.state = Object.assign(this.state, newState);
9376
this.updateRelative();
94-
this.makeUnstable();
9577
});
9678
}
9779

0 commit comments

Comments
 (0)