Skip to content

Commit 0f1448a

Browse files
feat(connection): add AdaptiveCurveBuilder and update connection types example
1 parent 00b4d56 commit 0f1448a

File tree

14 files changed

+546
-88
lines changed

14 files changed

+546
-88
lines changed

projects/f-examples/connections/connection-types/connection-types.component.html

Lines changed: 0 additions & 30 deletions
This file was deleted.

projects/f-examples/connections/connection-types/connection-types.component.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<f-flow fDraggable (fLoaded)="loaded()">
2+
<f-canvas>
3+
<f-connection [fReassignDisabled]="true" fType="straight" fOutputId="1" fInputId="2" />
4+
<div
5+
fNode
6+
fDragHandle
7+
[fNodePosition]="{ x: 0, y: 0 }"
8+
fNodeOutput
9+
fOutputId="1"
10+
fOutputConnectableSide="right"
11+
>
12+
Node
13+
</div>
14+
<div
15+
fNode
16+
fDragHandle
17+
[fNodePosition]="{ x: 200, y: 50 }"
18+
fNodeInput
19+
fInputId="2"
20+
fInputConnectableSide="left"
21+
>
22+
Node
23+
</div>
24+
25+
<f-connection [fReassignDisabled]="true" fType="segment" fOutputId="3" fInputId="4" />
26+
<div
27+
fNode
28+
fDragHandle
29+
[fNodePosition]="{ x: 0, y: 150 }"
30+
fNodeOutput
31+
fOutputId="3"
32+
fOutputConnectableSide="right"
33+
>
34+
Node
35+
</div>
36+
<div
37+
fNode
38+
fDragHandle
39+
[fNodePosition]="{ x: 200, y: 200 }"
40+
fNodeInput
41+
fInputId="4"
42+
fInputConnectableSide="left"
43+
>
44+
Node
45+
</div>
46+
47+
<f-connection [fReassignDisabled]="true" fType="bezier" fOutputId="5" fInputId="6" />
48+
<div
49+
fNode
50+
fDragHandle
51+
[fNodePosition]="{ x: 0, y: 300 }"
52+
fNodeOutput
53+
fOutputId="5"
54+
fOutputConnectableSide="right"
55+
>
56+
Node
57+
</div>
58+
<div
59+
fNode
60+
fDragHandle
61+
[fNodePosition]="{ x: 200, y: 350 }"
62+
fNodeInput
63+
fInputId="6"
64+
fInputConnectableSide="left"
65+
>
66+
Node
67+
</div>
68+
<f-connection [fReassignDisabled]="true" fType="adaptive-curve" fOutputId="7" fInputId="8" />
69+
<div
70+
fNode
71+
fDragHandle
72+
[fNodePosition]="{ x: 0, y: 450 }"
73+
fNodeOutput
74+
fOutputId="7"
75+
fOutputConnectableSide="right"
76+
>
77+
Node
78+
</div>
79+
<div
80+
fNode
81+
fDragHandle
82+
[fNodePosition]="{ x: 200, y: 500 }"
83+
fNodeInput
84+
fInputId="8"
85+
fInputConnectableSide="left"
86+
>
87+
Node
88+
</div>
89+
</f-canvas>
90+
</f-flow>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@use "../../flow-common";
1+
@use '../../flow-common';
22

33
::ng-deep f-flow {
44
@include flow-common.connection;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
2+
import { FCanvasComponent, FFlowModule } from '@foblex/flow';
3+
4+
@Component({
5+
selector: 'connection-types',
6+
styleUrls: ['./connection-types.scss'],
7+
templateUrl: './connection-types.html',
8+
changeDetection: ChangeDetectionStrategy.OnPush,
9+
standalone: true,
10+
imports: [FFlowModule],
11+
})
12+
export class ConnectionTypes {
13+
private readonly _canvas = viewChild.required(FCanvasComponent);
14+
15+
protected loaded(): void {
16+
this._canvas()?.resetScaleAndCenter(false);
17+
}
18+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { IFConnectionBuilderRequest, IFConnectionBuilderResponse } from '@foblex/flow';
2+
import { EFConnectableSide } from '@foblex/flow';
3+
import { AdaptiveCurveBuilder } from '@foblex/flow';
4+
5+
describe('AdaptiveCurveBuilder', () => {
6+
let builder: AdaptiveCurveBuilder;
7+
8+
beforeEach(() => {
9+
builder = new AdaptiveCurveBuilder();
10+
});
11+
12+
function expectCenterWithinPointsBBox(resp: IFConnectionBuilderResponse) {
13+
const xs = resp.points?.map((p) => p.x) || [];
14+
const ys = resp.points?.map((p) => p.y) || [];
15+
const minX = Math.min(...xs);
16+
const maxX = Math.max(...xs);
17+
const minY = Math.min(...ys);
18+
const maxY = Math.max(...ys);
19+
20+
expect(resp.connectionCenter).toBeDefined();
21+
expect(Number.isFinite(resp.connectionCenter.x)).toBe(true);
22+
expect(Number.isFinite(resp.connectionCenter.y)).toBe(true);
23+
24+
const EPS = 1e-2;
25+
expect(resp.connectionCenter.x).toBeGreaterThanOrEqual(minX - EPS);
26+
expect(resp.connectionCenter.x).toBeLessThanOrEqual(maxX + EPS);
27+
expect(resp.connectionCenter.y).toBeGreaterThanOrEqual(minY - EPS);
28+
expect(resp.connectionCenter.y).toBeLessThanOrEqual(maxY + EPS);
29+
}
30+
31+
it('builds a path and center for a horizontal connection (left → right)', () => {
32+
const request: IFConnectionBuilderRequest = {
33+
source: { x: 0, y: 0 },
34+
target: { x: 100, y: 0 },
35+
sourceSide: EFConnectableSide.RIGHT,
36+
targetSide: EFConnectableSide.LEFT,
37+
radius: 10,
38+
offset: 20,
39+
};
40+
41+
const response: IFConnectionBuilderResponse = builder.handle(request);
42+
43+
expect(response.path).toBeDefined();
44+
expect(response.path.startsWith('M 0 0 C')).toBe(true); // cubic path
45+
expect(response.points?.length).toBe(33); // 32 samples + start
46+
expect(response.secondPoint).toBeDefined();
47+
expect(response.penultimatePoint).toBeDefined();
48+
49+
expect(response.connectionCenter.x).toBeCloseTo(50, 5);
50+
expect(response.connectionCenter.y).toBeCloseTo(0, 5);
51+
});
52+
53+
it('builds a path and center for a vertical connection (top → bottom)', () => {
54+
const request: IFConnectionBuilderRequest = {
55+
source: { x: 0, y: 0 },
56+
target: { x: 0, y: 100 },
57+
sourceSide: EFConnectableSide.BOTTOM,
58+
targetSide: EFConnectableSide.TOP,
59+
radius: 10,
60+
offset: 20,
61+
};
62+
63+
const response: IFConnectionBuilderResponse = builder.handle(request);
64+
65+
expect(response.path).toBeDefined();
66+
expect(response.path.startsWith('M 0 0 C')).toBe(true);
67+
expect(response.points?.length).toBe(33);
68+
69+
expect(response.connectionCenter.x).toBeCloseTo(0, 5);
70+
expect(response.connectionCenter.y).toBeCloseTo(50, 5);
71+
});
72+
73+
it('builds a path and center for a diagonal connection', () => {
74+
const request: IFConnectionBuilderRequest = {
75+
source: { x: 0, y: 0 },
76+
target: { x: 100, y: 100 },
77+
sourceSide: EFConnectableSide.RIGHT,
78+
targetSide: EFConnectableSide.BOTTOM,
79+
radius: 10,
80+
offset: 20,
81+
};
82+
83+
const response: IFConnectionBuilderResponse = builder.handle(request);
84+
85+
expect(response.path).toBeDefined();
86+
expect(response.path).toContain('C');
87+
expect(response.points?.length).toBe(33);
88+
89+
expectCenterWithinPointsBBox(response);
90+
});
91+
92+
it('builds a path and center for a connection with offset', () => {
93+
const request: IFConnectionBuilderRequest = {
94+
source: { x: 0, y: 0 },
95+
target: { x: 50, y: 50 },
96+
sourceSide: EFConnectableSide.BOTTOM,
97+
targetSide: EFConnectableSide.LEFT,
98+
radius: 10,
99+
offset: 30,
100+
};
101+
102+
const response: IFConnectionBuilderResponse = builder.handle(request);
103+
104+
expect(response.path).toBeDefined();
105+
expect(response.path).toContain('C');
106+
expect(response.points?.length).toBe(33);
107+
expectCenterWithinPointsBBox(response);
108+
});
109+
110+
it('ensures control points are not equal to anchors (non-degenerate handles)', () => {
111+
const request: IFConnectionBuilderRequest = {
112+
source: { x: 10, y: 20 },
113+
target: { x: 110, y: 120 },
114+
sourceSide: EFConnectableSide.RIGHT,
115+
targetSide: EFConnectableSide.TOP,
116+
radius: 0,
117+
offset: 16,
118+
};
119+
120+
const response: IFConnectionBuilderResponse = builder.handle(request);
121+
122+
const { secondPoint: c1, penultimatePoint: c2 } = response;
123+
expect(c1).toBeDefined();
124+
expect(c2).toBeDefined();
125+
126+
expect(!(c1.x === request.source.x && c1.y === request.source.y)).toBe(true);
127+
expect(!(c2.x === request.target.x && c2.y === request.target.y)).toBe(true);
128+
129+
expectCenterWithinPointsBBox(response);
130+
});
131+
});

0 commit comments

Comments
 (0)