Skip to content

Commit 408d006

Browse files
feat: add drag-and-drop functionality for custom path control points
Co-authored-by: siarheihuzarevich <[email protected]>
1 parent bb6b84c commit 408d006

File tree

15 files changed

+306
-3
lines changed

15 files changed

+306
-3
lines changed

projects/f-flow/src/f-connection/common/f-connection-base.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ export abstract class FConnectionBase
130130

131131
private _penultimatePoint = PointExtensions.initialize();
132132
private _secondPoint = PointExtensions.initialize();
133+
private _pathPoints: IPoint[] = [];
134+
135+
public get pathPoints(): IPoint[] {
136+
return this._pathPoints;
137+
}
133138

134139
protected constructor() {
135140
super(inject(ElementRef<HTMLElement>).nativeElement);
@@ -153,6 +158,7 @@ export abstract class FConnectionBase
153158
this.path = pathResult.path;
154159
this._penultimatePoint = pathResult.penultimatePoint || point1;
155160
this._secondPoint = pathResult.secondPoint || point2;
161+
this._pathPoints = pathResult.points || [];
156162

157163
new ConnectionContentLayoutEngine().layout(this.line, pathResult, this._contents());
158164

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
ElementRef,
5+
inject,
6+
input,
7+
} from '@angular/core';
8+
import { IPoint } from '@foblex/2d';
9+
import { IHasHostElement } from '../../../i-has-host-element';
10+
import { F_CSS_CLASS } from '../../../domain/css-cls';
11+
12+
@Component({
13+
selector: 'circle[f-connection-drag-handle-control-point]',
14+
template: '',
15+
changeDetection: ChangeDetectionStrategy.OnPush,
16+
host: {
17+
'[class]': 'class',
18+
'[attr.data-index]': 'index()',
19+
},
20+
})
21+
export class FConnectionDragHandleControlPointComponent implements IHasHostElement {
22+
private readonly _elementReference = inject(ElementRef);
23+
24+
protected readonly class: string = F_CSS_CLASS.CONNECTION.DRAG_HANDLE;
25+
26+
public readonly index = input.required<number>();
27+
28+
public point!: IPoint;
29+
30+
public get hostElement(): SVGCircleElement {
31+
return this._elementReference.nativeElement;
32+
}
33+
34+
public redraw(point: IPoint): void {
35+
this.point = point;
36+
this.hostElement.setAttribute('cx', point.x.toString());
37+
this.hostElement.setAttribute('cy', point.y.toString());
38+
}
39+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from './f-connection-drag-handle-start.component';
22

33
export * from './f-connection-drag-handle-end.component';
4+
5+
export * from './f-connection-drag-handle-control-point.component';

projects/f-flow/src/f-connection/f-connection/f-connection.component.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
<circle f-connection-drag-handle-start r="8"></circle>
1111
}
1212
<circle f-connection-drag-handle-end r="8"></circle>
13+
@if (fType === 'custom-path' && !fDraggingDisabled()) {
14+
@for (point of controlPointsToRender(); track $index) {
15+
<circle f-connection-drag-handle-control-point r="8" [index]="$index"></circle>
16+
}
17+
}
1318
</g>
1419
@if (fText) {
1520
<text f-connection-text></text>

projects/f-flow/src/f-connection/f-connection/f-connection.component.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ import {
22
booleanAttribute,
33
ChangeDetectionStrategy,
44
Component,
5+
computed,
56
inject,
67
input,
78
Input,
89
numberAttribute,
910
OnChanges,
1011
OnDestroy,
1112
OnInit,
13+
viewChildren,
1214
} from '@angular/core';
13-
import { EFConnectionBehavior, EFConnectionConnectableSide, EFConnectionType } from '../common';
15+
import {
16+
EFConnectionBehavior,
17+
EFConnectionConnectableSide,
18+
EFConnectionType,
19+
FConnectionDragHandleControlPointComponent,
20+
} from '../common';
1421
import { NotifyDataChangedRequest } from '../../f-storage';
1522
import { F_CONNECTION } from '../common/f-connection.injection-token';
1623
//TODO: Need to deal with cyclic dependencies, since in some cases an error occurs when importing them ../common
@@ -99,6 +106,37 @@ export class FConnectionComponent extends FConnectionBase implements OnInit, OnC
99106

100107
private readonly _mediator = inject(FMediator);
101108

109+
public readonly fDragHandleControlPoints = viewChildren(
110+
FConnectionDragHandleControlPointComponent,
111+
);
112+
113+
public readonly controlPointsToRender = computed(() => {
114+
// Only render control points for custom-path connections
115+
if (this.fType !== EFConnectionType.CUSTOM_PATH) {
116+
return [];
117+
}
118+
return this.fControlPoints;
119+
});
120+
121+
public override redraw(): void {
122+
super.redraw();
123+
// Redraw control point handles for custom-path connections
124+
if (this.fType === EFConnectionType.CUSTOM_PATH) {
125+
this._redrawControlPointHandles();
126+
}
127+
}
128+
129+
private _redrawControlPointHandles(): void {
130+
const handles = this.fDragHandleControlPoints();
131+
const controlPoints = this.fControlPoints;
132+
133+
handles.forEach((handle, index) => {
134+
if (index < controlPoints.length) {
135+
handle.redraw(controlPoints[index]);
136+
}
137+
});
138+
}
139+
102140
public ngOnInit(): void {
103141
this._mediator.execute(new AddConnectionToStoreRequest(this));
104142
}

projects/f-flow/src/f-connection/providers.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import {
2-
FConnectionDragHandleEndComponent, FConnectionDragHandleStartComponent,
3-
FConnectionGradientComponent, FConnectionPathComponent, FConnectionSelectionComponent,
2+
FConnectionDragHandleEndComponent,
3+
FConnectionDragHandleStartComponent,
4+
FConnectionDragHandleControlPointComponent,
5+
FConnectionGradientComponent,
6+
FConnectionPathComponent,
7+
FConnectionSelectionComponent,
48
FConnectionTextComponent,
59
FConnectionTextPathDirective,
610
} from './common';
@@ -16,6 +20,7 @@ export const F_CONNECTION_PROVIDERS = [
1620
FConnectionTextPathDirective,
1721
FConnectionDragHandleStartComponent,
1822
FConnectionDragHandleEndComponent,
23+
FConnectionDragHandleControlPointComponent,
1924
FConnectionGradientComponent,
2025
FConnectionPathComponent,
2126
FConnectionSelectionComponent,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { inject, Injectable, Injector } from '@angular/core';
2+
import { FDragControlPointPreparationRequest } from './f-drag-control-point-preparation.request';
3+
import { IPoint, ITransformModel, Point } from '@foblex/2d';
4+
import { FComponentsStore } from '../../../../f-storage';
5+
import { FDraggableDataContext } from '../../../f-draggable-data-context';
6+
import { isValidEventTrigger, UpdateItemAndChildrenLayersRequest } from '../../../../domain';
7+
import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator';
8+
import { FConnectionBase } from '../../../../f-connection';
9+
import { FDragControlPointDragHandler } from '../f-drag-control-point.drag-handler';
10+
import { isPointerInsideControlPoint } from './is-pointer-inside-control-point';
11+
12+
@Injectable()
13+
@FExecutionRegister(FDragControlPointPreparationRequest)
14+
export class FDragControlPointPreparationExecution
15+
implements IExecution<FDragControlPointPreparationRequest, void>
16+
{
17+
private readonly _fMediator = inject(FMediator);
18+
private readonly _store = inject(FComponentsStore);
19+
private readonly _dragContext = inject(FDraggableDataContext);
20+
private readonly _injector = inject(Injector);
21+
22+
private _fConnection: FConnectionBase | undefined;
23+
private _controlPointIndex: number = -1;
24+
25+
private get _transform(): ITransformModel {
26+
return this._store.fCanvas!.transform;
27+
}
28+
29+
private get _fHost(): HTMLElement {
30+
return this._store.fFlow!.hostElement;
31+
}
32+
33+
private get _fConnections(): FConnectionBase[] {
34+
return this._store.fConnections;
35+
}
36+
37+
public handle(request: FDragControlPointPreparationRequest): void {
38+
const position = this._getPointInFlow(request);
39+
if (!this._isValid(position) || !this._isValidTrigger(request)) {
40+
return;
41+
}
42+
43+
this._dragContext.onPointerDownScale = this._transform.scale;
44+
this._dragContext.onPointerDownPosition = Point.fromPoint(request.event.getPosition())
45+
.elementTransform(this._fHost)
46+
.div(this._transform.scale);
47+
48+
this._dragContext.draggableItems = [
49+
new FDragControlPointDragHandler(
50+
this._injector,
51+
this._fConnection!,
52+
this._controlPointIndex,
53+
),
54+
];
55+
56+
setTimeout(() => this._updateConnectionLayer());
57+
}
58+
59+
private _isValid(position: IPoint): boolean {
60+
const result = this._getConnectionWithControlPoint(position);
61+
this._fConnection = result.connection;
62+
this._controlPointIndex = result.controlPointIndex;
63+
64+
return !!this._fConnection && this._controlPointIndex >= 0 && !this._dragContext.draggableItems.length;
65+
}
66+
67+
private _isValidTrigger(request: FDragControlPointPreparationRequest): boolean {
68+
return isValidEventTrigger(request.event.originalEvent, request.fTrigger);
69+
}
70+
71+
private _getPointInFlow(request: FDragControlPointPreparationRequest): IPoint {
72+
return Point.fromPoint(request.event.getPosition())
73+
.elementTransform(this._fHost)
74+
.sub(this._transform.scaledPosition)
75+
.sub(this._transform.position)
76+
.div(this._transform.scale);
77+
}
78+
79+
private _getConnectionWithControlPoint(
80+
position: IPoint,
81+
): { connection: FConnectionBase | undefined; controlPointIndex: number } {
82+
for (const connection of this._fConnections) {
83+
const result = isPointerInsideControlPoint(connection, position);
84+
if (result.isInside) {
85+
return { connection, controlPointIndex: result.controlPointIndex };
86+
}
87+
}
88+
89+
return { connection: undefined, controlPointIndex: -1 };
90+
}
91+
92+
private _updateConnectionLayer(): void {
93+
this._fMediator.execute<void>(
94+
new UpdateItemAndChildrenLayersRequest(
95+
this._fConnection!,
96+
this._store.fCanvas!.fConnectionsContainer().nativeElement,
97+
),
98+
);
99+
}
100+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { FEventTrigger } from '../../../../domain';
2+
import { IPointerEvent } from '../../../../drag-toolkit';
3+
4+
export class FDragControlPointPreparationRequest {
5+
constructor(
6+
public event: IPointerEvent,
7+
public fTrigger: FEventTrigger,
8+
) {}
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './f-drag-control-point-preparation.request';
2+
export * from './f-drag-control-point-preparation.execution';
3+
export * from './is-pointer-inside-control-point';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { IPoint } from '@foblex/2d';
2+
import { FConnectionBase } from '../../../../f-connection';
3+
import { EFConnectionType } from '../../../../f-connection/common';
4+
5+
export function isPointerInsideControlPoint(
6+
connection: FConnectionBase,
7+
position: IPoint,
8+
): { isInside: boolean; controlPointIndex: number } {
9+
// Only check for custom-path connections
10+
if (connection.fType !== EFConnectionType.CUSTOM_PATH || connection.fDraggingDisabled()) {
11+
return { isInside: false, controlPointIndex: -1 };
12+
}
13+
14+
const controlPoints = connection.fControlPoints;
15+
16+
// Check each control point
17+
for (let i = 0; i < controlPoints.length; i++) {
18+
if (_isPointInsideCircle(position, controlPoints[i])) {
19+
return { isInside: true, controlPointIndex: i };
20+
}
21+
}
22+
23+
return { isInside: false, controlPointIndex: -1 };
24+
}
25+
26+
function _isPointInsideCircle(point: IPoint, circleCenter: IPoint): boolean {
27+
return (point.x - circleCenter.x) ** 2 + (point.y - circleCenter.y) ** 2 <= 8 ** 2;
28+
}

0 commit comments

Comments
 (0)