Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<f-flow fDraggable (fLoaded)="onLoaded()">
<f-canvas>
<!-- Example 1: Simple L-shaped path with one control point -->
<f-connection
fType="custom-path"
fOutputId="1"
fInputId="2"
[fControlPoints]="controlPoints1"
[fRadius]="8"
>
</f-connection>
<div
fNode
fDragHandle
[fNodePosition]="{ x: 50, y: 50 }"
fNodeOutput
fOutputId="1"
fOutputConnectableSide="right"
class="f-node"
>
<div class="f-node-content">Node A</div>
</div>
<div
fNode
fDragHandle
[fNodePosition]="{ x: 350, y: 50 }"
fNodeInput
fInputId="2"
fInputConnectableSide="left"
class="f-node"
>
<div class="f-node-content">Node B</div>
</div>

<!-- Example 2: Z-shaped path with multiple control points -->
<f-connection
fType="custom-path"
fOutputId="3"
fInputId="4"
[fControlPoints]="controlPoints2"
[fRadius]="10"
>
</f-connection>
<div
fNode
fDragHandle
[fNodePosition]="{ x: 50, y: 200 }"
fNodeOutput
fOutputId="3"
fOutputConnectableSide="right"
class="f-node"
>
<div class="f-node-content">Node C</div>
</div>
<div
fNode
fDragHandle
[fNodePosition]="{ x: 350, y: 300 }"
fNodeInput
fInputId="4"
fInputConnectableSide="left"
class="f-node"
>
<div class="f-node-content">Node D</div>
</div>

<!-- Example 3: Complex industrial-style path -->
<f-connection
fType="custom-path"
fOutputId="5"
fInputId="6"
[fControlPoints]="controlPoints3"
[fRadius]="12"
>
</f-connection>
<div
fNode
fDragHandle
[fNodePosition]="{ x: 50, y: 400 }"
fNodeOutput
fOutputId="5"
fOutputConnectableSide="right"
class="f-node"
>
<div class="f-node-content">Source</div>
</div>
<div
fNode
fDragHandle
[fNodePosition]="{ x: 350, y: 550 }"
fNodeInput
fInputId="6"
fInputConnectableSide="left"
class="f-node"
>
<div class="f-node-content">Target</div>
</div>
</f-canvas>
</f-flow>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@use '../../flow-common';

::ng-deep f-flow {
@include flow-common.connection;
}

.f-node {
@include flow-common.node;
}

.f-node-content {
padding: 12px 20px;
background: white;
border: 2px solid #3b82f6;
border-radius: 8px;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core';
import { FCanvasComponent, FFlowModule } from '@foblex/flow';

@Component({
selector: 'custom-path-control-points',
styleUrls: ['./custom-path-control-points.component.scss'],
templateUrl: './custom-path-control-points.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [FFlowModule],
})
export class CustomPathControlPointsComponent {
@ViewChild(FCanvasComponent, { static: true })
public fCanvas!: FCanvasComponent;

// Example 1: Simple L-shaped path
public controlPoints1 = [{ x: 150, y: 50 }];

// Example 2: Z-shaped path with multiple control points
public controlPoints2 = [
{ x: 150, y: 200 },
{ x: 150, y: 250 },
{ x: 300, y: 250 },
];

// Example 3: Complex industrial-style path
public controlPoints3 = [
{ x: 100, y: 400 },
{ x: 100, y: 450 },
{ x: 200, y: 450 },
{ x: 200, y: 500 },
{ x: 300, y: 500 },
];

public onLoaded(): void {
this.fCanvas.resetScaleAndCenter(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { FCustomPathBuilder } from './f-custom-path.builder';
import {
IFConnectionBuilderRequest,
IFConnectionBuilderResponse,
} from '../../f-connection-builder';
import { EFConnectableSide } from '../../../f-connectors';

describe('FCustomPathBuilder', () => {
let builder: FCustomPathBuilder;

beforeEach(() => {
builder = new FCustomPathBuilder();
});

it('should create a straight path when no control points are provided', () => {
const request: IFConnectionBuilderRequest = {
source: { x: 0, y: 0 },
target: { x: 100, y: 100 },
sourceSide: EFConnectableSide.AUTO,
targetSide: EFConnectableSide.AUTO,
radius: 0,
offset: 0,
controlPoints: [],
};

const response: IFConnectionBuilderResponse = builder.handle(request);

expect(response.path).toBeDefined();
expect(response.path).toContain('M0 0');
expect(response.path).toContain('L100.0002 100.0002');
expect(response.connectionCenter).toBeDefined();
expect(response.points?.length).toBe(2);
});

it('should create a path with one control point', () => {
const request: IFConnectionBuilderRequest = {
source: { x: 0, y: 0 },
target: { x: 100, y: 100 },
sourceSide: EFConnectableSide.AUTO,
targetSide: EFConnectableSide.AUTO,
radius: 0,
offset: 0,
controlPoints: [{ x: 50, y: 0 }],
};

const response: IFConnectionBuilderResponse = builder.handle(request);

expect(response.path).toBeDefined();
expect(response.path).toContain('M0 0');
expect(response.path).toContain('L50 0');
expect(response.path).toContain('L100.0002 100.0002');
expect(response.points?.length).toBe(3);
expect(response.points).toEqual([
{ x: 0, y: 0 },
{ x: 50, y: 0 },
{ x: 100, y: 100 },
]);
});

it('should create a path with multiple control points', () => {
const request: IFConnectionBuilderRequest = {
source: { x: 0, y: 0 },
target: { x: 100, y: 100 },
sourceSide: EFConnectableSide.AUTO,
targetSide: EFConnectableSide.AUTO,
radius: 0,
offset: 0,
controlPoints: [
{ x: 50, y: 0 },
{ x: 50, y: 50 },
{ x: 100, y: 50 },
],
};

const response: IFConnectionBuilderResponse = builder.handle(request);

expect(response.path).toBeDefined();
expect(response.points?.length).toBe(5);
expect(response.points).toEqual([
{ x: 0, y: 0 },
{ x: 50, y: 0 },
{ x: 50, y: 50 },
{ x: 100, y: 50 },
{ x: 100, y: 100 },
]);
});

it('should apply border radius to corners', () => {
const request: IFConnectionBuilderRequest = {
source: { x: 0, y: 0 },
target: { x: 100, y: 100 },
sourceSide: EFConnectableSide.AUTO,
targetSide: EFConnectableSide.AUTO,
radius: 10,
offset: 0,
controlPoints: [{ x: 50, y: 0 }],
};

const response: IFConnectionBuilderResponse = builder.handle(request);

expect(response.path).toBeDefined();
// Should contain Q (quadratic bezier) for rounded corner
expect(response.path).toContain('Q');
});

it('should calculate correct connection center', () => {
const request: IFConnectionBuilderRequest = {
source: { x: 0, y: 0 },
target: { x: 100, y: 0 },
sourceSide: EFConnectableSide.AUTO,
targetSide: EFConnectableSide.AUTO,
radius: 0,
offset: 0,
controlPoints: [],
};

const response: IFConnectionBuilderResponse = builder.handle(request);

expect(response.connectionCenter).toBeDefined();
expect(response.connectionCenter.x).toBeCloseTo(50, 0);
expect(response.connectionCenter.y).toBeCloseTo(0, 0);
});

it('should return correct penultimate and second points', () => {
const request: IFConnectionBuilderRequest = {
source: { x: 0, y: 0 },
target: { x: 100, y: 100 },
sourceSide: EFConnectableSide.AUTO,
targetSide: EFConnectableSide.AUTO,
radius: 0,
offset: 0,
controlPoints: [
{ x: 50, y: 0 },
{ x: 50, y: 50 },
],
};

const response: IFConnectionBuilderResponse = builder.handle(request);

expect(response.secondPoint).toEqual({ x: 50, y: 0 });
expect(response.penultimatePoint).toEqual({ x: 50, y: 50 });
});
});
Loading