Skip to content

Commit eb71297

Browse files
committed
refactor: move connections as a plugin
1 parent e0bd2bf commit eb71297

9 files changed

+125
-114
lines changed

projects/flow/src/lib/flow-interface.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ export type ArrowPathFn = (
4343
export interface FlowPlugin {
4444
onInit?(data: FlowComponent): void;
4545
afterInit?(data: FlowComponent): void;
46-
beforeArrowUpdate?(data: FlowComponent): void;
46+
beforeUpdate?(data: FlowComponent): void;
47+
afterUpdate?(data: FlowComponent): void;
4748
}

projects/flow/src/lib/flow.component.ts

+8-83
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
OnInit,
1515
} from '@angular/core';
1616
import { startWith } from 'rxjs';
17-
import { Connections } from './connections';
1817
import { FlowChildComponent } from './flow-child.component';
1918
import { FlowService } from './flow.service';
2019
import {
@@ -188,7 +187,7 @@ export class FlowComponent
188187
.pipe(startWith(this.children))
189188
.subscribe((children) => {
190189
this.flow.update(this.children.map((x) => x.position));
191-
this.runPlugin((e) => e.beforeArrowUpdate?.(this));
190+
this.runPlugin((e) => e.beforeUpdate?.(this));
192191
this.createArrows();
193192
});
194193
requestAnimationFrame(() => this.updateArrows()); // this required for angular to render the dot
@@ -204,7 +203,7 @@ export class FlowComponent
204203

205204
updateDirection(direction: FlowDirection) {
206205
this.flow.direction = direction;
207-
this.runPlugin((e) => e.beforeArrowUpdate?.(this));
206+
this.runPlugin((e) => e.beforeUpdate?.(this));
208207
this.createArrows();
209208
}
210209

@@ -384,101 +383,27 @@ export class FlowComponent
384383
}
385384

386385
updateArrows(e?: FlowOptions) {
387-
const gElement: SVGGElement = this.g.nativeElement;
388-
const childObj = this.getChildInfo();
386+
this.runPlugin((e) => e.afterUpdate?.(this));
387+
// const gElement: SVGGElement = this.g.nativeElement;
388+
// const childObj = this.getChildInfo();
389389
// Handle reverse dependencies
390-
this.flow.connections = new Connections(this.list, this.flow.direction);
391-
392-
// Calculate new arrows
393-
this.flow.arrows.forEach((arrow) => {
394-
const [from, to] = arrow.deps;
395-
const fromItem = childObj[from];
396-
const toItem = childObj[to];
397-
if (fromItem && toItem) {
398-
const [endDotIndex, startDotIndex] = this.getClosestDots(toItem, from);
399-
400-
const startDot = this.getDotByIndex(
401-
childObj,
402-
fromItem.position,
403-
startDotIndex,
404-
this.flow.scale,
405-
this.flow.panX,
406-
this.flow.panY
407-
);
408-
const endDot = this.getDotByIndex(
409-
childObj,
410-
toItem.position,
411-
endDotIndex,
412-
this.flow.scale,
413-
this.flow.panX,
414-
this.flow.panY
415-
);
416-
417-
// we need to reverse the path because the arrow head is at the end
418-
arrow.d = this.flow.arrowFn(
419-
endDot,
420-
startDot,
421-
this.flow.config.ArrowSize,
422-
2
423-
);
424-
}
425-
426-
// Update the SVG paths
427-
this.flow.arrows.forEach((arrow) => {
428-
const pathElement = gElement.querySelector(
429-
`#${arrow.id}`
430-
) as SVGPathElement;
431-
if (pathElement) {
432-
pathElement.setAttribute('d', arrow.d);
433-
}
434-
});
435-
});
436-
437-
this.flow.connections.updateDotVisibility(this.oldChildObj());
390+
// this.flow.connections = new Connections(this.list, this.flow.direction);
438391
}
439392

440-
private oldChildObj() {
393+
oldChildObj() {
441394
return this.children.toArray().reduce((acc, curr) => {
442395
acc[curr.position.id] = curr;
443396
return acc;
444397
}, {} as Record<string, FlowChildComponent>);
445398
}
446399

447-
private getChildInfo() {
400+
getChildInfo() {
448401
return this.list.reduce((acc, curr) => {
449402
acc[curr.position.id] = curr;
450403
return acc;
451404
}, {} as Record<string, ChildInfo>);
452405
}
453406

454-
private getDotByIndex(
455-
childObj: Record<string, ChildInfo>,
456-
item: FlowOptions,
457-
dotIndex: number,
458-
scale: number,
459-
panX: number,
460-
panY: number
461-
): DotOptions {
462-
const child = childObj[item.id];
463-
const childDots = child.dots as DOMRect[];
464-
// Make sure the dot index is within bounds
465-
if (dotIndex < 0 || dotIndex >= childDots.length) {
466-
throw new Error(`Invalid dot index: ${dotIndex}`);
467-
}
468-
469-
const rect = childDots[dotIndex];
470-
const { left, top } = this.flow.zRect;
471-
// const rect = dotEl.nativeElement.getBoundingClientRect();
472-
const x = (rect.x + rect.width / 2 - panX - left) / scale;
473-
const y = (rect.y + rect.height / 2 - panY - top) / scale;
474-
475-
return { ...item, x, y, dotIndex };
476-
}
477-
478-
public getClosestDots(item: ChildInfo, dep?: string): number[] {
479-
return this.flow.connections.getClosestDotsSimplified(item, dep as string);
480-
}
481-
482407
ngOnDestroy(): void {
483408
this.el.nativeElement.removeEventListener('wheel', this.zoomHandle);
484409
}

projects/flow/src/lib/flow.service.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Injectable, NgZone } from '@angular/core';
22
import { BehaviorSubject, Subject } from 'rxjs';
3-
import { Connections } from './connections';
43
import {
54
ArrowPathFn,
65
FlowConfig,
@@ -29,7 +28,6 @@ export class FlowService {
2928
gridSize = 1;
3029
arrows: Arrow[] = [];
3130
zoomContainer: HTMLElement;
32-
connections: Connections;
3331
layoutUpdated = new Subject<void>();
3432
onMouse = new Subject<MouseEvent>();
3533

projects/flow/src/lib/plugins/arrangements.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export class Arrangements implements FlowPlugin {
143143
this.data = data;
144144
}
145145

146-
beforeArrowUpdate(data: FlowComponent): void {
146+
beforeUpdate(data: FlowComponent): void {
147147
this.data = data;
148148
this.runArrange();
149149
}

projects/flow/src/lib/connections.spec.ts projects/flow/src/lib/plugins/connections.spec.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FlowOptions, ChildInfo } from './flow-interface';
1+
import { FlowOptions, ChildInfo } from '../flow-interface';
22
import { Connections } from './connections';
33

44
describe('Connections', () => {
@@ -36,7 +36,8 @@ describe('Connections', () => {
3636
check(list, [0, 2]);
3737

3838
function check(list: ChildInfo[], expected: [number, number]) {
39-
connections = new Connections(list);
39+
connections = new Connections();
40+
connections.onInit({ list, flow: { direction: 'horizontal' } } as any);
4041
const actual = connections._findClosestConnectionPoints(list[0], list[1]);
4142
expect(actual).toEqual(expected);
4243
}
@@ -80,7 +81,8 @@ describe('Connections', () => {
8081
dep: string,
8182
expected: [number, number]
8283
) {
83-
connections = new Connections(list);
84+
connections = new Connections();
85+
connections.onInit({ list, flow: { direction: 'horizontal' } } as any);
8486
const actual = connections.getClosestDotsSimplified(list[index], dep);
8587
expect(actual).toEqual(expected);
8688
}

projects/flow/src/lib/connections.ts projects/flow/src/lib/plugins/connections.ts

+104-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { ChildInfo, FlowOptions } from './flow-interface';
2-
import { FlowChildComponent } from './flow-child.component';
3-
4-
export class Connections {
1+
import {
2+
ChildInfo,
3+
DotOptions,
4+
FlowOptions,
5+
FlowPlugin,
6+
} from '../flow-interface';
7+
import { FlowChildComponent } from '../flow-child.component';
8+
import { FlowComponent } from '../flow.component';
9+
10+
export class Connections implements FlowPlugin {
511
// key = id of the item
612
// value = ids of the items that depend on it
713
reverseDepsMap = new Map<string, string[]>();
@@ -10,12 +16,75 @@ export class Connections {
1016
// value = index of the closest dot
1117
closestDots = new Map<string, number>();
1218

13-
constructor(
14-
private list: ChildInfo[],
15-
private direction: 'horizontal' | 'vertical' = 'horizontal'
16-
) {
17-
this.setReverseDepsMap(list.map((x) => x.position));
18-
// console.count('Connections');
19+
data: FlowComponent;
20+
private list: ChildInfo[];
21+
private direction: 'horizontal' | 'vertical' = 'horizontal';
22+
23+
onInit(data: FlowComponent): void {
24+
this.setData(data);
25+
}
26+
27+
afterUpdate(data: FlowComponent): void {
28+
this.setData(data);
29+
30+
const gElement: SVGGElement = this.data.g.nativeElement;
31+
const childObj = this.data.getChildInfo();
32+
// Calculate new arrows
33+
this.data.flow.arrows.forEach((arrow) => {
34+
const [from, to] = arrow.deps;
35+
const fromItem = childObj[from];
36+
const toItem = childObj[to];
37+
if (fromItem && toItem) {
38+
const [endDotIndex, startDotIndex] = this.getClosestDotsSimplified(
39+
toItem,
40+
from
41+
);
42+
43+
const startDot = this.getDotByIndex(
44+
childObj,
45+
fromItem.position,
46+
startDotIndex,
47+
this.data.flow.scale,
48+
this.data.flow.panX,
49+
this.data.flow.panY
50+
);
51+
const endDot = this.getDotByIndex(
52+
childObj,
53+
toItem.position,
54+
endDotIndex,
55+
this.data.flow.scale,
56+
this.data.flow.panX,
57+
this.data.flow.panY
58+
);
59+
60+
// we need to reverse the path because the arrow head is at the end
61+
arrow.d = this.data.flow.arrowFn(
62+
endDot,
63+
startDot,
64+
this.data.flow.config.ArrowSize,
65+
2
66+
);
67+
}
68+
69+
// Update the SVG paths
70+
this.data.flow.arrows.forEach((arrow) => {
71+
const pathElement = gElement.querySelector(
72+
`#${arrow.id}`
73+
) as SVGPathElement;
74+
if (pathElement) {
75+
pathElement.setAttribute('d', arrow.d);
76+
}
77+
});
78+
});
79+
80+
this.updateDotVisibility(this.data.oldChildObj());
81+
}
82+
83+
private setData(data: FlowComponent) {
84+
this.data = data;
85+
this.list = data.list;
86+
this.direction = data.flow.direction;
87+
this.setReverseDepsMap(this.list.map((x) => x.position));
1988
}
2089

2190
public getClosestDotsSimplified(
@@ -28,13 +97,6 @@ export class Connections {
2897
];
2998
ids.forEach((x) => this.findClosestDot(x, item));
3099
// ids.forEach((x) => this.findClosestDot(x, item, childObj));
31-
32-
// Create unique keys for each dependency and retrieve the closest dots based on these keys
33-
const closestDotIndices = ids.map((x) => {
34-
const uniqueKey = `${item.position.id}-${x}`;
35-
return this.closestDots.get(uniqueKey) as number;
36-
});
37-
38100
// Remove duplicates
39101
// const uniqueClosestDotIndices = Array.from(new Set(closestDotIndices));
40102

@@ -67,12 +129,6 @@ export class Connections {
67129
}
68130
}
69131

70-
private computeDistance(dot1: DOMRect, dot2: DOMRect): number {
71-
const dx = dot1.x - dot2.x;
72-
const dy = dot1.y - dot2.y;
73-
return Math.sqrt(dx * dx + dy * dy);
74-
}
75-
76132
public _findClosestConnectionPoints(
77133
parent: ChildInfo,
78134
child: ChildInfo
@@ -132,7 +188,7 @@ export class Connections {
132188
return swapped ? [childIndex, parentIndex] : [parentIndex, childIndex];
133189
}
134190

135-
updateDotVisibility(childObj: Record<string, FlowChildComponent>) {
191+
private updateDotVisibility(childObj: Record<string, FlowChildComponent>) {
136192
Object.keys(childObj).forEach((id) => {
137193
const child = childObj[id];
138194
const position = child.position;
@@ -152,6 +208,30 @@ export class Connections {
152208
});
153209
}
154210

211+
private getDotByIndex(
212+
childObj: Record<string, ChildInfo>,
213+
item: FlowOptions,
214+
dotIndex: number,
215+
scale: number,
216+
panX: number,
217+
panY: number
218+
): DotOptions {
219+
const child = childObj[item.id];
220+
const childDots = child.dots as DOMRect[];
221+
// Make sure the dot index is within bounds
222+
if (dotIndex < 0 || dotIndex >= childDots.length) {
223+
throw new Error(`Invalid dot index: ${dotIndex}`);
224+
}
225+
226+
const rect = childDots[dotIndex];
227+
const { left, top } = this.data.flow.zRect;
228+
// const rect = dotEl.nativeElement.getBoundingClientRect();
229+
const x = (rect.x + rect.width / 2 - panX - left) / scale;
230+
const y = (rect.y + rect.height / 2 - panY - top) / scale;
231+
232+
return { ...item, x, y, dotIndex };
233+
}
234+
155235
private setReverseDepsMap(list: FlowOptions[]) {
156236
list.forEach((item) => {
157237
item.deps.forEach((depId) => {

projects/flow/src/public-api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from './lib/svg';
1515
export { FitToWindow } from './lib/plugins/fit-to-window';
1616
export { ScrollIntoView } from './lib/plugins/scroll-into-view';
1717
export { Arrangements } from './lib/plugins/arrangements';
18+
export { Connections } from './lib/plugins/connections';

src/app/demo/demo-one.component.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
FitToWindow,
1414
ScrollIntoView,
1515
Arrangements,
16+
Connections,
1617
} from '@ngu/flow';
1718
import { EditorComponent } from '../editor.component';
1819
import { ToolbarComponent } from './toolbar.component';
@@ -90,6 +91,7 @@ export class DemoOneComponent implements AfterViewInit {
9091
scroll: new ScrollIntoView('1'),
9192
fitWindow: new FitToWindow(true),
9293
arrange: new Arrangements(),
94+
connections: new Connections(),
9395
};
9496
config: FlowConfig = {
9597
Arrows: true,

src/app/demo/demo-two.component.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ScrollIntoView,
1414
FitToWindow,
1515
Arrangements,
16+
Connections,
1617
} from '@ngu/flow';
1718
import { EditorComponent } from '../editor.component';
1819
import { ToolbarComponent } from './toolbar.component';
@@ -100,6 +101,7 @@ export class DemoTwoComponent implements AfterViewInit {
100101
scroll: new ScrollIntoView('1'),
101102
fitWindow: new FitToWindow(false),
102103
arrange: new Arrangements(),
104+
connections: new Connections(),
103105
};
104106
config: FlowConfig = {
105107
Arrows: true,

0 commit comments

Comments
 (0)