Skip to content

Commit a8ea93e

Browse files
committed
refactor: optimize zoomToViewPort method to be asynchronous and improve usability; add PATH2D_CHUNK_SIZE constant for batch rendering; update Background component to use debounced state updates
1 parent 0da1dc7 commit a8ea93e

File tree

19 files changed

+561
-223
lines changed

19 files changed

+561
-223
lines changed

src/api/PublicGraphApi.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,35 @@ export class PublicGraphApi {
2828
this.zoomToRect(blocksRect, zoomConfig);
2929
}
3030

31-
public zoomToViewPort(zoomConfig?: ZoomConfig) {
32-
const blocksRect = this.getUsableRect();
33-
34-
this.zoomToRect(blocksRect, zoomConfig);
31+
/**
32+
* Zooms to fit all blocks in the viewport. This method is asynchronous and waits
33+
* for the usableRect to be ready before performing the zoom operation.
34+
*
35+
* @param zoomConfig - Configuration for zoom transition and padding
36+
* @returns Promise that resolves when zoom operation is complete
37+
*/
38+
public zoomToViewPort(zoomConfig?: ZoomConfig): Promise<void> {
39+
return new Promise((resolve) => {
40+
const currentRect = this.getUsableRect();
41+
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();
46+
return;
47+
}
48+
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+
});
59+
});
3560
}
3661

3762
public zoomToRect(rect: TRect, zoomConfig?: ZoomConfig) {
@@ -165,7 +190,7 @@ export class PublicGraphApi {
165190
}
166191

167192
public getUsableRect() {
168-
return this.graph.hitTest.usableRect.value;
193+
return this.graph.hitTest.getUsableRect();
169194
}
170195

171196
public unsetSelection() {

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

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import debounce from "lodash/debounce";
2-
1+
import { ESchedulerPriority } from "../../../../lib";
32
import { cache } from "../../../../lib/utils";
3+
import { debounce } from "../../../../utils/functions";
44

55
export type Path2DRenderStyleResult =
66
| { type: "stroke" }
@@ -14,8 +14,6 @@ export interface Path2DRenderInstance {
1414
isPathVisible?(): boolean;
1515
}
1616

17-
const CHUNK_SIZE = 100;
18-
1917
class Path2DChunk {
2018
protected items: Set<Path2DRenderInstance> = new Set();
2119

@@ -26,20 +24,20 @@ class Path2DChunk {
2624
protected path = cache(() => {
2725
const path = new Path2D();
2826
path.moveTo(0, 0);
29-
return this.visibleItems.get().reduce((path, item) => {
30-
if (item.isPathVisible?.() ?? true) {
31-
const subPath = item.getPath();
32-
if (subPath) {
33-
path.addPath(subPath);
34-
}
27+
// Use already filtered visibleItems - no need for additional visibility checks
28+
for (const item of this.visibleItems.get()) {
29+
const subPath = item.getPath();
30+
if (subPath) {
31+
path.addPath(subPath);
3532
}
36-
return path;
37-
}, path);
33+
}
34+
return path;
3835
});
3936

40-
protected applyStyles(ctx) {
41-
const val = Array.from(this.items).find((item) => item.isPathVisible?.() ?? true);
42-
return val?.style(ctx);
37+
protected applyStyles(ctx: CanvasRenderingContext2D) {
38+
// Style comes from first visible item
39+
const first = this.visibleItems.get()[0];
40+
return first?.style(ctx);
4341
}
4442

4543
public add(item: Path2DRenderInstance) {
@@ -58,33 +56,25 @@ class Path2DChunk {
5856
}
5957

6058
public render(ctx: CanvasRenderingContext2D) {
61-
const visibleItems = this.visibleItems.get();
62-
63-
if (visibleItems.length) {
64-
ctx.save();
65-
66-
const result = this.applyStyles(ctx);
67-
if (result) {
68-
switch (result.type) {
69-
case "fill": {
70-
ctx.fill(this.path.get(), result.fillRule);
71-
break;
72-
}
73-
case "stroke": {
74-
ctx.stroke(this.path.get());
75-
break;
76-
}
77-
case "both": {
78-
ctx.fill(this.path.get(), result.fillRule);
79-
ctx.stroke(this.path.get());
80-
}
81-
}
59+
const vis = this.visibleItems.get();
60+
if (!vis.length) return;
61+
62+
ctx.save();
63+
const style = this.applyStyles(ctx);
64+
if (style) {
65+
const p = this.path.get();
66+
if (style.type === "fill" || style.type === "both") {
67+
ctx.fill(p, style.fillRule);
8268
}
83-
ctx.restore();
84-
for (const item of visibleItems) {
85-
item.afterRender?.(ctx);
69+
if (style.type === "stroke" || style.type === "both") {
70+
ctx.stroke(p);
8671
}
8772
}
73+
ctx.restore();
74+
75+
for (const item of vis) {
76+
item.afterRender?.(ctx);
77+
}
8878
}
8979

9080
public get size() {
@@ -93,12 +83,16 @@ class Path2DChunk {
9383
}
9484

9585
class Path2DGroup {
96-
protected chunks: Path2DChunk[] = [new Path2DChunk()];
86+
protected chunks: Path2DChunk[] = [];
9787
protected itemToChunk: Map<Path2DRenderInstance, Path2DChunk> = new Map();
9888

89+
constructor(private chunkSize: number) {
90+
this.chunks.push(new Path2DChunk());
91+
}
92+
9993
public add(item: Path2DRenderInstance) {
10094
let lastChunk = this.chunks[this.chunks.length - 1];
101-
if (lastChunk.size >= CHUNK_SIZE) {
95+
if (lastChunk.size >= this.chunkSize) {
10296
lastChunk = new Path2DChunk();
10397
this.chunks.push(lastChunk);
10498
}
@@ -133,7 +127,10 @@ class Path2DGroup {
133127
}
134128

135129
export class BatchPath2DRenderer {
136-
constructor(protected onChange: () => void) {}
130+
constructor(
131+
protected onChange: () => void,
132+
private chunkSize: number = 100
133+
) {}
137134

138135
protected indexes: Map<number, Map<string, Path2DGroup>> = new Map();
139136

@@ -148,14 +145,23 @@ export class BatchPath2DRenderer {
148145
}, [] satisfies Path2DGroup[]);
149146
});
150147

148+
protected requestRender = debounce(
149+
() => {
150+
this.onChange?.();
151+
},
152+
{
153+
priority: ESchedulerPriority.HIGHEST,
154+
}
155+
);
156+
151157
protected getGroup(zIndex: number, group: string) {
152158
if (!this.indexes.has(zIndex)) {
153159
this.indexes.set(zIndex, new Map());
154160
}
155161
const index = this.indexes.get(zIndex);
156162

157163
if (!index.has(group)) {
158-
index.set(group, new Path2DGroup());
164+
index.set(group, new Path2DGroup(this.chunkSize));
159165
}
160166

161167
return index.get(group);
@@ -169,7 +175,7 @@ export class BatchPath2DRenderer {
169175
bucket.add(item);
170176
this.itemParams.set(item, params);
171177
this.orderedPaths.reset();
172-
this.onChange?.();
178+
this.requestRender();
173179
}
174180

175181
public update(item: Path2DRenderInstance, params: { zIndex: number; group: string }) {
@@ -186,15 +192,15 @@ export class BatchPath2DRenderer {
186192
bucket.delete(item);
187193
this.itemParams.delete(item);
188194
this.orderedPaths.reset();
189-
this.onChange?.();
195+
this.requestRender();
190196
}
191197

192198
public markDirty(item: Path2DRenderInstance) {
193199
const params = this.itemParams.get(item);
194200
if (params) {
195201
const group = this.getGroup(params.zIndex, params.group);
196202
group.resetItem(item);
197-
this.onChange?.();
203+
this.requestRender();
198204
}
199205
}
200206
}

src/components/canvas/connections/BlockConnections.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ export class BlockConnections extends Component<CoreComponentProps, TComponentSt
1818

1919
protected readonly unsubscribe: (() => void)[];
2020

21-
protected batch = new BatchPath2DRenderer(() => this.performRender());
21+
protected batch: BatchPath2DRenderer;
2222

2323
constructor(props: {}, parent: Component) {
2424
super(props, parent);
25+
this.batch = new BatchPath2DRenderer(
26+
() => this.performRender(),
27+
this.context.constants.connection.PATH2D_CHUNK_SIZE || 100
28+
);
2529
this.unsubscribe = this.subscribe();
2630
this.setContext({
2731
batch: this.batch,

src/components/canvas/layers/belowLayer/Background.ts

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1+
import { ESchedulerPriority } from "../../../../lib";
12
import { Component, TComponentProps, TComponentState } from "../../../../lib/Component";
3+
import { debounce } from "../../../../utils/functions";
24
import { IRect, Rect, TRect } from "../../../../utils/types/shapes";
35

46
import { TBelowLayerContext } from "./BelowLayer";
57
import { PointerGrid } from "./PointerGrid";
68

7-
type TBackgroundState = TComponentState & {
8-
extendedUsableRectX: number;
9-
extendedUsableRectY: number;
10-
extendedUsableRectWidth: number;
11-
extendedUsableRectHeight: number;
12-
};
9+
type TBackgroundState = TComponentState & TRect;
1310

1411
export class Background extends Component<TComponentProps, TBackgroundState, TBelowLayerContext> {
1512
private extendedUsableRect: IRect = new Rect(0, 0, 0, 0);
@@ -34,73 +31,67 @@ export class Background extends Component<TComponentProps, TBackgroundState, TBe
3431

3532
this.context.ctx.lineWidth = Math.floor(3 / cameraState.scale);
3633
this.context.ctx.strokeStyle = this.context.colors.canvas.border;
37-
this.context.ctx.strokeRect(
38-
this.state.extendedUsableRectX,
39-
this.state.extendedUsableRectY,
40-
this.state.extendedUsableRectWidth,
41-
this.state.extendedUsableRectHeight
42-
);
34+
this.context.ctx.strokeRect(this.state.x, this.state.y, this.state.width, this.state.height);
4335

4436
this.context.ctx.fillStyle = this.context.colors.canvas.layerBackground;
4537

46-
this.context.ctx.fillRect(
47-
this.state.extendedUsableRectX,
48-
this.state.extendedUsableRectY,
49-
this.state.extendedUsableRectWidth,
50-
this.state.extendedUsableRectHeight
51-
);
38+
this.context.ctx.fillRect(this.state.x, this.state.y, this.state.width, this.state.height);
5239

5340
this.context.ctx.fillStyle = "black";
5441
this.context.ctx.font = "48px serif";
5542
return;
5643
}
5744

5845
protected subscribe() {
59-
return this.context.graph.hitTest.usableRect.subscribe((usableRect) => {
60-
this.setupExtendedUsableRect(usableRect);
61-
});
46+
return this.context.graph.hitTest.onUsableRectUpdate(this.setupExtendedUsableRect);
6247
}
6348

6449
protected stateChanged(_nextState: TBackgroundState): void {
6550
this.shouldUpdateChildren = true;
6651
super.stateChanged(_nextState);
6752
}
6853

69-
private setupExtendedUsableRect(usableRect: TRect) {
70-
if (usableRect.x - this.context.constants.system.USABLE_RECT_GAP !== this.extendedUsableRect.x) {
71-
this.setState({
72-
extendedUsableRectX: usableRect.x - this.context.constants.system.USABLE_RECT_GAP,
73-
});
74-
}
75-
if (usableRect.y - this.context.constants.system.USABLE_RECT_GAP !== this.extendedUsableRect.y) {
76-
this.setState({
77-
extendedUsableRectY: usableRect.y - this.context.constants.system.USABLE_RECT_GAP,
78-
});
54+
private setupExtendedUsableRect = debounce(
55+
(usableRect: TRect) => {
56+
if (usableRect.x - this.context.constants.system.USABLE_RECT_GAP !== this.extendedUsableRect.x) {
57+
this.setState({
58+
x: usableRect.x - this.context.constants.system.USABLE_RECT_GAP,
59+
});
60+
}
61+
if (usableRect.y - this.context.constants.system.USABLE_RECT_GAP !== this.extendedUsableRect.y) {
62+
this.setState({
63+
y: usableRect.y - this.context.constants.system.USABLE_RECT_GAP,
64+
});
65+
}
66+
if (usableRect.width + this.context.constants.system.USABLE_RECT_GAP * 2 !== this.extendedUsableRect.width) {
67+
this.setState({
68+
width: usableRect.width + this.context.constants.system.USABLE_RECT_GAP * 2,
69+
});
70+
}
71+
if (usableRect.height + this.context.constants.system.USABLE_RECT_GAP * 2 !== this.extendedUsableRect.height) {
72+
this.setState({
73+
height: usableRect.height + this.context.constants.system.USABLE_RECT_GAP * 2,
74+
});
75+
}
76+
},
77+
{
78+
priority: ESchedulerPriority.HIGHEST,
7979
}
80-
if (usableRect.width + this.context.constants.system.USABLE_RECT_GAP * 2 !== this.extendedUsableRect.width) {
81-
this.setState({
82-
extendedUsableRectWidth: usableRect.width + this.context.constants.system.USABLE_RECT_GAP * 2,
83-
});
84-
}
85-
if (usableRect.height + this.context.constants.system.USABLE_RECT_GAP * 2 !== this.extendedUsableRect.height) {
86-
this.setState({
87-
extendedUsableRectHeight: usableRect.height + this.context.constants.system.USABLE_RECT_GAP * 2,
88-
});
89-
}
90-
}
80+
);
9181

9282
protected unmount() {
9383
super.unmount();
84+
this.setupExtendedUsableRect.cancel();
9485
this.unsubscribe();
9586
}
9687

9788
public updateChildren() {
9889
return [
9990
PointerGrid.create({
100-
x: this.state.extendedUsableRectX,
101-
y: this.state.extendedUsableRectY,
102-
width: this.state.extendedUsableRectWidth,
103-
height: this.state.extendedUsableRectHeight,
91+
x: this.state.x,
92+
y: this.state.y,
93+
width: this.state.width,
94+
height: this.state.height,
10495
}),
10596
];
10697
}

0 commit comments

Comments
 (0)