Skip to content

Commit bb32f7d

Browse files
feat: add custom path connection type with control points support
Co-authored-by: siarheihuzarevich <13272876+siarheihuzarevich@users.noreply.github.com>
1 parent 0a442a5 commit bb32f7d

File tree

13 files changed

+442
-1
lines changed

13 files changed

+442
-1
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<f-flow fDraggable (fLoaded)="onLoaded()">
2+
<f-canvas>
3+
<!-- Example 1: Simple L-shaped path with one control point -->
4+
<f-connection
5+
fType="custom-path"
6+
fOutputId="1"
7+
fInputId="2"
8+
[fControlPoints]="controlPoints1"
9+
[fRadius]="8"
10+
>
11+
</f-connection>
12+
<div
13+
fNode
14+
fDragHandle
15+
[fNodePosition]="{ x: 50, y: 50 }"
16+
fNodeOutput
17+
fOutputId="1"
18+
fOutputConnectableSide="right"
19+
class="f-node"
20+
>
21+
<div class="f-node-content">Node A</div>
22+
</div>
23+
<div
24+
fNode
25+
fDragHandle
26+
[fNodePosition]="{ x: 350, y: 50 }"
27+
fNodeInput
28+
fInputId="2"
29+
fInputConnectableSide="left"
30+
class="f-node"
31+
>
32+
<div class="f-node-content">Node B</div>
33+
</div>
34+
35+
<!-- Example 2: Z-shaped path with multiple control points -->
36+
<f-connection
37+
fType="custom-path"
38+
fOutputId="3"
39+
fInputId="4"
40+
[fControlPoints]="controlPoints2"
41+
[fRadius]="10"
42+
>
43+
</f-connection>
44+
<div
45+
fNode
46+
fDragHandle
47+
[fNodePosition]="{ x: 50, y: 200 }"
48+
fNodeOutput
49+
fOutputId="3"
50+
fOutputConnectableSide="right"
51+
class="f-node"
52+
>
53+
<div class="f-node-content">Node C</div>
54+
</div>
55+
<div
56+
fNode
57+
fDragHandle
58+
[fNodePosition]="{ x: 350, y: 300 }"
59+
fNodeInput
60+
fInputId="4"
61+
fInputConnectableSide="left"
62+
class="f-node"
63+
>
64+
<div class="f-node-content">Node D</div>
65+
</div>
66+
67+
<!-- Example 3: Complex industrial-style path -->
68+
<f-connection
69+
fType="custom-path"
70+
fOutputId="5"
71+
fInputId="6"
72+
[fControlPoints]="controlPoints3"
73+
[fRadius]="12"
74+
>
75+
</f-connection>
76+
<div
77+
fNode
78+
fDragHandle
79+
[fNodePosition]="{ x: 50, y: 400 }"
80+
fNodeOutput
81+
fOutputId="5"
82+
fOutputConnectableSide="right"
83+
class="f-node"
84+
>
85+
<div class="f-node-content">Source</div>
86+
</div>
87+
<div
88+
fNode
89+
fDragHandle
90+
[fNodePosition]="{ x: 350, y: 550 }"
91+
fNodeInput
92+
fInputId="6"
93+
fInputConnectableSide="left"
94+
class="f-node"
95+
>
96+
<div class="f-node-content">Target</div>
97+
</div>
98+
</f-canvas>
99+
</f-flow>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@use '../../flow-common';
2+
3+
::ng-deep f-flow {
4+
@include flow-common.connection;
5+
}
6+
7+
.f-node {
8+
@include flow-common.node;
9+
}
10+
11+
.f-node-content {
12+
padding: 12px 20px;
13+
background: white;
14+
border: 2px solid #3b82f6;
15+
border-radius: 8px;
16+
font-weight: 500;
17+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
18+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core';
2+
import { FCanvasComponent, FFlowModule } from '@foblex/flow';
3+
4+
@Component({
5+
selector: 'custom-path-control-points',
6+
styleUrls: ['./custom-path-control-points.component.scss'],
7+
templateUrl: './custom-path-control-points.component.html',
8+
changeDetection: ChangeDetectionStrategy.OnPush,
9+
standalone: true,
10+
imports: [FFlowModule],
11+
})
12+
export class CustomPathControlPointsComponent {
13+
@ViewChild(FCanvasComponent, { static: true })
14+
public fCanvas!: FCanvasComponent;
15+
16+
// Example 1: Simple L-shaped path
17+
public controlPoints1 = [{ x: 150, y: 50 }];
18+
19+
// Example 2: Z-shaped path with multiple control points
20+
public controlPoints2 = [
21+
{ x: 150, y: 200 },
22+
{ x: 150, y: 250 },
23+
{ x: 300, y: 250 },
24+
];
25+
26+
// Example 3: Complex industrial-style path
27+
public controlPoints3 = [
28+
{ x: 100, y: 400 },
29+
{ x: 100, y: 450 },
30+
{ x: 200, y: 450 },
31+
{ x: 200, y: 500 },
32+
{ x: 300, y: 500 },
33+
];
34+
35+
public onLoaded(): void {
36+
this.fCanvas.resetScaleAndCenter(false);
37+
}
38+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { FCustomPathBuilder } from './f-custom-path.builder';
2+
import {
3+
IFConnectionBuilderRequest,
4+
IFConnectionBuilderResponse,
5+
} from '../../f-connection-builder';
6+
import { EFConnectableSide } from '../../../f-connectors';
7+
8+
describe('FCustomPathBuilder', () => {
9+
let builder: FCustomPathBuilder;
10+
11+
beforeEach(() => {
12+
builder = new FCustomPathBuilder();
13+
});
14+
15+
it('should create a straight path when no control points are provided', () => {
16+
const request: IFConnectionBuilderRequest = {
17+
source: { x: 0, y: 0 },
18+
target: { x: 100, y: 100 },
19+
sourceSide: EFConnectableSide.AUTO,
20+
targetSide: EFConnectableSide.AUTO,
21+
radius: 0,
22+
offset: 0,
23+
controlPoints: [],
24+
};
25+
26+
const response: IFConnectionBuilderResponse = builder.handle(request);
27+
28+
expect(response.path).toBeDefined();
29+
expect(response.path).toContain('M0 0');
30+
expect(response.path).toContain('L100.0002 100.0002');
31+
expect(response.connectionCenter).toBeDefined();
32+
expect(response.points).toHaveLength(2);
33+
});
34+
35+
it('should create a path with one control point', () => {
36+
const request: IFConnectionBuilderRequest = {
37+
source: { x: 0, y: 0 },
38+
target: { x: 100, y: 100 },
39+
sourceSide: EFConnectableSide.AUTO,
40+
targetSide: EFConnectableSide.AUTO,
41+
radius: 0,
42+
offset: 0,
43+
controlPoints: [{ x: 50, y: 0 }],
44+
};
45+
46+
const response: IFConnectionBuilderResponse = builder.handle(request);
47+
48+
expect(response.path).toBeDefined();
49+
expect(response.path).toContain('M0 0');
50+
expect(response.path).toContain('L50 0');
51+
expect(response.path).toContain('L100.0002 100.0002');
52+
expect(response.points).toHaveLength(3);
53+
expect(response.points).toEqual([
54+
{ x: 0, y: 0 },
55+
{ x: 50, y: 0 },
56+
{ x: 100, y: 100 },
57+
]);
58+
});
59+
60+
it('should create a path with multiple control points', () => {
61+
const request: IFConnectionBuilderRequest = {
62+
source: { x: 0, y: 0 },
63+
target: { x: 100, y: 100 },
64+
sourceSide: EFConnectableSide.AUTO,
65+
targetSide: EFConnectableSide.AUTO,
66+
radius: 0,
67+
offset: 0,
68+
controlPoints: [
69+
{ x: 50, y: 0 },
70+
{ x: 50, y: 50 },
71+
{ x: 100, y: 50 },
72+
],
73+
};
74+
75+
const response: IFConnectionBuilderResponse = builder.handle(request);
76+
77+
expect(response.path).toBeDefined();
78+
expect(response.points).toHaveLength(5);
79+
expect(response.points).toEqual([
80+
{ x: 0, y: 0 },
81+
{ x: 50, y: 0 },
82+
{ x: 50, y: 50 },
83+
{ x: 100, y: 50 },
84+
{ x: 100, y: 100 },
85+
]);
86+
});
87+
88+
it('should apply border radius to corners', () => {
89+
const request: IFConnectionBuilderRequest = {
90+
source: { x: 0, y: 0 },
91+
target: { x: 100, y: 100 },
92+
sourceSide: EFConnectableSide.AUTO,
93+
targetSide: EFConnectableSide.AUTO,
94+
radius: 10,
95+
offset: 0,
96+
controlPoints: [{ x: 50, y: 0 }],
97+
};
98+
99+
const response: IFConnectionBuilderResponse = builder.handle(request);
100+
101+
expect(response.path).toBeDefined();
102+
// Should contain Q (quadratic bezier) for rounded corner
103+
expect(response.path).toContain('Q');
104+
});
105+
106+
it('should calculate correct connection center', () => {
107+
const request: IFConnectionBuilderRequest = {
108+
source: { x: 0, y: 0 },
109+
target: { x: 100, y: 0 },
110+
sourceSide: EFConnectableSide.AUTO,
111+
targetSide: EFConnectableSide.AUTO,
112+
radius: 0,
113+
offset: 0,
114+
controlPoints: [],
115+
};
116+
117+
const response: IFConnectionBuilderResponse = builder.handle(request);
118+
119+
expect(response.connectionCenter).toBeDefined();
120+
expect(response.connectionCenter.x).toBeCloseTo(50, 0);
121+
expect(response.connectionCenter.y).toBeCloseTo(0, 0);
122+
});
123+
124+
it('should return correct penultimate and second points', () => {
125+
const request: IFConnectionBuilderRequest = {
126+
source: { x: 0, y: 0 },
127+
target: { x: 100, y: 100 },
128+
sourceSide: EFConnectableSide.AUTO,
129+
targetSide: EFConnectableSide.AUTO,
130+
radius: 0,
131+
offset: 0,
132+
controlPoints: [
133+
{ x: 50, y: 0 },
134+
{ x: 50, y: 50 },
135+
],
136+
};
137+
138+
const response: IFConnectionBuilderResponse = builder.handle(request);
139+
140+
expect(response.secondPoint).toEqual({ x: 50, y: 0 });
141+
expect(response.penultimatePoint).toEqual({ x: 50, y: 50 });
142+
});
143+
});

0 commit comments

Comments
 (0)