diff --git a/.env.example b/.env.example deleted file mode 100644 index 2ed2288c..00000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -GITHUB_CLIENT_ID=your_github_client_id -GITHUB_CLIENT_SECRET=your_github_client_secret -GITHUB_CALLBACK_URL=https://flow.foblex.com/auth/github/callback -JWT_SECRET=your_secret_token diff --git a/.github/git-commit-instructions.md b/.github/git-commit-instructions.md new file mode 100644 index 00000000..ce669e70 --- /dev/null +++ b/.github/git-commit-instructions.md @@ -0,0 +1,17 @@ +Write your commit message in the Conventional Commits format. + +Requirements: + +- Title: (): +- Use the following types: feat, fix, refactor, perf, docs, test, chore. +- Scope: module/feature name (e.g., auth, payments, flow-editor). +- Title up to 72 characters, without a period at the end. + +Body (required if there is more than one change): + +- 3–7 bullet points, each with a past tense verb. +- Indicate important behavior changes, migrations, and removed fields/methods. +- If there is a BREAKING CHANGE, add it as a separate section "BREAKING CHANGE:". + +If the change log shows the issue number (JIRA/GH issue), add the following footer at the end: "Refs: XXX-123". +Don't invent things that aren't in the diff. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc7fd11..177b0d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,74 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [18.0.0](https://github.com/foblex/flow/compare/v17.9.5...v18.0.0) (2026-01-26) + +### Highlights + +- **Connection Waypoints (new feature)** — interactive waypoint editing for connections with candidate points and drag-and-drop. +- **Pinch-to-zoom (new feature)** — smooth multi-touch zoom for trackpads and touch devices. +- **Control Flow + Content Projection compatibility** — improved support for `@if/@for` rendering by extending projection slots for nodes/connections. +- **Custom Backgrounds** — richer SVG pattern support plus a new example for advanced backgrounds. + +### Features + +- **zoom:** add pinch-to-zoom support for touch/trackpad gestures ([5dfeeb5](https://github.com/foblex/flow/commit/5dfeeb5f16dcf9dfbcf4ee38dfea22ded1c37f83)) +- **connection:** introduce **waypoints** with candidates + drag-to-add / drag-to-move interactions and demo example ([19772df](https://github.com/foblex/flow/commit/19772df7649e66b4115bd6a057613d3fa25faeec), [229d6e3](https://github.com/foblex/flow/commit/229d6e3b773a64a1e55654c373e3b556a8d38c8e)) +- **connection:** add anchors utility and refactor connection builders + candidate generation ([3276baf](https://github.com/foblex/flow/commit/3276baf8405eec062022b285524e3c641d901424), [b18109a](https://github.com/foblex/flow/commit/b18109a2c26fdac7aa8e5980fb5bbd791d3a4b42)) +- **canvas:** improve content projection for Angular Control Flow usage (`@if/@for`) by supporting grouped slots (`[fNodes]`, `[fConnections]`) ([b18109a](https://github.com/foblex/flow/commit/b18109a2c26fdac7aa8e5980fb5bbd791d3a4b42)) +- **background:** support custom/complex SVG patterns + add a dedicated example ([29e28c8](https://github.com/foblex/flow/commit/29e28c81faf9da5189d9bf108323f4f4deb9387d)) +- **showcase:** add AI Low Code Platform entry and links ([6013ee7](https://github.com/foblex/flow/commit/6013ee7c23f5d88d8861027b56618b4bffc9fce2), [dd84ae8](https://github.com/foblex/flow/commit/dd84ae8730d8296a56d47ce5fb4966cc4de55e4b)) + +### Improvements + +- Refactored multiple connection/interaction modules to improve readability, maintainability and internal consistency. +- Candidate/waypoint handling is now more predictable across all connection types. + +### Documentation + +- fix url to custom-connection-type component in guide ([a6421ac](https://github.com/foblex/flow/commit/a6421aca5fd3ba2576a7f155e5ad8b06c5af9ed7)) + +### ⚠️ Breaking Changes + +#### 1) Custom connection builders: `IFConnectionBuilderResponse` updated + +If you implemented a custom connection type / builder, the response interface changed: + +- `connectionCenter` was **removed** +- `points` is now **required** (was optional) +- `candidates` was **added** + +✅ **Note:** you don’t have to calculate or return `points` or `candidates` yourself. +If you don’t support them, return empty arrays. + +```ts +export interface IFConnectionBuilderResponse { + path: string; + penultimatePoint: IPoint; + secondPoint: IPoint; + points: IPoint[]; // can be [] + candidates: IPoint[]; // can be [] +} +``` + +#### 2) FConnection API cleanup: removed deprecated inputs + +Deprecated connection inputs were removed from FConnectionComponent: + +- `fText` - removed (use FConnectionContent instead) +- `fTextStartOffset` - removed (use FConnectionContent instead) + +Removed legacy directive: + +- [fConnectionCenter] - removed (use FConnectionContent instead) + +#### 3) FCanvas API cleanup + +Removed deprecated zoom aliases from FCanvasComponent: + +- setZoom(...) - removed → use setScale(...) +- resetZoom() - removed → use resetScale() + ### [17.9.5](https://github.com/foblex/flow/compare/v17.8.0...v17.9.5) (2025-10-27) ### Features diff --git a/cypress/e2e/assign-node-to-connection-on-drop.component.cy.ts b/cypress/e2e/assign-node-to-connection-on-drop.component.cy.ts deleted file mode 100644 index 7db7610e..00000000 --- a/cypress/e2e/assign-node-to-connection-on-drop.component.cy.ts +++ /dev/null @@ -1,32 +0,0 @@ -// describe('AssignNodeToConnectionOnDropComponent', () => { -// beforeEach(() => { -// cy.visit('http://localhost:4200/examples/assign-node-to-connection-on-drop'); -// cy.get('f-flow').scrollIntoView(); -// }) -// -// it('should drag node to connection and connect it', function () { -// cy.get('f-flow').scrollIntoView(); -// -// cy.get('#connection_112').should('exist'); -// -// cy.get('div[data-f-input-id=\'1\']').should('exist'); -// cy.get('div[data-f-input-id=\'2\']').should('exist'); -// cy.get('div[data-f-input-id=\'3\']').should('exist'); -// -// cy.get('div[data-f-node-id="3"]').then(($target) => { -// const targetRect = $target[ 0 ].getBoundingClientRect(); -// const endY = targetRect.y + targetRect.height / 2; -// -// cy.get('div[data-f-node-id="3"]') -// .trigger('mousedown', { button: 0, clientY: endY, force: true }) -// .trigger('mousemove', { button: 0, clientY: endY + 140, force: true }) -// .trigger('pointerup', { force: true }); -// -// cy.get('#connection_232').should('exist'); -// cy.get('#connection_113').should('exist'); -// }); -// }); -// }) -// -// -// diff --git a/cypress/e2e/drag-handle.component.cy.ts b/cypress/e2e/drag-handle.component.cy.ts deleted file mode 100644 index a2f3e640..00000000 --- a/cypress/e2e/drag-handle.component.cy.ts +++ /dev/null @@ -1,53 +0,0 @@ -describe('DragHandleComponent', () => { - beforeEach(() => { - cy.visit('http://localhost:4200/examples/drag-handle'); - cy.get('f-flow').scrollIntoView(); - }); - - it('should drag fNode element and update its transform translate', function () { - - cy.wait(500).get('.f-node.f-drag-handle').then(($dragHandle: JQuery) => { - const dragHandleRect = $dragHandle.get(0).getBoundingClientRect(); - - cy.get('.f-node.f-drag-handle') - .first() - .trigger('mousedown', {button: 0, force: true}) - .trigger('mousemove', {clientX: -250, clientY: 0}) - .trigger('pointerup', {clientX: 0, clientY: 0}); - - cy.wait(1500).get('.f-node.f-drag-handle') - .first().then(($dragHandle2: JQuery) => { - const dragHandleRect2 = $dragHandle2.get(0).getBoundingClientRect(); - expect(Math.round(dragHandleRect.x + 250)).to.equal(Math.round(dragHandleRect2.x)); - }) - }); - }); - - it('should click fNode element and update its selection state', function () { - cy.wait(500).get('.f-node.f-drag-handle') - .should('not.have.class', 'f-selected'); - - cy.wait(500).get('.f-node.f-drag-handle').click(); - - cy.wait(500).get('.f-node.f-drag-handle').should('have.class', 'f-selected'); - }); - - it('should drag fCanvas element and update its transform translate', function () { - cy.get('f-flow').first().then(($flow) => { - cy.get('f-canvas').first().then(($canvas) => { - const transform = $canvas.css('transform'); - - cy.wrap($flow) - .trigger('mousedown', {clientX: 10, clientY: 10, button: 0, force: true}) - .trigger('mousemove', {clientX: 0, clientY: 0}) - .trigger('mousemove', {clientX: 20, clientY: 20}) - .trigger('pointerup', {clientX: 30, clientY: 30, force: true}); - - cy.wrap($canvas).invoke('css', 'transform') - .should('not.equal', transform); - }); - }); - }); -}); - - diff --git a/cypress/e2e/drag-handle.cy.ts b/cypress/e2e/drag-handle.cy.ts new file mode 100644 index 00000000..ddb61450 --- /dev/null +++ b/cypress/e2e/drag-handle.cy.ts @@ -0,0 +1,56 @@ +describe('DragHandle', () => { + beforeEach(() => { + cy.visit('http://localhost:4200/examples/drag-handle'); + cy.get('f-flow').scrollIntoView(); + }); + + it('should drag fNode element and update its transform translate', function () { + cy.wait(500) + .get('.f-node.f-drag-handle') + .then(($dragHandle: JQuery) => { + const dragHandleRect = $dragHandle.get(0).getBoundingClientRect(); + + cy.get('.f-node.f-drag-handle') + .first() + .trigger('mousedown', { button: 0, force: true }) + .trigger('mousemove', { clientX: -250, clientY: 0 }) + .trigger('pointerup', { clientX: 0, clientY: 0 }); + + cy.wait(1500) + .get('.f-node.f-drag-handle') + .first() + .then(($dragHandle2: JQuery) => { + const dragHandleRect2 = $dragHandle2.get(0).getBoundingClientRect(); + expect(Math.round(dragHandleRect.x + 250)).to.equal(Math.round(dragHandleRect2.x)); + }); + }); + }); + + it('should click fNode element and update its selection state', function () { + cy.wait(500).get('.f-node.f-drag-handle').should('not.have.class', 'f-selected'); + + cy.wait(500).get('.f-node.f-drag-handle').click(); + + cy.wait(500).get('.f-node.f-drag-handle').should('have.class', 'f-selected'); + }); + + it('should drag fCanvas element and update its transform translate', function () { + cy.get('f-flow') + .first() + .then(($flow) => { + cy.get('f-canvas') + .first() + .then(($canvas) => { + const transform = $canvas.css('transform'); + + cy.wrap($flow) + .trigger('mousedown', { clientX: 10, clientY: 10, button: 0, force: true }) + .trigger('mousemove', { clientX: 0, clientY: 0 }) + .trigger('mousemove', { clientX: 20, clientY: 20 }) + .trigger('pointerup', { clientX: 30, clientY: 30, force: true }); + + cy.wrap($canvas).invoke('css', 'transform').should('not.equal', transform); + }); + }); + }); +}); diff --git a/cypress/e2e/drag-to-connect.component.cy.ts b/cypress/e2e/drag-to-connect.cy.ts similarity index 55% rename from cypress/e2e/drag-to-connect.component.cy.ts rename to cypress/e2e/drag-to-connect.cy.ts index cd41bcfb..27b22591 100644 --- a/cypress/e2e/drag-to-connect.component.cy.ts +++ b/cypress/e2e/drag-to-connect.cy.ts @@ -1,8 +1,8 @@ -describe('DragToConnectComponent', () => { +describe('DragToConnect', () => { beforeEach(() => { cy.visit('http://localhost:4200/examples/drag-to-connect'); cy.get('f-flow').scrollIntoView(); - }) + }); it('should start creating a connection and show connection-for-create element', function () { cy.get('.f-node-output').then(($output) => { @@ -10,20 +10,27 @@ describe('DragToConnectComponent', () => { const startX = outputRect.left + outputRect.width / 2; const startY = outputRect.top + outputRect.height / 2; - cy.get('.f-connection-for-create').should('exist') - .invoke('css', 'display').should('equal', 'none'); + cy.get('.f-connection-for-create') + .should('exist') + .invoke('css', 'display') + .should('equal', 'none'); cy.wrap($output) - .trigger('mousedown', {button: 0, clientX: startX, clientY: startY, force: true}) - .trigger('mousemove', {clientX: startX + 100, clientY: startY + 100, force: true}) + .trigger('mousedown', { button: 0, clientX: startX, clientY: startY, force: true }) + .trigger('mousemove', { clientX: startX + 100, clientY: startY + 100, force: true }) .wait(500) - .get('.f-connection-for-create').should('exist') - .invoke('css', 'display').should('equal', 'block') + .get('.f-connection-for-create') + .should('exist') + .invoke('css', 'display') + .should('equal', 'block') .wrap($output) .trigger('pointerup'); - cy.wait(500).get('.f-connection-for-create').should('exist') - .invoke('css', 'display').should('equal', 'none'); + cy.wait(500) + .get('.f-connection-for-create') + .should('exist') + .invoke('css', 'display') + .should('equal', 'none'); }); }); @@ -41,15 +48,12 @@ describe('DragToConnectComponent', () => { const endX = inputRect.left + inputRect.width / 2; cy.wrap($output) - .trigger('mousedown', {button: 0, clientX: startX, clientY: startY, force: true}) - .trigger('mousemove', {clientX: endX, clientY: endY, force: true}) - .trigger('pointerup', {clientX: endX, clientY: endY, force: true}); + .trigger('mousedown', { button: 0, clientX: startX, clientY: startY, force: true }) + .trigger('mousemove', { clientX: endX, clientY: endY, force: true }) + .trigger('pointerup', { clientX: endX, clientY: endY, force: true }); - cy.get('#f-connection-0', {timeout: 2000}).should('exist'); + cy.get('#f-connection-0', { timeout: 2000 }).should('exist'); }); }); }); -}) - - - +}); diff --git a/cypress/e2e/drag-to-reassign.component.cy.ts b/cypress/e2e/drag-to-reassign.component.cy.ts deleted file mode 100644 index 4122c88c..00000000 --- a/cypress/e2e/drag-to-reassign.component.cy.ts +++ /dev/null @@ -1,36 +0,0 @@ -describe('DragToReassignComponent', () => { - beforeEach(() => { - cy.visit('http://localhost:4200/examples/drag-to-reassign'); - cy.get('f-flow').scrollIntoView(); - }) - - it('should drag from input to another input and reassign the connection', function () { - cy.get('#connection_113').should('exist'); - - cy.get('div[data-f-input-id="3"]').should('exist'); - cy.get('div[data-f-input-id="4"]').should('exist'); - - cy.get('.f-connection-drag-handle').first().then(($handle) => { - const handleRect = $handle[0].getBoundingClientRect(); - const startY = handleRect.top + handleRect.height / 2; - const startX = handleRect.left + handleRect.width / 2; - - cy.get('div[data-f-input-id="4"]').first().then(($input2) => { - const input2Rect = $input2[ 0 ].getBoundingClientRect(); - const endY = input2Rect.top + input2Rect.height / 2; - const endX = input2Rect.left + input2Rect.width / 2; - - cy.get('.f-connection-drag-handle') - .trigger('mousedown', { button: 0, clientY: startY, clientX: startX, force: true }) - .trigger('mousemove', { clientY: endY, clientX: endX, force: true }) - .trigger('pointerup', { clientY: endY, clientX: endX, force: true }); - - cy.get('#connection_113').should('not.exist'); - cy.get('#connection_114').should('exist'); - }); - }); - }); -}) - - - diff --git a/cypress/e2e/drag-to-reassign.cy.ts b/cypress/e2e/drag-to-reassign.cy.ts new file mode 100644 index 00000000..2d9e103a --- /dev/null +++ b/cypress/e2e/drag-to-reassign.cy.ts @@ -0,0 +1,37 @@ +describe('DragToReassign', () => { + beforeEach(() => { + cy.visit('http://localhost:4200/examples/drag-to-reassign'); + cy.get('f-flow').scrollIntoView(); + }); + + it('should drag from input to another input and reassign the connection', function () { + cy.get('#connection_113').should('exist'); + + cy.get('div[data-f-input-id="3"]').should('exist'); + cy.get('div[data-f-input-id="4"]').should('exist'); + + cy.get('.f-connection-drag-handle') + .first() + .then(($handle) => { + const handleRect = $handle[0].getBoundingClientRect(); + const startY = handleRect.top + handleRect.height / 2; + const startX = handleRect.left + handleRect.width / 2; + + cy.get('div[data-f-input-id="4"]') + .first() + .then(($input2) => { + const input2Rect = $input2[0].getBoundingClientRect(); + const endY = input2Rect.top + input2Rect.height / 2; + const endX = input2Rect.left + input2Rect.width / 2; + + cy.get('.f-connection-drag-handle') + .trigger('mousedown', { button: 0, clientY: startY, clientX: startX, force: true }) + .trigger('mousemove', { clientY: endY, clientX: endX, force: true }) + .trigger('pointerup', { clientY: endY, clientX: endX, force: true }); + + cy.get('#connection_113').should('not.exist'); + cy.get('#connection_114').should('exist'); + }); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 675bdfe9..459a2e9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "f-flow", - "version": "17.9.5", + "version": "18.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "version": "17.9.5", + "version": "18.0.0", "license": "MIT", "dependencies": { "@angular/animations": "18.2.13", diff --git a/package.json b/package.json index d0930d0f..f9c27c9d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "17.9.5", + "version": "18.0.0", "description": "An Angular library designed to simplify the creation and manipulation of dynamic flow. Provides components for flows, nodes, and connections, automating node manipulation and inter-node connections.", "author": "Siarhei Huzarevich", "homepage": "https://flow.foblex.com", @@ -84,15 +84,11 @@ "@foblex/mutator": "^1.0.7", "@foblex/platform": "1.0.4", "@foblex/utils": "1.1.1", - "axios": "^1.11.0", "chokidar": "^4.0.3", - "cookie-parser": "^1.4.7", "dagre": "^0.8.5", - "dotenv": "^17.2.1", "elkjs": "^0.9.3", "express": "^4.21.2", "html2canvas": "^1.4.1", - "jsonwebtoken": "^9.0.2", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.3" @@ -103,12 +99,10 @@ "@angular/compiler-cli": "18.2.13", "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", - "@types/cookie-parser": "^1.4.9", "@types/dagre": "^0.7.52", "@types/express": "^4.17.17", "@types/html2canvas": "^1.0.0", "@types/jasmine": "~5.1.0", - "@types/jsonwebtoken": "^9.0.10", "@types/node": "^18.18.0", "angular-eslint": "20.2.0", "copyfiles": "2.4.1", diff --git a/projects/f-examples/advanced/undo-redo-v2/undo-redo-v2.html b/projects/f-examples/advanced/undo-redo-v2/undo-redo-v2.html index 79207ac1..d762bd04 100644 --- a/projects/f-examples/advanced/undo-redo-v2/undo-redo-v2.html +++ b/projects/f-examples/advanced/undo-redo-v2/undo-redo-v2.html @@ -1,64 +1,123 @@ @let model = viewModel(); -@if (model) { - - - - - - - + + + + + + + - - - - - + + + + + - @for (connection of connections(); track connection.id) { - - - - - - - - - } - @for (node of nodes(); track node.id; let index = $index) { -
{{ node.text }} -
-
-
-
-
- } -
-
-} + @for (connection of connections(); track connection.id) { + + + + + + + + + } @for (node of nodes(); track node.id; let index = $index) { +
+ {{ node.text }} +
+
+
+
+
+ } +
+
diff --git a/projects/f-examples/advanced/undo-redo-v2/undo-redo-v2.ts b/projects/f-examples/advanced/undo-redo-v2/undo-redo-v2.ts index b547a943..382a3573 100644 --- a/projects/f-examples/advanced/undo-redo-v2/undo-redo-v2.ts +++ b/projects/f-examples/advanced/undo-redo-v2/undo-redo-v2.ts @@ -1,20 +1,30 @@ import { ChangeDetectionStrategy, - Component, computed, effect, inject, Injectable, Injector, - OnInit, signal, untracked, + Component, + computed, + effect, + inject, + Injectable, + Injector, + OnInit, + signal, + untracked, viewChild, } from '@angular/core'; import { EFMarkerType, FCanvasChangeEvent, FCanvasComponent, - FCreateConnectionEvent, FFlowComponent, - FFlowModule, FMoveNodesEvent, - FReassignConnectionEvent, FSelectionChangeEvent + FCreateConnectionEvent, + FFlowComponent, + FFlowModule, + FMoveNodesEvent, + FReassignConnectionEvent, + FSelectionChangeEvent, } from '@foblex/flow'; -import {IPoint} from '@foblex/2d'; -import {Mutator} from "@foblex/mutator"; -import {generateGuid} from "@foblex/utils"; +import { IPoint } from '@foblex/2d'; +import { Mutator } from '@foblex/mutator'; +import { generateGuid } from '@foblex/utils'; interface INode { id: string; @@ -34,41 +44,40 @@ interface IState { selection?: { nodes: string[]; connections: string[]; - }, + }; transform?: { position: IPoint; scale: number; - } + }; } @Injectable() -class FlowState extends Mutator { -} +class FlowState extends Mutator {} const DEFAULT_STATE: IState = { nodes: { ['node1']: { id: 'node1', - position: {x: 0, y: 200}, + position: { x: 0, y: 200 }, text: 'Node 1', }, ['node2']: { id: 'node2', - position: {x: 200, y: 200}, + position: { x: 200, y: 200 }, text: 'Node 2', - } + }, }, connections: { ['connection1']: { id: 'connection1', source: 'node1-output-0', target: 'node2-input-1', - } + }, }, transform: { - position: {x: 0, y: 0}, + position: { x: 0, y: 0 }, scale: 1, - } + }, }; @Component({ @@ -78,9 +87,7 @@ const DEFAULT_STATE: IState = { changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, providers: [FlowState], - imports: [ - FFlowModule - ] + imports: [FFlowModule], }) export class UndoRedoV2 implements OnInit { protected readonly state = inject(FlowState); @@ -112,10 +119,13 @@ export class UndoRedoV2 implements OnInit { } private _listenStateChanges(): void { - effect(() => { - this.state.changes(); - untracked(() => this._applyChanges()); - }, {injector: this._injector}); + effect( + () => { + this.state.changes(); + untracked(() => this._applyChanges()); + }, + { injector: this._injector }, + ); } private _applyChanges(): void { @@ -133,7 +143,7 @@ export class UndoRedoV2 implements OnInit { } } - private _applySelectionChanges({selection}: IState): void { + private _applySelectionChanges({ selection }: IState): void { this._flow()?.select(selection?.nodes || [], selection?.connections || [], false); } @@ -149,11 +159,12 @@ export class UndoRedoV2 implements OnInit { private _ifCanvasChangedFromInitialReCenterUpdateInitialState(event: FCanvasChangeEvent): void { if (this._isChangeAfterLoadedResetAndCenter) { this._isChangeAfterLoadedResetAndCenter = false; - this.state.patchBase({transform: {...event}}); + this.state.patchBase({ transform: { ...event } }); + return; } this.state.update({ - transform: createTransformObject(event) + transform: createTransformObject(event), }); } @@ -162,8 +173,8 @@ export class UndoRedoV2 implements OnInit { const connection = createConnectionObject(event); this.state.create({ connections: { - [connection.id]: connection - } + [connection.id]: connection, + }, }); } } @@ -172,15 +183,15 @@ export class UndoRedoV2 implements OnInit { if (event.newTargetId) { this.state.update({ connections: { - [event.connectionId]: {target: event.newTargetId} - } + [event.connectionId]: { target: event.newTargetId }, + }, }); } } protected moveNodes(event: FMoveNodesEvent): void { this.state.update({ - nodes: createMoveNodesChangeObject(event.fNodes) + nodes: createMoveNodesChangeObject(event.fNodes), }); } @@ -189,23 +200,23 @@ export class UndoRedoV2 implements OnInit { selection: { nodes: [...event.fNodeIds], connections: [...event.fConnectionIds], - } + }, }); } } -function createTransformObject({position, scale}: FCanvasChangeEvent) { - return {position, scale}; +function createTransformObject({ position, scale }: FCanvasChangeEvent) { + return { position, scale }; } -function createConnectionObject({fOutputId, fInputId}: FCreateConnectionEvent) { +function createConnectionObject({ fOutputId, fInputId }: FCreateConnectionEvent) { return { - id: generateGuid(), source: fOutputId, target: fInputId! - } + id: generateGuid(), + source: fOutputId, + target: fInputId!, + }; } -function createMoveNodesChangeObject(nodes: { id: string; position: IPoint; }[]) { - return Object.fromEntries( - nodes.map(({id, position}) => [id, {position}]) - ); +function createMoveNodesChangeObject(nodes: { id: string; position: IPoint }[]) { + return Object.fromEntries(nodes.map(({ id, position }) => [id, { position }])); } diff --git a/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component.html b/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component.html deleted file mode 100644 index 65d18961..00000000 --- a/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component.html +++ /dev/null @@ -1,26 +0,0 @@ - - - @for (connection of connections; track connection.inputId) { - - - } - - @for (node of nodes; track node.id) { -
-
-
- @if (node.isConnected) { - I'm connected node - } @else { - Drag me to connection - } -
- } -
-
diff --git a/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component.ts b/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component.ts deleted file mode 100644 index ba783670..00000000 --- a/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, inject, ViewChild } from '@angular/core'; -import { - FCanvasComponent, - FFlowComponent, - FFlowModule, FNodeIntersectedWithConnections, -} from '@foblex/flow'; -import { IPoint } from '@foblex/2d'; - -@Component({ - selector: 'assign-node-to-connection-on-drop', - styleUrls: [ './assign-node-to-connection-on-drop.component.scss' ], - templateUrl: './assign-node-to-connection-on-drop.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - FFlowModule - ] -}) -export class AssignNodeToConnectionOnDropComponent { - - private _elementReference = inject(ElementRef); - private _changeDetectorRef = inject(ChangeDetectorRef); - - @ViewChild(FCanvasComponent, { static: true }) - public fCanvas!: FCanvasComponent; - - @ViewChild(FFlowComponent, { static: true }) - public fFlowComponent!: FFlowComponent; - - public nodes: { id: string, isConnected: boolean, position: IPoint }[] = [ { - id: '1', - isConnected: true, - position: { x: 0, y: 0 } - }, { - id: '2', - isConnected: true, - position: { x: 400, y: 0 } - }, { - id: '3', - isConnected: false, - position: { x: 200, y: 200 } - } ]; - - public connections: { id: string, outputId: string, inputId: string }[] = [ - - { id: '1', outputId: this.nodes[ 0 ].id, inputId: this.nodes[ 1 ].id } - ]; - - public onNodeIntersectedWithConnection(event: FNodeIntersectedWithConnections): void { - const node = this.nodes.find((x) => x.id === event.fNodeId); - const connection = this.connections.find((x) => x.id === event.fConnectionIds[0]); - - const previousInputId = connection!.inputId; - connection!.inputId = event.fNodeId; - - this.connections.push({ - id: '2', - outputId: event.fNodeId, - inputId: previousInputId - }); - - node!.isConnected = true; - - this._changeDetectorRef.detectChanges(); - } - - public onLoaded(): void { - this.fCanvas.resetScaleAndCenter(false); - } -} diff --git a/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.html b/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.html new file mode 100644 index 00000000..60b375f8 --- /dev/null +++ b/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.html @@ -0,0 +1,25 @@ + + + @for (connection of connections(); track $index) { + + + } @for (node of nodes; track node.id) { +
+
+
+ @if (node.connected) { I'm connected node } @else { Drag me to connection } +
+ } +
+
diff --git a/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component.scss b/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.scss similarity index 83% rename from projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component.scss rename to projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.scss index eb2374f4..6a501c4c 100644 --- a/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component.scss +++ b/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.scss @@ -1,4 +1,4 @@ -@use "../../flow-common"; +@use '../../flow-common'; ::ng-deep f-flow { @include flow-common.connection; diff --git a/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.ts b/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.ts new file mode 100644 index 00000000..5e983fdd --- /dev/null +++ b/projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.ts @@ -0,0 +1,83 @@ +import { ChangeDetectionStrategy, Component, signal, viewChild } from '@angular/core'; +import { FCanvasComponent, FFlowModule, FNodeIntersectedWithConnections } from '@foblex/flow'; + +@Component({ + selector: 'assign-node-to-connection-on-drop', + styleUrls: ['./assign-node-to-connection-on-drop.scss'], + templateUrl: './assign-node-to-connection-on-drop.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [FFlowModule], +}) +export class AssignNodeToConnectionOnDrop { + private readonly _canvas = viewChild.required(FCanvasComponent); + + protected readonly nodes = [ + { + id: '1', + connected: true, + position: { x: 0, y: 0 }, + }, + { + id: '2', + connected: true, + position: { x: 400, y: 0 }, + }, + { + id: '3', + position: { x: 200, y: 200 }, + }, + ]; + + protected readonly connections = signal([ + { id: '1', source: this.nodes[0].id, target: this.nodes[1].id }, + ]); + + protected nodeIntersectedWithConnection({ + fNodeId, + fConnectionIds, + }: FNodeIntersectedWithConnections): void { + const connectionId = fConnectionIds?.[0]; + if (!connectionId) { + throw new Error('Connection not found'); + } + + const node = this.nodes.find((x) => x.id === fNodeId); + if (!node) { + throw new Error('Node not found'); + } + + const lastTarget = this._updateCurrentConnection(connectionId, fNodeId); + + this.connections.update((x) => [ + ...x, + { + id: '2', + source: fNodeId, + target: lastTarget, + }, + ]); + + node.connected = true; + } + + private _updateCurrentConnection(id: string, newTarget: string): string { + let lastTarget = ''; + this.connections.update((x) => { + const connection = x.find((c) => c.id === id); + if (!connection) { + throw new Error('Connection not found'); + } + lastTarget = connection.target; + connection.target = newTarget; + + return x; + }); + + return lastTarget; + } + + protected loaded(): void { + this._canvas()?.resetScaleAndCenter(false); + } +} diff --git a/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.html b/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.html deleted file mode 100644 index 9295b047..00000000 --- a/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.html +++ /dev/null @@ -1,28 +0,0 @@ - - - -
- I'm a node -
-
- I'm a node -
- - -
- I'm a node -
-
- I'm a node -
- - - -
- I'm a node -
-
- I'm a node -
-
-
diff --git a/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.ts b/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.ts deleted file mode 100644 index 253863ce..00000000 --- a/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; -import { FCanvasComponent, FFlowModule } from '@foblex/flow'; - -@Component({ - selector: 'connection-behaviours', - styleUrls: [ './connection-behaviours.component.scss' ], - templateUrl: './connection-behaviours.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - FFlowModule - ] -}) -export class ConnectionBehavioursComponent { - - @ViewChild(FCanvasComponent, { static: true }) - public fCanvas!: FCanvasComponent; - - public onLoaded(): void { - this.fCanvas.resetScaleAndCenter(false); - } -} diff --git a/projects/f-examples/connections/connection-behaviours/connection-behaviours.html b/projects/f-examples/connections/connection-behaviours/connection-behaviours.html new file mode 100644 index 00000000..d29e93f4 --- /dev/null +++ b/projects/f-examples/connections/connection-behaviours/connection-behaviours.html @@ -0,0 +1,76 @@ + + + +
fixed
+
+
+ I'm a node +
+
+ I'm a node +
+ + +
fixed_center
+
+
+ I'm a node +
+
+ I'm a node +
+ + +
floating
+
+ +
+ I'm a node +
+
+ I'm a node +
+
+
diff --git a/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.scss b/projects/f-examples/connections/connection-behaviours/connection-behaviours.scss similarity index 81% rename from projects/f-examples/connections/connection-behaviours/connection-behaviours.component.scss rename to projects/f-examples/connections/connection-behaviours/connection-behaviours.scss index 1a1d3981..66196a92 100644 --- a/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.scss +++ b/projects/f-examples/connections/connection-behaviours/connection-behaviours.scss @@ -1,4 +1,4 @@ -@use "../../flow-common"; +@use '../../flow-common'; ::ng-deep f-flow { @include flow-common.connection; diff --git a/projects/f-examples/connections/connection-behaviours/connection-behaviours.ts b/projects/f-examples/connections/connection-behaviours/connection-behaviours.ts new file mode 100644 index 00000000..8ab4d853 --- /dev/null +++ b/projects/f-examples/connections/connection-behaviours/connection-behaviours.ts @@ -0,0 +1,18 @@ +import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core'; +import { FCanvasComponent, FFlowModule } from '@foblex/flow'; + +@Component({ + selector: 'connection-behaviours', + styleUrls: ['./connection-behaviours.scss'], + templateUrl: './connection-behaviours.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [FFlowModule], +}) +export class ConnectionBehaviours { + private readonly _canvas = viewChild.required(FCanvasComponent); + + protected loaded(): void { + this._canvas()?.resetScaleAndCenter(false); + } +} diff --git a/projects/f-examples/connections/connection-center/connection-center.component.html b/projects/f-examples/connections/connection-center/connection-center.component.html deleted file mode 100644 index 7342daaf..00000000 --- a/projects/f-examples/connections/connection-center/connection-center.component.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -
- Connection Center - - Select - - @for (option of options; track option) { - {{ option }} - } - - -
-
-
- I'm a node -
-
- I'm a node -
-
-
diff --git a/projects/f-examples/connections/connection-center/connection-center.component.scss b/projects/f-examples/connections/connection-center/connection-center.component.scss deleted file mode 100644 index 0f5d41e4..00000000 --- a/projects/f-examples/connections/connection-center/connection-center.component.scss +++ /dev/null @@ -1,16 +0,0 @@ -@use "../../flow-common"; - -::ng-deep f-flow { - @include flow-common.connection; - - .f-connection-center { - padding: 12px; - background: var(--node-background-color); - border-radius: var(--node-border-radius); - border: 0.2px solid var(--node-border-color); - } -} - -.f-node { - @include flow-common.node; -} diff --git a/projects/f-examples/connections/connection-center/connection-center.component.ts b/projects/f-examples/connections/connection-center/connection-center.component.ts deleted file mode 100644 index 3fb70e12..00000000 --- a/projects/f-examples/connections/connection-center/connection-center.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; -import { FCanvasComponent, FFlowModule } from '@foblex/flow'; -import { MatFormField, MatLabel } from '@angular/material/form-field'; -import { MatOption } from '@angular/material/core'; -import { MatSelect } from '@angular/material/select'; - -@Component({ - selector: 'connection-center', - styleUrls: ['./connection-center.component.scss'], - templateUrl: './connection-center.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [FFlowModule, MatFormField, MatLabel, MatOption, MatSelect], -}) -export class ConnectionCenterComponent { - protected options: string[] = ['Option 1', 'Option 2', 'Option 3', 'Option 4', 'Option 5']; - - protected value: string = 'Option 3'; - - @ViewChild(FCanvasComponent, { static: true }) - protected fCanvas!: FCanvasComponent; - - protected onLoaded(): void { - this.fCanvas.resetScaleAndCenter(false); - } -} diff --git a/projects/f-examples/connections/connection-content/connection-content.html b/projects/f-examples/connections/connection-content/connection-content.html index 5bcc345d..7854e8ac 100644 --- a/projects/f-examples/connections/connection-content/connection-content.html +++ b/projects/f-examples/connections/connection-content/connection-content.html @@ -33,6 +33,17 @@ Any Content
+ +
+ Any Content +
+
Node 6
+
+ Node 7 +
+
+ Node 8 +
diff --git a/projects/f-examples/connections/connection-markers/connection-markers.component.html b/projects/f-examples/connections/connection-markers/connection-markers.component.html deleted file mode 100644 index 1a825ac5..00000000 --- a/projects/f-examples/connections/connection-markers/connection-markers.component.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - -
- I'm a node -
-
- I'm a node -
- - - - - - - - - - - - - - - -
- I'm a node -
-
- I'm a node -
- - - - - - - - - - - - - - - -
- I'm a node -
-
- I'm a node -
-
-
diff --git a/projects/f-examples/connections/connection-markers/connection-markers.component.ts b/projects/f-examples/connections/connection-markers/connection-markers.component.ts deleted file mode 100644 index 5e43d348..00000000 --- a/projects/f-examples/connections/connection-markers/connection-markers.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; -import { EFMarkerType, FCanvasComponent, FFlowModule } from '@foblex/flow'; - -@Component({ - selector: 'connection-markers', - styleUrls: [ './connection-markers.component.scss' ], - templateUrl: './connection-markers.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - FFlowModule - ] -}) -export class ConnectionMarkersComponent { - - @ViewChild(FCanvasComponent, { static: true }) - public fCanvas!: FCanvasComponent; - - public eMarkerType = EFMarkerType; - - public onLoaded(): void { - this.fCanvas.resetScaleAndCenter(false); - } -} diff --git a/projects/f-examples/connections/connection-markers/connection-markers.html b/projects/f-examples/connections/connection-markers/connection-markers.html new file mode 100644 index 00000000..89062dc7 --- /dev/null +++ b/projects/f-examples/connections/connection-markers/connection-markers.html @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + + + +
+ I'm a node +
+
+ I'm a node +
+ + + + + + + + + + + + + + + +
+ I'm a node +
+
+ I'm a node +
+ + + + + + + + + + + + + + + +
+ I'm a node +
+
+ I'm a node +
+
+
diff --git a/projects/f-examples/connections/connection-markers/connection-markers.component.scss b/projects/f-examples/connections/connection-markers/connection-markers.scss similarity index 84% rename from projects/f-examples/connections/connection-markers/connection-markers.component.scss rename to projects/f-examples/connections/connection-markers/connection-markers.scss index 2af821c0..6f031ea2 100644 --- a/projects/f-examples/connections/connection-markers/connection-markers.component.scss +++ b/projects/f-examples/connections/connection-markers/connection-markers.scss @@ -1,4 +1,4 @@ -@use "../../flow-common"; +@use '../../flow-common'; ::ng-deep f-flow { @include flow-common.connection; diff --git a/projects/f-examples/connections/connection-markers/connection-markers.ts b/projects/f-examples/connections/connection-markers/connection-markers.ts new file mode 100644 index 00000000..d3117024 --- /dev/null +++ b/projects/f-examples/connections/connection-markers/connection-markers.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core'; +import { EFMarkerType, FCanvasComponent, FFlowModule } from '@foblex/flow'; + +@Component({ + selector: 'connection-markers', + styleUrls: ['./connection-markers.scss'], + templateUrl: './connection-markers.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [FFlowModule], +}) +export class ConnectionMarkers { + private readonly _canvas = viewChild.required(FCanvasComponent); + + protected readonly eMarkerType = EFMarkerType; + + protected loaded(): void { + this._canvas()?.resetScaleAndCenter(false); + } +} diff --git a/projects/f-examples/connections/connection-text/connection-text.component.html b/projects/f-examples/connections/connection-text/connection-text.component.html deleted file mode 100644 index 820fa018..00000000 --- a/projects/f-examples/connections/connection-text/connection-text.component.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - -
- I'm a node -
-
- I'm a node -
- - - -
- I'm a node -
-
- I'm a node -
- - - -
- I'm a node -
-
- I'm a node -
-
-
-
- - Text Offset - - @for (option of offsetOptions; track option) { - {{ option }} - } - - -
diff --git a/projects/f-examples/connections/connection-text/connection-text.component.scss b/projects/f-examples/connections/connection-text/connection-text.component.scss deleted file mode 100644 index 0b7ed0e0..00000000 --- a/projects/f-examples/connections/connection-text/connection-text.component.scss +++ /dev/null @@ -1,11 +0,0 @@ -@use "../../flow-common"; - -::ng-deep f-flow { - @include flow-common.connection; -} - -.f-node { - @include flow-common.node; -} - -@include flow-common.examples-toolbar; diff --git a/projects/f-examples/connections/connection-text/connection-text.component.ts b/projects/f-examples/connections/connection-text/connection-text.component.ts deleted file mode 100644 index 416c9fa2..00000000 --- a/projects/f-examples/connections/connection-text/connection-text.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; -import { FCanvasComponent, FFlowModule } from '@foblex/flow'; -import { MatFormField, MatLabel } from '@angular/material/form-field'; -import { MatOption } from '@angular/material/core'; -import { MatSelect } from '@angular/material/select'; - -@Component({ - selector: 'connection-text', - styleUrls: ['./connection-text.component.scss'], - templateUrl: './connection-text.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [FFlowModule, MatFormField, MatLabel, MatOption, MatSelect], -}) -export class ConnectionTextComponent { - protected offsetOptions: string[] = ['10', '25%', '30', '50%', '75', '100%']; - - protected fTextStartOffset: string = '50%'; - - @ViewChild(FCanvasComponent, { static: true }) - protected fCanvas!: FCanvasComponent; - - protected onLoaded(): void { - this.fCanvas.resetScaleAndCenter(false); - } -} diff --git a/projects/f-examples/connections/connection-types/connection-types.html b/projects/f-examples/connections/connection-types/connection-types.html index eca71d85..4d54ad5c 100644 --- a/projects/f-examples/connections/connection-types/connection-types.html +++ b/projects/f-examples/connections/connection-types/connection-types.html @@ -1,6 +1,6 @@ - +
- +
- +
Node
- + +
+ + + @if (waypointsOn()) { + + } + +
+ Node +
+
+ Node +
+ + + @if (waypointsOn()) { + + } + +
+ Node +
+
+ Node +
+ + + @if (waypointsOn()) { + + } + +
+ Node +
+
+ Node +
+ + + @if (waypointsOn()) { + + } + +
+ Node +
+
+ Node +
+
+ + +
+ + Show waypoints + + + Enable waypoints + +
diff --git a/projects/f-examples/connections/connection-waypoints/connection-waypoints.scss b/projects/f-examples/connections/connection-waypoints/connection-waypoints.scss new file mode 100644 index 00000000..0c2838b8 --- /dev/null +++ b/projects/f-examples/connections/connection-waypoints/connection-waypoints.scss @@ -0,0 +1,45 @@ +@use '../../flow-common'; + +::ng-deep f-flow { + @include flow-common.connection; + + .f-connection { + .f-connection-waypoints { + circle { + vector-effect: non-scaling-stroke; + transform-box: fill-box; + transform-origin: center; + transition: transform 120ms ease, stroke 120ms ease, fill 120ms ease; + } + + .f-candidate { + fill: var(--node-background-color); + stroke: #30a46c; + stroke-width: 2; + cursor: pointer; + + &:hover { + stroke: #298459; + transform: scale(1.1); + } + } + + .f-waypoint { + fill: var(--node-background-color); + stroke: var(--minimap-node-selected-color); + stroke-width: 2; + cursor: grab; + + &:hover { + transform: scale(1.1); + } + } + } + } +} + +.f-node { + @include flow-common.node; +} + +@include flow-common.examples-toolbar; diff --git a/projects/f-examples/connections/connection-waypoints/connection-waypoints.ts b/projects/f-examples/connections/connection-waypoints/connection-waypoints.ts new file mode 100644 index 00000000..413f22d4 --- /dev/null +++ b/projects/f-examples/connections/connection-waypoints/connection-waypoints.ts @@ -0,0 +1,55 @@ +import { ChangeDetectionStrategy, Component, signal, viewChild } from '@angular/core'; +import { FCanvasComponent, FConnectionWaypointsChangedEvent, FFlowModule } from '@foblex/flow'; +import { FCheckboxComponent } from '@foblex/m-render'; + +@Component({ + selector: 'connection-waypoints', + styleUrls: ['./connection-waypoints.scss'], + templateUrl: './connection-waypoints.html', + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [FFlowModule, FCheckboxComponent], +}) +export class ConnectionWaypoints { + private readonly _canvas = viewChild.required(FCanvasComponent); + + protected waypointsStraight = [ + { x: 50, y: 60 }, + { x: 100, y: 0 }, + { x: 150, y: 30 }, + ]; + protected waypointsSegment = [ + { x: 120, y: 100 }, + { x: 300, y: 150 }, + ]; + protected waypointsBezier = [ + { x: 50, y: 350 }, + { x: 100, y: 400 }, + { x: 150, y: 350 }, + ]; + protected waypointsAdaptiveCurve = [ + { x: 50, y: 450 }, + { x: 100, y: 500 }, + { x: 150, y: 550 }, + { x: 200, y: 500 }, + ]; + + protected readonly waypointsVisibility = signal(true); + protected readonly waypointsOn = signal(true); + + protected loaded(): void { + this._canvas()?.fitToScreen({ x: 100, y: 100 }, false); + } + + protected changed({ connectionId, waypoints }: FConnectionWaypointsChangedEvent): void { + console.log('Connection waypoints changed', connectionId, waypoints); + } + + protected toggleWaypointsVisibility(): void { + this.waypointsVisibility.update((x) => !x); + } + + protected toggleWaypointsOn(): void { + this.waypointsOn.update((x) => !x); + } +} diff --git a/projects/f-examples/connections/custom-connection-type/custom-connection-type.component.ts b/projects/f-examples/connections/custom-connection-type/custom-connection-type.component.ts index 3d85b993..ca4a4bcb 100644 --- a/projects/f-examples/connections/custom-connection-type/custom-connection-type.component.ts +++ b/projects/f-examples/connections/custom-connection-type/custom-connection-type.component.ts @@ -1,52 +1,76 @@ -import {ChangeDetectionStrategy, Component, ViewChild} from '@angular/core'; +// eslint-disable-next-line max-classes-per-file +import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; import { F_CONNECTION_BUILDERS, FCanvasComponent, FFlowModule, IFConnectionBuilder, IFConnectionBuilderRequest, - IFConnectionBuilderResponse + IFConnectionBuilderResponse, } from '@foblex/flow'; -import {IPoint, PointExtensions} from '@foblex/2d'; +import { IPoint, PointExtensions } from '@foblex/2d'; class OffsetStraightBuilder implements IFConnectionBuilder { - public handle(request: IFConnectionBuilderRequest): IFConnectionBuilderResponse { - const {source, target} = request; + const { source, target } = request; const path = `M ${source.x} ${source.y} L ${source.x + 20} ${source.y} L ${target.x - 20} ${target.y} L ${target.x} ${target.y}`; return { path, - connectionCenter: {x: 0, y: 0}, penultimatePoint: PointExtensions.initialize(target.x - 20, target.y), - secondPoint: PointExtensions.initialize(source.x + 20, source.y) + secondPoint: PointExtensions.initialize(source.x + 20, source.y), + points: [ + source, + PointExtensions.initialize(source.x + 20, source.y), + PointExtensions.initialize(target.x - 20, target.y), + target, + ], + candidates: [], }; } } class CircleConnectionBuilder implements IFConnectionBuilder { - public handle(request: IFConnectionBuilderRequest): IFConnectionBuilderResponse { - const {source, target} = request; - const d = this.getD(request); + const { source, target } = request; + const d = this._getD(request); const path = `M ${source.x} ${source.y} S${d.x} ${d.y} ${target.x} ${target.y}`; - return {path, connectionCenter: {x: 0, y: 0}, penultimatePoint: d, secondPoint: d}; + + return { + path, + penultimatePoint: d, + secondPoint: d, + points: [source, d, target], + candidates: [], + }; } - private getD(request: IFConnectionBuilderRequest): IPoint { + private _getD(request: IFConnectionBuilderRequest): IPoint { const offset: number = request.offset; const cx: number = (request.source.x + request.radius + request.target.x) / 2; const cy: number = (request.source.y + request.radius + request.target.y) / 2; - const dx: number = cx + (offset * (request.source.y - request.target.y)) / Math.sqrt(Math.pow(request.source.x - request.target.x, 2) + Math.pow(request.source.y - request.target.y, 2)) || cx; - const dy: number = cy - (offset * (request.source.x - request.target.x)) / Math.sqrt(Math.pow(request.source.x - request.target.x, 2) + Math.pow(request.source.y - request.target.y, 2)) || cy; + const dx: number = + cx + + (offset * (request.source.y - request.target.y)) / + Math.sqrt( + Math.pow(request.source.x - request.target.x, 2) + + Math.pow(request.source.y - request.target.y, 2), + ) || cx; + const dy: number = + cy - + (offset * (request.source.x - request.target.x)) / + Math.sqrt( + Math.pow(request.source.x - request.target.x, 2) + + Math.pow(request.source.y - request.target.y, 2), + ) || cy; - return {x: dx, y: dy}; + return { x: dx, y: dy }; } } const connectionBuilders = { ['offset_straight']: new OffsetStraightBuilder(), - ['circle']: new CircleConnectionBuilder() + ['circle']: new CircleConnectionBuilder(), }; @Component({ @@ -55,16 +79,11 @@ const connectionBuilders = { templateUrl: './custom-connection-type.component.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - providers: [ - {provide: F_CONNECTION_BUILDERS, useValue: connectionBuilders} - ], - imports: [ - FFlowModule - ] + providers: [{ provide: F_CONNECTION_BUILDERS, useValue: connectionBuilders }], + imports: [FFlowModule], }) export class CustomConnectionTypeComponent { - - @ViewChild(FCanvasComponent, {static: true}) + @ViewChild(FCanvasComponent, { static: true }) public fCanvas!: FCanvasComponent; public onLoaded(): void { diff --git a/projects/f-examples/connections/drag-to-connect/drag-to-connect.component.ts b/projects/f-examples/connections/drag-to-connect/drag-to-connect.component.ts deleted file mode 100644 index 257ab0df..00000000 --- a/projects/f-examples/connections/drag-to-connect/drag-to-connect.component.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ViewChild } from '@angular/core'; -import { FCanvasComponent, FCreateConnectionEvent, FFlowModule } from '@foblex/flow'; - -@Component({ - selector: 'drag-to-connect', - styleUrls: [ './drag-to-connect.component.scss' ], - templateUrl: './drag-to-connect.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - FFlowModule - ] -}) -export class DragToConnectComponent { - - @ViewChild(FCanvasComponent, { static: true }) - public fCanvas!: FCanvasComponent; - - public connections: { outputId: string, inputId: string }[] = []; - - constructor( - private changeDetectorRef: ChangeDetectorRef - ) { - } - - public addConnection(event: FCreateConnectionEvent): void { - if(!event.fInputId) { - return; - } - this.connections.push({ outputId: event.fOutputId, inputId: event.fInputId }); - this.changeDetectorRef.detectChanges(); - } - - public onLoaded(): void { - this.fCanvas.resetScaleAndCenter(false); - } -} diff --git a/projects/f-examples/connections/drag-to-connect/drag-to-connect.component.html b/projects/f-examples/connections/drag-to-connect/drag-to-connect.html similarity index 57% rename from projects/f-examples/connections/drag-to-connect/drag-to-connect.component.html rename to projects/f-examples/connections/drag-to-connect/drag-to-connect.html index e0e4e64c..f302caa8 100644 --- a/projects/f-examples/connections/drag-to-connect/drag-to-connect.component.html +++ b/projects/f-examples/connections/drag-to-connect/drag-to-connect.html @@ -1,13 +1,13 @@ - + - @for (connection of connections; track connection.inputId) { - + @for (connection of connections(); track $index) { + }
([]); + + protected createConnection(event: FCreateConnectionEvent): void { + const target = event.fInputId; + if (!target) { + return; + } + this.connections.update((x) => [...x, { source: event.fOutputId, target }]); + } + + protected loaded(): void { + this._canvas()?.resetScaleAndCenter(false); + } +} diff --git a/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.html b/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.html similarity index 76% rename from projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.html rename to projects/f-examples/connections/drag-to-reassign/drag-to-reassign.html index 74b38a56..43dcf861 100644 --- a/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.html +++ b/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.html @@ -1,13 +1,13 @@ @for (connection of connections(); track connection.id) { - + }
Output
diff --git a/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.scss b/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.scss similarity index 88% rename from projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.scss rename to projects/f-examples/connections/drag-to-reassign/drag-to-reassign.scss index ba886e7d..83cc5c58 100644 --- a/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.scss +++ b/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.scss @@ -1,4 +1,4 @@ -@use "../../flow-common"; +@use '../../flow-common'; ::ng-deep f-flow { @include flow-common.connection; diff --git a/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.ts b/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.ts similarity index 90% rename from projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.ts rename to projects/f-examples/connections/drag-to-reassign/drag-to-reassign.ts index 0cd3018c..f2d25a14 100644 --- a/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.ts +++ b/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.ts @@ -4,13 +4,13 @@ import { FCheckboxComponent } from '@foblex/m-render'; @Component({ selector: 'drag-to-reassign', - styleUrls: ['./drag-to-reassign.component.scss'], - templateUrl: './drag-to-reassign.component.html', + styleUrls: ['./drag-to-reassign.scss'], + templateUrl: './drag-to-reassign.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [FFlowModule, FCheckboxComponent], }) -export class DragToReassignComponent { +export class DragToReassign { private readonly _canvas = viewChild.required(FCanvasComponent); protected readonly reassignableStart = signal(false); diff --git a/projects/f-examples/connectors/node-as-connector/node-as-connector.component.html b/projects/f-examples/connectors/node-as-connector/node-as-connector.component.html index 2d07f873..6161ca13 100644 --- a/projects/f-examples/connectors/node-as-connector/node-as-connector.component.html +++ b/projects/f-examples/connectors/node-as-connector/node-as-connector.component.html @@ -1,14 +1,11 @@ - - +
I'm node
-
I'm node
- +
+ I'm node +
diff --git a/projects/f-examples/nodes/drag-handle/drag-handle.component.html b/projects/f-examples/nodes/drag-handle/drag-handle.html similarity index 75% rename from projects/f-examples/nodes/drag-handle/drag-handle.component.html rename to projects/f-examples/nodes/drag-handle/drag-handle.html index 53febae0..0f20b1f8 100644 --- a/projects/f-examples/nodes/drag-handle/drag-handle.component.html +++ b/projects/f-examples/nodes/drag-handle/drag-handle.html @@ -1,18 +1,18 @@ - +
+ [fNodePosition]="{ x: 0, y: 0 }" (fNodePositionChange)="positionChanged($event)"> Node is the drag handle
+ [fNodePosition]="{ x: 120, y: 100 }" class="drag-handle-inside" (fNodePositionChange)="positionChanged($event)"> Only the icon is the drag handle
+ [fNodePosition]="{ x: 350, y: 0 }" class="drag-handle-outside" (fNodePositionChange)="positionChanged($event)">
@@ -20,7 +20,7 @@
+ [fNodePosition]="{ x: 130, y: 200 }" (fNodePositionChange)="positionChanged($event)"> Only the image is the drag handle
diff --git a/projects/f-examples/nodes/drag-handle/drag-handle.component.scss b/projects/f-examples/nodes/drag-handle/drag-handle.scss similarity index 100% rename from projects/f-examples/nodes/drag-handle/drag-handle.component.scss rename to projects/f-examples/nodes/drag-handle/drag-handle.scss diff --git a/projects/f-examples/nodes/drag-handle/drag-handle.component.ts b/projects/f-examples/nodes/drag-handle/drag-handle.ts similarity index 57% rename from projects/f-examples/nodes/drag-handle/drag-handle.component.ts rename to projects/f-examples/nodes/drag-handle/drag-handle.ts index 831735d9..1be922f0 100644 --- a/projects/f-examples/nodes/drag-handle/drag-handle.component.ts +++ b/projects/f-examples/nodes/drag-handle/drag-handle.ts @@ -1,29 +1,24 @@ import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core'; -import { - FCanvasComponent, - FFlowModule, FMoveNodesEvent -} from '@foblex/flow'; -import {IPoint} from "@foblex/2d"; +import { FCanvasComponent, FFlowModule, FMoveNodesEvent } from '@foblex/flow'; +import { IPoint } from '@foblex/2d'; @Component({ selector: 'drag-handle', - styleUrls: [ './drag-handle.component.scss' ], - templateUrl: './drag-handle.component.html', + styleUrls: ['./drag-handle.scss'], + templateUrl: './drag-handle.html', changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [ - FFlowModule, - ] + imports: [FFlowModule], }) -export class DragHandleComponent { - protected readonly fCanvas = viewChild(FCanvasComponent); +export class DragHandle { + private readonly _canvas = viewChild(FCanvasComponent); /** * Triggered after the component is fully loaded. * Resets the canvas scale and centers the view without animation. */ - protected onLoaded(): void { - this.fCanvas()?.resetScaleAndCenter(false); + protected loaded(): void { + this._canvas()?.resetScaleAndCenter(false); } /** @@ -32,7 +27,7 @@ export class DragHandleComponent { * * @param event - Node movement event containing affected nodes and delta. */ - protected onMoveNodes(event: FMoveNodesEvent): void { + protected moveNodes(event: FMoveNodesEvent): void { // Handle node movement. } @@ -41,7 +36,7 @@ export class DragHandleComponent { * * @param position - The new position of the node. */ - protected onNodePositionChange(position: IPoint): void { + protected positionChanged(position: IPoint): void { // Handle node position change. } } diff --git a/projects/f-flow/LICENSE b/projects/f-flow/LICENSE index 2ee88de1..f49685f3 100644 --- a/projects/f-flow/LICENSE +++ b/projects/f-flow/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Foblex +Copyright (c) 2026 Foblex Flow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/projects/f-flow/package.json b/projects/f-flow/package.json index c03674b1..01706d61 100644 --- a/projects/f-flow/package.json +++ b/projects/f-flow/package.json @@ -1,6 +1,6 @@ { "name": "@foblex/flow", - "version": "17.9.81", + "version": "18.0.0", "description": "An Angular library designed to simplify the creation and manipulation of dynamic flow. Provides components for flows, nodes, and connections, automating node manipulation and inter-node connections.", "main": "index.js", "types": "index.d.ts", diff --git a/projects/f-flow/src/domain/css-cls.ts b/projects/f-flow/src/domain/css-cls.ts index 98f9af6e..936762c4 100644 --- a/projects/f-flow/src/domain/css-cls.ts +++ b/projects/f-flow/src/domain/css-cls.ts @@ -23,8 +23,4 @@ export const F_CSS_CLASS = { CONNECTABLE: 'f-connector-connectable', }, - - CONNECTION: { - DRAG_HANDLE: 'f-connection-drag-handle', - }, }; diff --git a/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store-request.ts b/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store-request.ts index 206e96da..5c85fd54 100644 --- a/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store-request.ts +++ b/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store-request.ts @@ -1,10 +1,7 @@ -import { FConnectionBase } from '../../../f-connection'; +import { FConnectionBase } from '../../../f-connection-v2'; export class AddConnectionForCreateToStoreRequest { static readonly fToken = Symbol('AddConnectionForCreateToStoreRequest'); - constructor( - public fConnection: FConnectionBase, - ) { - } + constructor(public readonly connection: FConnectionBase) {} } diff --git a/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store.execution.ts b/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store.ts similarity index 66% rename from projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store.execution.ts rename to projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store.ts index 2bb3b1e9..9de04ffb 100644 --- a/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store.execution.ts +++ b/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/add-connection-for-create-to-store.ts @@ -8,11 +8,12 @@ import { FComponentsStore } from '../../../f-storage'; */ @Injectable() @FExecutionRegister(AddConnectionForCreateToStoreRequest) -export class AddConnectionForCreateToStoreExecution implements IExecution { - +export class AddConnectionForCreateToStore + implements IExecution +{ private readonly _store = inject(FComponentsStore); - public handle(request: AddConnectionForCreateToStoreRequest): void { - this._store.fTempConnection = request.fConnection; + public handle({ connection }: AddConnectionForCreateToStoreRequest): void { + this._store.fTempConnection = connection; } } diff --git a/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/index.ts b/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/index.ts index 84a1b541..006c7cfa 100644 --- a/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/index.ts +++ b/projects/f-flow/src/domain/f-connection/add-connection-for-create-to-store/index.ts @@ -1,3 +1,3 @@ -export * from './add-connection-for-create-to-store.execution'; +export * from './add-connection-for-create-to-store'; export * from './add-connection-for-create-to-store-request'; diff --git a/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store-request.ts b/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store-request.ts index cb190b07..cce48d9c 100644 --- a/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store-request.ts +++ b/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store-request.ts @@ -1,10 +1,7 @@ -import { FMarkerBase } from '../../../f-connection'; +import { FConnectionMarkerBase } from '../../../f-connection-v2'; export class AddConnectionMarkerToStoreRequest { static readonly fToken = Symbol('AddConnectionMarkerToStoreRequest'); - constructor( - public fComponent: FMarkerBase, - ) { - } + constructor(public readonly component: FConnectionMarkerBase) {} } diff --git a/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store.execution.ts b/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store.ts similarity index 65% rename from projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store.execution.ts rename to projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store.ts index 013bc6f5..e26d100d 100644 --- a/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store.execution.ts +++ b/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/add-connection-marker-to-store.ts @@ -8,11 +8,12 @@ import { FComponentsStore } from '../../../f-storage'; */ @Injectable() @FExecutionRegister(AddConnectionMarkerToStoreRequest) -export class AddConnectionMarkerToStoreExecution implements IExecution { - +export class AddConnectionMarkerToStore + implements IExecution +{ private readonly _store = inject(FComponentsStore); - public handle(request: AddConnectionMarkerToStoreRequest): void { - this._store.addComponent(this._store.fMarkers, request.fComponent); + public handle({ component }: AddConnectionMarkerToStoreRequest): void { + this._store.addComponent(this._store.fMarkers, component); } } diff --git a/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/index.ts b/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/index.ts index b6d2f60b..3df8a3da 100644 --- a/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/index.ts +++ b/projects/f-flow/src/domain/f-connection/add-connection-marker-to-store/index.ts @@ -1,3 +1,3 @@ -export * from './add-connection-marker-to-store.execution'; +export * from './add-connection-marker-to-store'; export * from './add-connection-marker-to-store-request'; diff --git a/projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store-request.ts b/projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store-request.ts index e3e16652..82046f61 100644 --- a/projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store-request.ts +++ b/projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store-request.ts @@ -1,10 +1,7 @@ -import { FConnectionBase } from '../../../f-connection'; +import { FConnectionBase } from '../../../f-connection-v2'; export class AddConnectionToStoreRequest { static readonly fToken = Symbol('AddConnectionToStoreRequest'); - constructor( - public fConnection: FConnectionBase, - ) { - } + constructor(public readonly connection: FConnectionBase) {} } diff --git a/projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store.execution.ts b/projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store.ts similarity index 68% rename from projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store.execution.ts rename to projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store.ts index 6e44e58f..c057f494 100644 --- a/projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store.execution.ts +++ b/projects/f-flow/src/domain/f-connection/add-connection-to-store/add-connection-to-store.ts @@ -8,12 +8,11 @@ import { FComponentsStore } from '../../../f-storage'; */ @Injectable() @FExecutionRegister(AddConnectionToStoreRequest) -export class AddConnectionToStoreExecution implements IExecution { - +export class AddConnectionToStore implements IExecution { private readonly _store = inject(FComponentsStore); - public handle(request: AddConnectionToStoreRequest): void { - this._store.fConnections.push(request.fConnection); + public handle({ connection }: AddConnectionToStoreRequest): void { + this._store.fConnections.push(connection); this._store.dataChanged(); } } diff --git a/projects/f-flow/src/domain/f-connection/add-connection-to-store/index.ts b/projects/f-flow/src/domain/f-connection/add-connection-to-store/index.ts index dac70443..cae103b5 100644 --- a/projects/f-flow/src/domain/f-connection/add-connection-to-store/index.ts +++ b/projects/f-flow/src/domain/f-connection/add-connection-to-store/index.ts @@ -1,3 +1,3 @@ -export * from './add-connection-to-store.execution'; +export * from './add-connection-to-store'; export * from './add-connection-to-store-request'; diff --git a/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store-request.ts b/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store-request.ts index ca66c8db..a810049c 100644 --- a/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store-request.ts +++ b/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store-request.ts @@ -1,10 +1,7 @@ -import { FConnectionBase } from '../../../f-connection'; +import { FConnectionBase } from '../../../f-connection-v2'; export class AddSnapConnectionToStoreRequest { static readonly fToken = Symbol('AddSnapConnectionToStoreRequest'); - constructor( - public fConnection: FConnectionBase, - ) { - } + constructor(public readonly connection: FConnectionBase) {} } diff --git a/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store.execution.ts b/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store.ts similarity index 66% rename from projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store.execution.ts rename to projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store.ts index 1afe7a9e..03b96be7 100644 --- a/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store.execution.ts +++ b/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/add-snap-connection-to-store.ts @@ -8,11 +8,10 @@ import { FComponentsStore } from '../../../f-storage'; */ @Injectable() @FExecutionRegister(AddSnapConnectionToStoreRequest) -export class AddSnapConnectionToStoreExecution implements IExecution { - +export class AddSnapConnectionToStore implements IExecution { private readonly _store = inject(FComponentsStore); - public handle(request: AddSnapConnectionToStoreRequest): void { - this._store.fSnapConnection = request.fConnection; + public handle({ connection }: AddSnapConnectionToStoreRequest): void { + this._store.fSnapConnection = connection; } } diff --git a/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/index.ts b/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/index.ts index e0881dbe..4fa9cf2b 100644 --- a/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/index.ts +++ b/projects/f-flow/src/domain/f-connection/add-snap-connection-to-store/index.ts @@ -1,3 +1,3 @@ -export * from './add-snap-connection-to-store.execution'; +export * from './add-snap-connection-to-store'; export * from './add-snap-connection-to-store-request'; diff --git a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/calculate-connection-line-by-behavior-request.ts b/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/calculate-connection-line-by-behavior-request.ts deleted file mode 100644 index 933978d3..00000000 --- a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/calculate-connection-line-by-behavior-request.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { FConnectionBase } from '../../../f-connection'; -import { EFConnectableSide } from '../../../f-connectors'; -import { IRoundedRect } from '@foblex/2d'; - -export class CalculateConnectionLineByBehaviorRequest { - static readonly fToken = Symbol('CalculateConnectionLineByBehaviorRequest'); - - constructor( - public readonly sourceRect: IRoundedRect, - public readonly targetRect: IRoundedRect, - public readonly connection: FConnectionBase, - public readonly sourceConnectableSide: EFConnectableSide, - public readonly targetConnectableSide: EFConnectableSide, - ) {} -} diff --git a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/index.ts b/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/index.ts deleted file mode 100644 index 9e2b45e0..00000000 --- a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './calculate-connection-line-by-behavior'; - -export * from './calculate-connection-line-by-behavior-request'; - -export * from './models/calculate-behavior-request'; - -export * from './utils/fixed-center-behavior'; - -export * from './utils/fixed-outbound-behavior'; - -export * from './utils/floating-behavior'; diff --git a/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers-request.ts b/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers-request.ts index e4cb8249..8553c729 100644 --- a/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers-request.ts +++ b/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers-request.ts @@ -1,10 +1,7 @@ -import { FConnectionBase } from '../../../f-connection'; +import { FConnectionBase } from '../../../f-connection-v2'; export class CreateConnectionMarkersRequest { static readonly fToken = Symbol('CreateConnectionMarkersRequest'); - constructor( - public fConnection: FConnectionBase, - ) { - } + constructor(public readonly connection: FConnectionBase) {} } diff --git a/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers.execution.ts b/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers.execution.ts deleted file mode 100644 index fe2ce0e8..00000000 --- a/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers.execution.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { inject, Injectable } from '@angular/core'; -import { CreateConnectionMarkersRequest } from './create-connection-markers-request'; -import { FConnectionBase, FMarkerBase } from '../../../f-connection'; -import { FExecutionRegister, IExecution } from '@foblex/mediator'; -import { BrowserService } from '@foblex/platform'; -import { normalizeDomElementId } from '@foblex/utils'; -import { FComponentsStore } from '../../../f-storage'; - -/** - * Execution that creates connection markers for a given connection. - */ -@Injectable() -@FExecutionRegister(CreateConnectionMarkersRequest) -export class CreateConnectionMarkersExecution implements IExecution { - - private readonly _browser = inject(BrowserService); - private readonly _store = inject(FComponentsStore); - - public handle(request: CreateConnectionMarkersRequest): void { - const element: SVGDefsElement = createSVGElement('defs', this._browser); - const fConnection = request.fConnection; - - this.getMarkers(fConnection).forEach((marker) => { - - const markerElement = this.createMarkerElement(marker, fConnection.fId()); - - const clone = marker.hostElement.cloneNode(true) as HTMLElement; - clone.setAttribute('height', `${marker.height}`); - clone.setAttribute('width', `${marker.width}`); - clone.removeAttribute('markerUnits'); - clone.style.display = 'unset'; - markerElement.append(clone); - - element.append(markerElement); - }); - const defs = fConnection.fDefs(); - if (defs) { - defs.nativeElement.innerHTML = element.innerHTML; - } - - this.makeSafariCompatible(fConnection); - } - - public getMarkers(fConnection: FConnectionBase): FMarkerBase[] { - return this._store.fMarkers.filter((x) => fConnection.hostElement.contains(x.hostElement)); - } - - // Safari does not support markers on path elements if markers are defined after the path element - private makeSafariCompatible(fConnection: FConnectionBase): void { - fConnection.fPath().hostElement.replaceWith(fConnection.fPath().hostElement); - } - - private createMarkerElement(marker: FMarkerBase, fConnectionId: string): SVGElement { - const markerElement = createSVGElement('marker', this._browser); - - markerElement.setAttribute('id', normalizeDomElementId(marker.type + '-' + fConnectionId)); - - markerElement.setAttribute('markerHeight', `${marker.height}`); - markerElement.setAttribute('markerWidth', `${marker.width}`); - markerElement.setAttribute('orient', `${marker.orient}`); - markerElement.setAttribute('refX', `${marker.refX}`); - markerElement.setAttribute('refY', `${marker.refY}`); - markerElement.setAttribute('markerUnits', `${marker.markerUnits}`); - - return markerElement; - } -} - -function createSVGElement(tag: K, fBrowser: BrowserService): SVGElementTagNameMap[K] { - return fBrowser.document.createElementNS('http://www.w3.org/2000/svg', tag); -} diff --git a/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers.ts b/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers.ts new file mode 100644 index 00000000..f06bfad2 --- /dev/null +++ b/projects/f-flow/src/domain/f-connection/create-connection-markers/create-connection-markers.ts @@ -0,0 +1,75 @@ +import { inject, Injectable } from '@angular/core'; +import { CreateConnectionMarkersRequest } from './create-connection-markers-request'; +import { FExecutionRegister, IExecution } from '@foblex/mediator'; +import { BrowserService } from '@foblex/platform'; +import { normalizeDomElementId } from '@foblex/utils'; +import { FComponentsStore } from '../../../f-storage'; +import { FConnectionBase, FConnectionMarkerBase } from '../../../f-connection-v2'; + +/** + * Execution that creates connection markers for a given connection. + */ +@Injectable() +@FExecutionRegister(CreateConnectionMarkersRequest) +export class CreateConnectionMarkers implements IExecution { + private readonly _browser = inject(BrowserService); + private readonly _store = inject(FComponentsStore); + + public handle({ connection }: CreateConnectionMarkersRequest): void { + const element = createSVGElement('defs', this._browser); + + this._findConnectionMarkers(connection).forEach((marker) => { + const markerElement = createMarkerElement(marker, connection.fId(), this._browser); + + const clone = marker.hostElement.cloneNode(true) as HTMLElement; + clone.setAttribute('height', `${marker.height}`); + clone.setAttribute('width', `${marker.width}`); + clone.removeAttribute('markerUnits'); + clone.style.display = 'unset'; + markerElement.append(clone); + + element.append(markerElement); + }); + + const defs = connection.fDefs(); + if (defs) { + defs.nativeElement.innerHTML = element.innerHTML; + } + + this._makeSafariCompatible(connection); + } + + public _findConnectionMarkers(connection: FConnectionBase): FConnectionMarkerBase[] { + return this._store.fMarkers.filter((x) => connection.hostElement.contains(x.hostElement)); + } + + // Safari does not support markers on path elements if markers are defined after the path element + private _makeSafariCompatible(fConnection: FConnectionBase): void { + fConnection.fPath().hostElement.replaceWith(fConnection.fPath().hostElement); + } +} + +function createMarkerElement( + marker: FConnectionMarkerBase, + connectionId: string, + browser: BrowserService, +): SVGElement { + const markerElement = createSVGElement('marker', browser); + + markerElement.setAttribute('id', normalizeDomElementId(marker.type + '-' + connectionId)); + markerElement.setAttribute('markerHeight', `${marker.height}`); + markerElement.setAttribute('markerWidth', `${marker.width}`); + markerElement.setAttribute('orient', `${marker.orient}`); + markerElement.setAttribute('refX', `${marker.refX}`); + markerElement.setAttribute('refY', `${marker.refY}`); + markerElement.setAttribute('markerUnits', `${marker.markerUnits}`); + + return markerElement; +} + +function createSVGElement( + tag: K, + fBrowser: BrowserService, +): SVGElementTagNameMap[K] { + return fBrowser.document.createElementNS('http://www.w3.org/2000/svg', tag); +} diff --git a/projects/f-flow/src/domain/f-connection/create-connection-markers/index.ts b/projects/f-flow/src/domain/f-connection/create-connection-markers/index.ts index 407ca2c0..b729a436 100644 --- a/projects/f-flow/src/domain/f-connection/create-connection-markers/index.ts +++ b/projects/f-flow/src/domain/f-connection/create-connection-markers/index.ts @@ -1,3 +1,3 @@ -export * from './create-connection-markers.execution'; +export * from './create-connection-markers'; export * from './create-connection-markers-request'; diff --git a/projects/f-flow/src/domain/f-connection/index.ts b/projects/f-flow/src/domain/f-connection/index.ts index 50ea67ed..68bdeff0 100644 --- a/projects/f-flow/src/domain/f-connection/index.ts +++ b/projects/f-flow/src/domain/f-connection/index.ts @@ -8,8 +8,6 @@ export * from './add-snap-connection-to-store'; export * from './create-connection-markers'; -export * from './calculate-connection-line-by-behavior'; - export * from './redraw-connections'; export * from './remove-connection-for-create-from-store'; @@ -18,6 +16,8 @@ export * from './remove-connection-from-store'; export * from './remove-connection-marker-from-store'; +export * from './remove-connection-waypoint'; + export * from './remove-snap-connection-from-store'; export * from './providers'; diff --git a/projects/f-flow/src/domain/f-connection/providers.ts b/projects/f-flow/src/domain/f-connection/providers.ts index ba45d830..b8f7be14 100644 --- a/projects/f-flow/src/domain/f-connection/providers.ts +++ b/projects/f-flow/src/domain/f-connection/providers.ts @@ -1,38 +1,38 @@ -import { CreateConnectionMarkersExecution } from './create-connection-markers'; -import { AddConnectionForCreateToStoreExecution } from './add-connection-for-create-to-store'; -import { AddConnectionToStoreExecution } from './add-connection-to-store'; -import { AddSnapConnectionToStoreExecution } from './add-snap-connection-to-store'; -import { RemoveConnectionForCreateFromStoreExecution } from './remove-connection-for-create-from-store'; -import { RemoveConnectionFromStoreExecution } from './remove-connection-from-store'; -import { RemoveSnapConnectionFromStoreExecution } from './remove-snap-connection-from-store'; -import { AddConnectionMarkerToStoreExecution } from './add-connection-marker-to-store'; -import { RemoveConnectionMarkerFromStoreExecution } from './remove-connection-marker-from-store'; +import { CreateConnectionMarkers } from './create-connection-markers'; +import { AddConnectionForCreateToStore } from './add-connection-for-create-to-store'; +import { AddConnectionToStore } from './add-connection-to-store'; +import { AddSnapConnectionToStore } from './add-snap-connection-to-store'; +import { RemoveConnectionForCreateFromStore } from './remove-connection-for-create-from-store'; +import { RemoveConnectionFromStore } from './remove-connection-from-store'; +import { RemoveSnapConnectionFromStore } from './remove-snap-connection-from-store'; +import { AddConnectionMarkerToStore } from './add-connection-marker-to-store'; +import { RemoveConnectionMarkerFromStore } from './remove-connection-marker-from-store'; import { RedrawConnections } from './redraw-connections'; -import { CalculateConnectionLineByBehavior } from './calculate-connection-line-by-behavior'; +import { RemoveConnectionWaypoint } from './remove-connection-waypoint'; /** * This file exports all the connection-related features for the F-Flow domain. */ export const F_CONNECTION_FEATURES = [ - AddConnectionForCreateToStoreExecution, + AddConnectionForCreateToStore, - AddConnectionMarkerToStoreExecution, + AddConnectionMarkerToStore, - AddConnectionToStoreExecution, + AddConnectionToStore, - AddSnapConnectionToStoreExecution, + AddSnapConnectionToStore, - CreateConnectionMarkersExecution, - - CalculateConnectionLineByBehavior, + CreateConnectionMarkers, RedrawConnections, - RemoveConnectionForCreateFromStoreExecution, + RemoveConnectionForCreateFromStore, + + RemoveConnectionFromStore, - RemoveConnectionFromStoreExecution, + RemoveConnectionMarkerFromStore, - RemoveConnectionMarkerFromStoreExecution, + RemoveSnapConnectionFromStore, - RemoveSnapConnectionFromStoreExecution, + RemoveConnectionWaypoint, ]; diff --git a/projects/f-flow/src/domain/f-connection/redraw-connections/redraw-connections.ts b/projects/f-flow/src/domain/f-connection/redraw-connections/redraw-connections.ts index 44c3f079..942535d3 100644 --- a/projects/f-flow/src/domain/f-connection/redraw-connections/redraw-connections.ts +++ b/projects/f-flow/src/domain/f-connection/redraw-connections/redraw-connections.ts @@ -2,13 +2,16 @@ import { ILine, IRoundedRect } from '@foblex/2d'; import { inject, Injectable } from '@angular/core'; import { RedrawConnectionsRequest } from './redraw-connections-request'; import { FComponentsStore } from '../../../f-storage'; -import { CalculateConnectionLineByBehaviorRequest } from '../calculate-connection-line-by-behavior'; import { FConnectorBase } from '../../../f-connectors'; -import { FConnectionBase } from '../../../f-connection'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { CreateConnectionMarkersRequest } from '../create-connection-markers'; import { GetNormalizedConnectorRectRequest } from '../../get-normalized-connector-rect'; import { DragRectCache } from '../../drag-rect-cache'; +import { + ConnectionBehaviourBuilder, + ConnectionBehaviourBuilderRequest, + FConnectionBase, +} from '../../../f-connection-v2'; /** * Execution that redraws connections in the FComponentsStore. @@ -20,37 +23,42 @@ import { DragRectCache } from '../../drag-rect-cache'; export class RedrawConnections implements IExecution { private readonly _mediator = inject(FMediator); private readonly _store = inject(FComponentsStore); + private readonly _connectionBehaviour = inject(ConnectionBehaviourBuilder); public handle(_request: RedrawConnectionsRequest): void { this._resetConnectors(); if (this._store.fTempConnection) { - this._setMarkers(this._store.fTempConnection); + this._createMarkers(this._store.fTempConnection); } if (this._store.fSnapConnection) { - this._setMarkers(this._store.fSnapConnection); + this._createMarkers(this._store.fSnapConnection); } - this._store.fConnections.forEach((x) => { - this._setupConnection(this._getOutput(x.fOutputId()), this._getInput(x.fInputId()), x); + this._store.fConnections.forEach((connection) => { + this._setupConnection( + this._getSourceConnector(connection.fOutputId()), + this._getTargetConnector(connection.fInputId()), + connection, + ); }); DragRectCache.invalidateAll(); } - private _getOutput(id: string): FConnectorBase { + private _getSourceConnector(id: string): FConnectorBase { const result = this._store.fOutputs.find((x) => x.fId() === id); if (!result) { - throw new Error(`Output with id ${id} not found`); + throw new Error(`Source connector with id ${id} not found`); } return result; } - private _getInput(id: string): FConnectorBase { + private _getTargetConnector(id: string): FConnectorBase { const result = this._store.fInputs.find((x) => x.fId() === id); if (!result) { - throw new Error(`Input with id ${id} not found`); + throw new Error(`Target connector with id ${id} not found`); } return result; @@ -62,44 +70,44 @@ export class RedrawConnections implements IExecution( - new GetNormalizedConnectorRectRequest(output.hostElement), - ), - this._mediator.execute( - new GetNormalizedConnectorRectRequest(input.hostElement), - ), + return this._connectionBehaviour.handle( + new ConnectionBehaviourBuilderRequest( + this._calculateConnectorRect(source), + this._calculateConnectorRect(target), connection, - output.fConnectableSide, - input.fConnectableSide, + source.fConnectableSide, + target.fConnectableSide, ), ); } - private _setMarkers(connection: FConnectionBase): void { + private _calculateConnectorRect(connector: FConnectorBase): IRoundedRect { + return this._mediator.execute( + new GetNormalizedConnectorRectRequest(connector.hostElement), + ); + } + + private _createMarkers(connection: FConnectionBase): void { this._mediator.execute(new CreateConnectionMarkersRequest(connection)); } } diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/index.ts b/projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/index.ts index 64116a89..80163139 100644 --- a/projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/index.ts +++ b/projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/index.ts @@ -1,3 +1,3 @@ -export * from './remove-connection-for-create-from-store.execution'; +export * from './remove-connection-for-create-from-store'; export * from './remove-connection-for-create-from-store-request'; diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/remove-connection-for-create-from-store.execution.ts b/projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/remove-connection-for-create-from-store.ts similarity index 73% rename from projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/remove-connection-for-create-from-store.execution.ts rename to projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/remove-connection-for-create-from-store.ts index 524b07a2..38618903 100644 --- a/projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/remove-connection-for-create-from-store.execution.ts +++ b/projects/f-flow/src/domain/f-connection/remove-connection-for-create-from-store/remove-connection-for-create-from-store.ts @@ -8,11 +8,12 @@ import { FComponentsStore } from '../../../f-storage'; */ @Injectable() @FExecutionRegister(RemoveConnectionForCreateFromStoreRequest) -export class RemoveConnectionForCreateFromStoreExecution implements IExecution { - +export class RemoveConnectionForCreateFromStore + implements IExecution +{ private readonly _store = inject(FComponentsStore); - public handle(request: RemoveConnectionForCreateFromStoreRequest): void { + public handle(_request: RemoveConnectionForCreateFromStoreRequest): void { this._store.fTempConnection = undefined; } } diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-from-store/index.ts b/projects/f-flow/src/domain/f-connection/remove-connection-from-store/index.ts index 6b6dee49..e11d7cf4 100644 --- a/projects/f-flow/src/domain/f-connection/remove-connection-from-store/index.ts +++ b/projects/f-flow/src/domain/f-connection/remove-connection-from-store/index.ts @@ -1,3 +1,3 @@ -export * from './remove-connection-from-store.execution'; +export * from './remove-connection-from-store'; export * from './remove-connection-from-store-request'; diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store-request.ts b/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store-request.ts index 86cd67fe..9bf47b2b 100644 --- a/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store-request.ts +++ b/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store-request.ts @@ -1,10 +1,7 @@ -import { FConnectionBase } from '../../../f-connection'; +import { FConnectionBase } from '../../../f-connection-v2'; export class RemoveConnectionFromStoreRequest { static readonly fToken = Symbol('RemoveConnectionFromStoreRequest'); - constructor( - public fConnection: FConnectionBase, - ) { - } + constructor(public readonly connection: FConnectionBase) {} } diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store.execution.ts b/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store.execution.ts deleted file mode 100644 index 7a9fdb7b..00000000 --- a/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store.execution.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { inject, Injectable } from '@angular/core'; -import { FExecutionRegister, IExecution } from '@foblex/mediator'; -import { RemoveConnectionFromStoreRequest } from './remove-connection-from-store-request'; -import { FComponentsStore } from '../../../f-storage'; -import { FConnectionBase } from '../../../f-connection/common'; - -/** - * Execution that removes a connection from the FComponentsStore. - */ -@Injectable() -@FExecutionRegister(RemoveConnectionFromStoreRequest) -export class RemoveConnectionFromStoreExecution implements IExecution { - - private _store = inject(FComponentsStore); - - public handle(request: RemoveConnectionFromStoreRequest): void { - this._store.fConnections.splice(this._getIndexOfConnection(request.fConnection), 1); - this._store.dataChanged(); - } - - private _getIndexOfConnection(fConnection: FConnectionBase): number { - const result = this._store.fConnections.indexOf(fConnection); - if (result === -1) { - throw new Error(`Connection not found in store`); - } - - return result; - } -} diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store.ts b/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store.ts new file mode 100644 index 00000000..27606b1b --- /dev/null +++ b/projects/f-flow/src/domain/f-connection/remove-connection-from-store/remove-connection-from-store.ts @@ -0,0 +1,29 @@ +import { inject, Injectable } from '@angular/core'; +import { FExecutionRegister, IExecution } from '@foblex/mediator'; +import { RemoveConnectionFromStoreRequest } from './remove-connection-from-store-request'; +import { FComponentsStore } from '../../../f-storage'; + +/** + * Execution that removes a connection from the FComponentsStore. + */ +@Injectable() +@FExecutionRegister(RemoveConnectionFromStoreRequest) +export class RemoveConnectionFromStore + implements IExecution +{ + private _store = inject(FComponentsStore); + + public handle({ connection }: RemoveConnectionFromStoreRequest): void { + this._store.fConnections.splice(indexOf(this._store.fConnections, connection), 1); + this._store.dataChanged(); + } +} + +function indexOf(array: T[], element: T): number { + const result = array.indexOf(element); + if (result === -1) { + throw new Error(`Connection not found in store`); + } + + return result; +} diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/index.ts b/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/index.ts index a3e92cc8..2d7ffa90 100644 --- a/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/index.ts +++ b/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/index.ts @@ -1,3 +1,3 @@ -export * from './remove-connection-marker-from-store.execution'; +export * from './remove-connection-marker-from-store'; export * from './remove-connection-marker-from-store-request'; diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store-request.ts b/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store-request.ts index b4b14b12..9924e2b1 100644 --- a/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store-request.ts +++ b/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store-request.ts @@ -1,10 +1,7 @@ -import { FMarkerBase } from '../../../f-connection'; +import { FConnectionMarkerBase } from '../../../f-connection-v2'; export class RemoveConnectionMarkerFromStoreRequest { static readonly fToken = Symbol('RemoveConnectionMarkerFromStoreRequest'); - constructor( - public fComponent: FMarkerBase, - ) { - } + constructor(public readonly component: FConnectionMarkerBase) {} } diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store.execution.ts b/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store.ts similarity index 64% rename from projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store.execution.ts rename to projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store.ts index 9f54f252..3755575d 100644 --- a/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store.execution.ts +++ b/projects/f-flow/src/domain/f-connection/remove-connection-marker-from-store/remove-connection-marker-from-store.ts @@ -8,11 +8,12 @@ import { FComponentsStore } from '../../../f-storage'; */ @Injectable() @FExecutionRegister(RemoveConnectionMarkerFromStoreRequest) -export class RemoveConnectionMarkerFromStoreExecution implements IExecution { - +export class RemoveConnectionMarkerFromStore + implements IExecution +{ private readonly _store = inject(FComponentsStore); - public handle(request: RemoveConnectionMarkerFromStoreRequest): void { - this._store.removeComponent(this._store.fMarkers, request.fComponent); + public handle({ component }: RemoveConnectionMarkerFromStoreRequest): void { + this._store.removeComponent(this._store.fMarkers, component); } } diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-waypoint/index.ts b/projects/f-flow/src/domain/f-connection/remove-connection-waypoint/index.ts new file mode 100644 index 00000000..a95b442d --- /dev/null +++ b/projects/f-flow/src/domain/f-connection/remove-connection-waypoint/index.ts @@ -0,0 +1,3 @@ +export * from './remove-connection-waypoint'; + +export * from './remove-connection-waypoint-request'; diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-waypoint/remove-connection-waypoint-request.ts b/projects/f-flow/src/domain/f-connection/remove-connection-waypoint/remove-connection-waypoint-request.ts new file mode 100644 index 00000000..2378779c --- /dev/null +++ b/projects/f-flow/src/domain/f-connection/remove-connection-waypoint/remove-connection-waypoint-request.ts @@ -0,0 +1,7 @@ +export class RemoveConnectionWaypointRequest { + static readonly fToken = Symbol('RemoveConnectionWaypointRequest'); + constructor( + public readonly waypointIndex: number, + public readonly connectionId: string, + ) {} +} diff --git a/projects/f-flow/src/domain/f-connection/remove-connection-waypoint/remove-connection-waypoint.ts b/projects/f-flow/src/domain/f-connection/remove-connection-waypoint/remove-connection-waypoint.ts new file mode 100644 index 00000000..b5ecc1b1 --- /dev/null +++ b/projects/f-flow/src/domain/f-connection/remove-connection-waypoint/remove-connection-waypoint.ts @@ -0,0 +1,41 @@ +import { inject, Injectable } from '@angular/core'; +import { RemoveConnectionWaypointRequest } from './remove-connection-waypoint-request'; +import { FExecutionRegister, IExecution } from '@foblex/mediator'; +import { FComponentsStore } from '../../../f-storage'; +import { FConnectionBase, FConnectionWaypointsChangedEvent } from '../../../f-connection-v2'; + +@Injectable() +@FExecutionRegister(RemoveConnectionWaypointRequest) +export class RemoveConnectionWaypoint implements IExecution { + private readonly _store = inject(FComponentsStore); + + public handle({ waypointIndex, connectionId }: RemoveConnectionWaypointRequest): void { + const connection = this._findConnection(connectionId); + + const current = connection.fWaypoints()?.waypoints().slice(); + if (!current) { + throw new Error('Connection waypoints not found'); + } + current.splice(waypointIndex, 1); + + connection.fWaypoints()?.waypoints.set(current); + + this._store.fDraggable?.fConnectionWaypointsChanged.emit(this._changeEvent(connection)); + } + + private _findConnection(id: string): FConnectionBase { + const result = this._store.fConnections.find((x) => x.fId() === id); + if (!result) { + throw new Error(`Cannot find connection with id ${id}`); + } + + return result; + } + + private _changeEvent(connection: FConnectionBase): FConnectionWaypointsChangedEvent { + return new FConnectionWaypointsChangedEvent( + connection.fId(), + connection.fWaypoints()?.waypoints() || [], + ); + } +} diff --git a/projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/index.ts b/projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/index.ts index 6826c9e3..4fb91b45 100644 --- a/projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/index.ts +++ b/projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/index.ts @@ -1,3 +1,3 @@ -export * from './remove-snap-connection-from-store.execution'; +export * from './remove-snap-connection-from-store'; export * from './remove-snap-connection-from-store-request'; diff --git a/projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/remove-snap-connection-from-store.execution.ts b/projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/remove-snap-connection-from-store.ts similarity index 73% rename from projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/remove-snap-connection-from-store.execution.ts rename to projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/remove-snap-connection-from-store.ts index 05e53342..2fed8ee7 100644 --- a/projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/remove-snap-connection-from-store.execution.ts +++ b/projects/f-flow/src/domain/f-connection/remove-snap-connection-from-store/remove-snap-connection-from-store.ts @@ -8,11 +8,12 @@ import { FComponentsStore } from '../../../f-storage'; */ @Injectable() @FExecutionRegister(RemoveSnapConnectionFromStoreRequest) -export class RemoveSnapConnectionFromStoreExecution implements IExecution { - +export class RemoveSnapConnectionFromStore + implements IExecution +{ private readonly _store = inject(FComponentsStore); - public handle(request: RemoveSnapConnectionFromStoreRequest): void { + public handle(_request: RemoveSnapConnectionFromStoreRequest): void { this._store.fSnapConnection = undefined; } } diff --git a/projects/f-flow/src/domain/f-connectors/calculate-source-connectors-to-connect/calculate-source-connectors-to-connect.ts b/projects/f-flow/src/domain/f-connectors/calculate-source-connectors-to-connect/calculate-source-connectors-to-connect.ts index 27b12fb0..a54c1bf3 100644 --- a/projects/f-flow/src/domain/f-connectors/calculate-source-connectors-to-connect/calculate-source-connectors-to-connect.ts +++ b/projects/f-flow/src/domain/f-connectors/calculate-source-connectors-to-connect/calculate-source-connectors-to-connect.ts @@ -1,17 +1,13 @@ import { CalculateSourceConnectorsToConnectRequest } from './calculate-source-connectors-to-connect-request'; import { inject, Injectable } from '@angular/core'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; -import { - EFConnectableSide, - FConnectorBase, - FNodeInputBase, - FNodeOutputBase, -} from '../../../f-connectors'; +import { FConnectorBase, FNodeInputBase, FNodeOutputBase } from '../../../f-connectors'; import { FComponentsStore } from '../../../f-storage'; import { IConnectorAndRect } from '../i-connector-and-rect'; import { GetConnectorAndRectRequest } from '../get-connector-and-rect'; import { IPoint } from '@foblex/2d'; import { CalculateConnectableSideByConnectedPositionsRequest, isCalculateMode } from '../../f-node'; +import { EFConnectableSide } from '../../../f-connection-v2'; /** * Execution that retrieves all source connectors that can be connected to a given target connector, diff --git a/projects/f-flow/src/domain/f-connectors/calculate-target-connectors-to-connect/calculate-target-connectors-to-connect.ts b/projects/f-flow/src/domain/f-connectors/calculate-target-connectors-to-connect/calculate-target-connectors-to-connect.ts index 14b077cd..924e3044 100644 --- a/projects/f-flow/src/domain/f-connectors/calculate-target-connectors-to-connect/calculate-target-connectors-to-connect.ts +++ b/projects/f-flow/src/domain/f-connectors/calculate-target-connectors-to-connect/calculate-target-connectors-to-connect.ts @@ -2,7 +2,6 @@ import { CalculateTargetConnectorsToConnectRequest } from './calculate-target-co import { inject, Injectable } from '@angular/core'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { - EFConnectableSide, FConnectorBase, FNodeInputBase, FNodeOutletBase, @@ -13,6 +12,7 @@ import { IConnectorAndRect } from '../i-connector-and-rect'; import { GetConnectorAndRectRequest } from '../get-connector-and-rect'; import { CalculateConnectableSideByConnectedPositionsRequest, isCalculateMode } from '../../f-node'; import { IPoint } from '@foblex/2d'; +import { EFConnectableSide } from '../../../f-connection-v2'; /** * Execution that retrieves all input connectors that can be connected to a given output or outlet connector, diff --git a/projects/f-flow/src/domain/f-draggable/add-dnd-to-store/add-dnd-to-store-request.ts b/projects/f-flow/src/domain/f-draggable/add-dnd-to-store/add-dnd-to-store-request.ts index a72d1d8e..a21c8577 100644 --- a/projects/f-flow/src/domain/f-draggable/add-dnd-to-store/add-dnd-to-store-request.ts +++ b/projects/f-flow/src/domain/f-draggable/add-dnd-to-store/add-dnd-to-store-request.ts @@ -3,8 +3,5 @@ import { FDraggableBase } from '../../../f-draggable'; export class AddDndToStoreRequest { static readonly fToken = Symbol('AddDndToStoreRequest'); - constructor( - public fComponent: FDraggableBase, - ) { - } + constructor(public fComponent: FDraggableBase) {} } diff --git a/projects/f-flow/src/domain/f-draggable/on-pointer-move/on-pointer-move.execution.ts b/projects/f-flow/src/domain/f-draggable/on-pointer-move/on-pointer-move.execution.ts index 8558a689..199dc41b 100644 --- a/projects/f-flow/src/domain/f-draggable/on-pointer-move/on-pointer-move.execution.ts +++ b/projects/f-flow/src/domain/f-draggable/on-pointer-move/on-pointer-move.execution.ts @@ -4,7 +4,7 @@ import { OnPointerMoveRequest } from './on-pointer-move-request'; import { FDraggableDataContext } from '../../../f-draggable'; import { IPoint, Point } from '@foblex/2d'; import { FComponentsStore } from '../../../f-storage'; -import { IPointerEvent } from "../../../drag-toolkit"; +import { IPointerEvent } from '../../../drag-toolkit'; /** * Execution that handles pointer move events during a drag operation. @@ -14,7 +14,6 @@ import { IPointerEvent } from "../../../drag-toolkit"; @Injectable() @FExecutionRegister(OnPointerMoveRequest) export class OnPointerMoveExecution implements IExecution { - private readonly _store = inject(FComponentsStore); private get _hostElement(): HTMLElement { @@ -23,15 +22,16 @@ export class OnPointerMoveExecution implements IExecution { - item.onPointerMove({ ...difference }); + item.onPointerMove({ ...difference }, event); }); } diff --git a/projects/f-flow/src/domain/f-draggable/prepare-drag-sequence/prepare-drag-sequence-request.ts b/projects/f-flow/src/domain/f-draggable/prepare-drag-sequence/prepare-drag-sequence-request.ts index 47afa8ce..94ecf3a1 100644 --- a/projects/f-flow/src/domain/f-draggable/prepare-drag-sequence/prepare-drag-sequence-request.ts +++ b/projects/f-flow/src/domain/f-draggable/prepare-drag-sequence/prepare-drag-sequence-request.ts @@ -1,4 +1,3 @@ export class PrepareDragSequenceRequest { static readonly fToken = Symbol('PrepareDragSequenceRequest'); - } diff --git a/projects/f-flow/src/domain/f-draggable/prepare-drag-sequence/prepare-drag-sequence.execution.ts b/projects/f-flow/src/domain/f-draggable/prepare-drag-sequence/prepare-drag-sequence.execution.ts index 50e8b9de..d75c4e1a 100644 --- a/projects/f-flow/src/domain/f-draggable/prepare-drag-sequence/prepare-drag-sequence.execution.ts +++ b/projects/f-flow/src/domain/f-draggable/prepare-drag-sequence/prepare-drag-sequence.execution.ts @@ -10,20 +10,17 @@ import { StartDragSequenceRequest } from '../start-drag-sequence'; @Injectable() @FExecutionRegister(PrepareDragSequenceRequest) export class PrepareDragSequenceExecution implements IExecution { - private readonly _mediator = inject(FMediator); private readonly _dragContext = inject(FDraggableDataContext); - public handle(request: PrepareDragSequenceRequest): void { + public handle(_request: PrepareDragSequenceRequest): void { this._callPrepareDragSequence(); this._mediator.execute(new StartDragSequenceRequest()); } private _callPrepareDragSequence(): void { - this._dragContext.draggableItems.forEach((item) => { - item.prepareDragSequence?.(); - }); + this._dragContext.draggableItems.forEach((x) => x.prepareDragSequence?.()); } } diff --git a/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-connections-state/calculate-connections-state-request.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-connections-state/calculate-connections-state-request.ts new file mode 100644 index 00000000..7f4358e1 --- /dev/null +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-connections-state/calculate-connections-state-request.ts @@ -0,0 +1,3 @@ +export class CalculateConnectionsStateRequest { + static readonly fToken = Symbol('CalculateConnectionsStateRequest'); +} diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-connections/get-flow-state-connections.execution.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-connections-state/calculate-connections-state.ts similarity index 58% rename from projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-connections/get-flow-state-connections.execution.ts rename to projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-connections-state/calculate-connections-state.ts index 719818c6..8ff89e87 100644 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-connections/get-flow-state-connections.execution.ts +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-connections-state/calculate-connections-state.ts @@ -1,21 +1,21 @@ import { inject, Injectable } from '@angular/core'; -import { GetFlowStateConnectionsRequest } from './get-flow-state-connections-request'; +import { CalculateConnectionsStateRequest } from './calculate-connections-state-request'; import { FExecutionRegister, IExecution } from '@foblex/mediator'; import { FComponentsStore } from '../../../../f-storage'; import { IFFlowStateConnection } from '../i-f-flow-state-connection'; -import { FConnectionBase } from '../../../../f-connection'; +import { FConnectionBase } from '../../../../f-connection-v2'; /** * Execution that retrieves the current Flow state connections from the FComponentsStore. */ @Injectable() -@FExecutionRegister(GetFlowStateConnectionsRequest) -export class GetFlowStateConnectionsExecution - implements IExecution +@FExecutionRegister(CalculateConnectionsStateRequest) +export class CalculateConnectionsState + implements IExecution { private readonly _store = inject(FComponentsStore); - public handle(_request: GetFlowStateConnectionsRequest): IFFlowStateConnection[] { + public handle(_request: CalculateConnectionsStateRequest): IFFlowStateConnection[] { return this._store.fConnections.map(this._mapToConnectionState); } @@ -27,6 +27,9 @@ export class GetFlowStateConnectionsExecution fType: x.fType, fBehavior: x.fBehavior, isSelected: x.isSelected(), + waypoints: x.fWaypoints()?.waypoints() || [], + fInputSide: x.fInputSide(), + fOutputSide: x.fOutputSide(), }; } } diff --git a/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-connections-state/index.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-connections-state/index.ts new file mode 100644 index 00000000..08ae9ae0 --- /dev/null +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-connections-state/index.ts @@ -0,0 +1,3 @@ +export * from './calculate-connections-state'; + +export * from './calculate-connections-state-request'; diff --git a/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-flow-state-request.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-flow-state-request.ts new file mode 100644 index 00000000..db8f82ab --- /dev/null +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-flow-state-request.ts @@ -0,0 +1,3 @@ +export class CalculateFlowStateRequest { + static readonly fToken = Symbol('CalculateFlowStateRequest'); +} diff --git a/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-flow-state.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-flow-state.ts new file mode 100644 index 00000000..ae5a0ffa --- /dev/null +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-flow-state.ts @@ -0,0 +1,42 @@ +import { CalculateFlowStateRequest } from './calculate-flow-state-request'; +import { inject, Injectable } from '@angular/core'; +import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; +import { IFFlowState } from './i-f-flow-state'; +import { FComponentsStore } from '../../../f-storage'; +import { FGroupDirective, FNodeDirective } from '../../../f-node'; +import { IPoint, ITransformModel, PointExtensions } from '@foblex/2d'; +import { CalculateNodesStateRequest } from './calculate-nodes-state'; +import { CalculateConnectionsStateRequest } from './calculate-connections-state'; +import { FCanvasBase } from '../../../f-canvas'; + +/** + * Execution that retrieves the current state of the Flow, including its position, scale, nodes, groups, and connections. + */ +@Injectable() +@FExecutionRegister(CalculateFlowStateRequest) +export class CalculateFlowState implements IExecution { + private readonly _mediator = inject(FMediator); + private readonly _store = inject(FComponentsStore); + + private get _canvas(): FCanvasBase { + return this._store.fCanvas as FCanvasBase; + } + + private get _transform(): ITransformModel { + return this._canvas.transform; + } + + private get _canvasPosition(): IPoint { + return PointExtensions.sum(this._transform.position, this._transform.scaledPosition); + } + + public handle(_payload: CalculateFlowStateRequest): IFFlowState { + return { + position: this._canvasPosition, + scale: this._canvas.transform.scale, + nodes: this._mediator.execute(new CalculateNodesStateRequest(FNodeDirective)), + groups: this._mediator.execute(new CalculateNodesStateRequest(FGroupDirective)), + connections: this._mediator.execute(new CalculateConnectionsStateRequest()), + }; + } +} diff --git a/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-nodes-state/calculate-nodes-state-request.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-nodes-state/calculate-nodes-state-request.ts new file mode 100644 index 00000000..c8b99ca5 --- /dev/null +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-nodes-state/calculate-nodes-state-request.ts @@ -0,0 +1,6 @@ +import { Type } from '@angular/core'; + +export class CalculateNodesStateRequest { + static readonly fToken = Symbol('CalculateNodesStateRequest'); + constructor(public readonly component: Type) {} +} diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-nodes/get-flow-state-nodes.execution.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-nodes-state/calculate-nodes-state.ts similarity index 80% rename from projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-nodes/get-flow-state-nodes.execution.ts rename to projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-nodes-state/calculate-nodes-state.ts index d61c4104..c6572388 100644 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-nodes/get-flow-state-nodes.execution.ts +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-nodes-state/calculate-nodes-state.ts @@ -1,5 +1,5 @@ import { inject, Injectable } from '@angular/core'; -import { GetFlowStateNodesRequest } from './get-flow-state-nodes-request'; +import { CalculateNodesStateRequest } from './calculate-nodes-state-request'; import { FExecutionRegister, IExecution } from '@foblex/mediator'; import { IFFlowStateNode } from '../i-f-flow-state-node'; import { FComponentsStore } from '../../../../f-storage'; @@ -9,15 +9,15 @@ import { IFFlowStateConnector } from '../i-f-flow-state-connector'; * Execution that retrieves the state of Flow nodes, including their position, size, inputs, outputs, and selection status. */ @Injectable() -@FExecutionRegister(GetFlowStateNodesRequest) -export class GetFlowStateNodesExecution - implements IExecution +@FExecutionRegister(CalculateNodesStateRequest) +export class CalculateNodesState + implements IExecution { private readonly _store = inject(FComponentsStore); - public handle(request: GetFlowStateNodesRequest): IFFlowStateNode[] { + public handle({ component }: CalculateNodesStateRequest): IFFlowStateNode[] { return this._store.fNodes - .filter((x) => x instanceof request.type) + .filter((x) => x instanceof component) .map((x) => { return { id: x.fId(), diff --git a/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-nodes-state/index.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-nodes-state/index.ts new file mode 100644 index 00000000..9b62192a --- /dev/null +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/calculate-nodes-state/index.ts @@ -0,0 +1,3 @@ +export * from './calculate-nodes-state'; + +export * from './calculate-nodes-state-request'; diff --git a/projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state-connection.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state-connection.ts new file mode 100644 index 00000000..78a42e34 --- /dev/null +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state-connection.ts @@ -0,0 +1,26 @@ +import { + EFConnectionBehavior, + EFConnectionConnectableSide, + EFConnectionType, +} from '../../../f-connection-v2'; +import { IPoint } from '@foblex/2d'; + +export interface IFFlowStateConnection { + id: string; + + fOutputId: string; + + fInputId: string; + + fType: EFConnectionType | string; + + fBehavior: EFConnectionBehavior; + + isSelected: boolean; + + waypoints: IPoint[]; + + fInputSide: EFConnectionConnectableSide; + + fOutputSide: EFConnectionConnectableSide; +} diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state-connector.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state-connector.ts similarity index 61% rename from projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state-connector.ts rename to projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state-connector.ts index edc38aae..00b5c633 100644 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state-connector.ts +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state-connector.ts @@ -1,7 +1,6 @@ -import { EFConnectableSide } from '../../../f-connectors'; +import { EFConnectableSide } from '../../../f-connection-v2'; export interface IFFlowStateConnector { - id: string; fConnectableSide: EFConnectableSide; diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state-node.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state-node.ts similarity index 99% rename from projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state-node.ts rename to projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state-node.ts index 8da3cf6e..be8e1ba4 100644 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state-node.ts +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state-node.ts @@ -2,7 +2,6 @@ import { IPoint, ISize } from '@foblex/2d'; import { IFFlowStateConnector } from './i-f-flow-state-connector'; export interface IFFlowStateNode { - id: string; parentId?: string; diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state.ts similarity index 99% rename from projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state.ts rename to projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state.ts index b46df62b..e292d061 100644 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state.ts +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/i-f-flow-state.ts @@ -3,7 +3,6 @@ import { IFFlowStateConnection } from './i-f-flow-state-connection'; import { IPoint } from '@foblex/2d'; export interface IFFlowState { - position: IPoint; scale: number; diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/index.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/index.ts similarity index 53% rename from projects/f-flow/src/domain/f-flow/get-flow-state/index.ts rename to projects/f-flow/src/domain/f-flow/calculate-flow-state/index.ts index 575de4a5..13220f92 100644 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/index.ts +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/index.ts @@ -1,10 +1,10 @@ -export * from './get-flow-state-connections'; +export * from './calculate-connections-state'; -export * from './get-flow-state-nodes'; +export * from './calculate-nodes-state'; -export * from './get-flow-state.execution'; +export * from './calculate-flow-state'; -export * from './get-flow-state.request'; +export * from './calculate-flow-state-request'; export * from './i-f-flow-state'; diff --git a/projects/f-flow/src/domain/f-flow/calculate-flow-state/providers.ts b/projects/f-flow/src/domain/f-flow/calculate-flow-state/providers.ts new file mode 100644 index 00000000..835023d9 --- /dev/null +++ b/projects/f-flow/src/domain/f-flow/calculate-flow-state/providers.ts @@ -0,0 +1,14 @@ +import { CalculateFlowState } from './calculate-flow-state'; +import { CalculateNodesState } from './calculate-nodes-state'; +import { CalculateConnectionsState } from './calculate-connections-state'; + +/** + * Providers for retrieving the current Flow state, including nodes, groups, and connections. + */ +export const GET_FLOW_STATE_PROVIDERS = [ + CalculateFlowState, + + CalculateNodesState, + + CalculateConnectionsState, +]; diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-connections/get-flow-state-connections-request.ts b/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-connections/get-flow-state-connections-request.ts deleted file mode 100644 index 9b618e85..00000000 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-connections/get-flow-state-connections-request.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class GetFlowStateConnectionsRequest { - static readonly fToken = Symbol('GetFlowStateConnectionsRequest'); -} diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-connections/index.ts b/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-connections/index.ts deleted file mode 100644 index 8adf166e..00000000 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-connections/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './get-flow-state-connections.execution'; - -export * from './get-flow-state-connections-request'; diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-nodes/get-flow-state-nodes-request.ts b/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-nodes/get-flow-state-nodes-request.ts deleted file mode 100644 index e512bf10..00000000 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-nodes/get-flow-state-nodes-request.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Type } from '@angular/core'; - -export class GetFlowStateNodesRequest { - static readonly fToken = Symbol('GetFlowStateNodesRequest'); - constructor( - public type: Type, - ) { - } -} diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-nodes/index.ts b/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-nodes/index.ts deleted file mode 100644 index b58065f2..00000000 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state-nodes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './get-flow-state-nodes.execution'; - -export * from './get-flow-state-nodes-request'; diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state.execution.ts b/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state.execution.ts deleted file mode 100644 index c86f1bf2..00000000 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state.execution.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { GetFlowStateRequest } from './get-flow-state.request'; -import { inject, Injectable } from '@angular/core'; -import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; -import { IFFlowState } from './i-f-flow-state'; -import { FComponentsStore } from '../../../f-storage'; -import { FGroupDirective, FNodeDirective } from '../../../f-node'; -import { IPoint, ITransformModel, PointExtensions } from '@foblex/2d'; -import { GetFlowStateNodesRequest } from './get-flow-state-nodes'; -import { GetFlowStateConnectionsRequest } from './get-flow-state-connections'; - -/** - * Execution that retrieves the current state of the Flow, including its position, scale, nodes, groups, and connections. - */ -@Injectable() -@FExecutionRegister(GetFlowStateRequest) -export class GetFlowStateExecution implements IExecution { - - private readonly _mediator = inject(FMediator); - private readonly _store = inject(FComponentsStore); - - public handle(payload: GetFlowStateRequest): IFFlowState { - return { - position: this._getCanvasPosition(this._store.fCanvas!.transform), - scale: this._store.fCanvas!.transform.scale, - nodes: this._mediator.execute(new GetFlowStateNodesRequest(FNodeDirective)), - groups: this._mediator.execute(new GetFlowStateNodesRequest(FGroupDirective)), - connections: this._mediator.execute(new GetFlowStateConnectionsRequest()), - } - } - - private _getCanvasPosition(transform: ITransformModel): IPoint { - return PointExtensions.sum(transform.position, transform.scaledPosition); - } -} - - diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state.request.ts b/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state.request.ts deleted file mode 100644 index 1047f651..00000000 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/get-flow-state.request.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class GetFlowStateRequest { - static readonly fToken = Symbol('GetFlowStateRequest'); -} diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state-connection.ts b/projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state-connection.ts deleted file mode 100644 index 05db2529..00000000 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/i-f-flow-state-connection.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { EFConnectionBehavior, EFConnectionType } from '../../../f-connection'; - -export interface IFFlowStateConnection { - - id: string; - - fOutputId: string; - - fInputId: string; - - fType: EFConnectionType | string; - - fBehavior: EFConnectionBehavior; - - isSelected: boolean; -} diff --git a/projects/f-flow/src/domain/f-flow/get-flow-state/providers.ts b/projects/f-flow/src/domain/f-flow/get-flow-state/providers.ts deleted file mode 100644 index cbd9e746..00000000 --- a/projects/f-flow/src/domain/f-flow/get-flow-state/providers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { GetFlowStateExecution } from './get-flow-state.execution'; -import { GetFlowStateNodesExecution } from './get-flow-state-nodes'; -import { GetFlowStateConnectionsExecution } from './get-flow-state-connections'; - -/** - * Providers for retrieving the current Flow state, including nodes, groups, and connections. - */ -export const GET_FLOW_STATE_PROVIDERS = [ - - GetFlowStateExecution, - - GetFlowStateNodesExecution, - - GetFlowStateConnectionsExecution, -]; diff --git a/projects/f-flow/src/domain/f-flow/index.ts b/projects/f-flow/src/domain/f-flow/index.ts index c37ed777..1c52c898 100644 --- a/projects/f-flow/src/domain/f-flow/index.ts +++ b/projects/f-flow/src/domain/f-flow/index.ts @@ -4,7 +4,7 @@ export * from './get-flow'; export * from './get-flow-host-element'; -export * from './get-flow-state'; +export * from './calculate-flow-state'; export * from './remove-flow-from-store'; diff --git a/projects/f-flow/src/domain/f-flow/providers.ts b/projects/f-flow/src/domain/f-flow/providers.ts index 6b612e1e..001e09d4 100644 --- a/projects/f-flow/src/domain/f-flow/providers.ts +++ b/projects/f-flow/src/domain/f-flow/providers.ts @@ -2,7 +2,7 @@ import { GetFlowHostElementExecution } from './get-flow-host-element'; import { AddFlowToStoreExecution } from './add-flow-to-store'; import { RemoveFlowFromStoreExecution } from './remove-flow-from-store'; import { GetFlowExecution } from './get-flow'; -import { GET_FLOW_STATE_PROVIDERS } from './get-flow-state'; +import { GET_FLOW_STATE_PROVIDERS } from './calculate-flow-state'; /** * Providers for managing the Flow in the FComponentsStore. @@ -10,7 +10,6 @@ import { GET_FLOW_STATE_PROVIDERS } from './get-flow-state'; * as well as getting the Flow host element and its state. */ export const F_FLOW_FEATURES = [ - AddFlowToStoreExecution, GetFlowExecution, diff --git a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectable-side-by-connected-positions/calculate-connectable-side-by-connected-positions.ts b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectable-side-by-connected-positions/calculate-connectable-side-by-connected-positions.ts index 31fe0d59..2399d7bb 100644 --- a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectable-side-by-connected-positions/calculate-connectable-side-by-connected-positions.ts +++ b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectable-side-by-connected-positions/calculate-connectable-side-by-connected-positions.ts @@ -1,12 +1,13 @@ import { inject, Injectable } from '@angular/core'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { CalculateConnectableSideByConnectedPositionsRequest } from './calculate-connectable-side-by-connected-positions-request'; -import { EFConnectableSide, FConnectorBase } from '../../../../f-connectors'; +import { FConnectorBase } from '../../../../f-connectors'; import { IPoint, IRoundedRect } from '@foblex/2d'; import { determineSide } from '../utils'; import { CALCULATABLE_SIDES } from '../constants'; import { TCalculateMode } from '../models'; import { GetNormalizedElementRectRequest } from '../../../get-normalized-element-rect'; +import { EFConnectableSide } from '../../../../f-connection-v2'; /** * Execution that calculates the connectable side for a connector diff --git a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectable-side-by-internal-position/calculate-connectable-side-by-internal-position.ts b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectable-side-by-internal-position/calculate-connectable-side-by-internal-position.ts index eb6a73a5..f5db9b3f 100644 --- a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectable-side-by-internal-position/calculate-connectable-side-by-internal-position.ts +++ b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectable-side-by-internal-position/calculate-connectable-side-by-internal-position.ts @@ -1,9 +1,9 @@ import { Injectable, InjectionToken, inject } from '@angular/core'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { CalculateConnectableSideByInternalPositionRequest } from './calculate-connectable-side-by-internal-position-request'; -import { EFConnectableSide } from '../../../../f-connectors'; import { RectExtensions } from '@foblex/2d'; import { IsDragStartedRequest } from '../../../f-draggable'; +import { EFConnectableSide } from '../../../../f-connection-v2'; /** * Injection token for configuring the side detection tolerance (in pixels). diff --git a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectors-connectable-sides.ts b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectors-connectable-sides.ts index b7e4473f..8fda8b4d 100644 --- a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectors-connectable-sides.ts +++ b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/calculate-connectors-connectable-sides.ts @@ -1,10 +1,11 @@ import { inject, Injectable } from '@angular/core'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { CalculateConnectorsConnectableSidesRequest } from './calculate-connectors-connectable-sides-request'; -import { EFConnectableSide, FConnectorBase } from '../../../f-connectors'; +import { FConnectorBase } from '../../../f-connectors'; import { CalculateConnectableSideByConnectedPositionsRequest } from './calculate-connectable-side-by-connected-positions'; import { CalculateConnectableSideByInternalPositionRequest } from './calculate-connectable-side-by-internal-position'; import { isCalculateMode } from './utils'; +import { EFConnectableSide } from '../../../f-connection-v2'; /** * Execution that calculates connectable sides for all connectors of a node. diff --git a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/constants/calculatable-sides.ts b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/constants/calculatable-sides.ts index 1ef9ff6f..90421e54 100644 --- a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/constants/calculatable-sides.ts +++ b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/constants/calculatable-sides.ts @@ -1,4 +1,4 @@ -import { EFConnectableSide } from '../../../../f-connectors'; +import { EFConnectableSide } from '../../../../f-connection-v2'; export const CALCULATABLE_SIDES = { [EFConnectableSide.CALCULATE]: [ diff --git a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/models/t-calculate-mode.ts b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/models/t-calculate-mode.ts index 75cc788c..29f3d5a8 100644 --- a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/models/t-calculate-mode.ts +++ b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/models/t-calculate-mode.ts @@ -1,4 +1,4 @@ -import { EFConnectableSide } from '../../../../f-connectors'; +import { EFConnectableSide } from '../../../../f-connection-v2'; export type TCalculateMode = | EFConnectableSide.CALCULATE diff --git a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/utils/determine-side.ts b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/utils/determine-side.ts index 0d115a39..9847f279 100644 --- a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/utils/determine-side.ts +++ b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/utils/determine-side.ts @@ -2,7 +2,7 @@ * Converts allowed sides array to a compact bit mask. * If not provided or empty -> all sides allowed. */ -import { EFConnectableSide } from '../../../../f-connectors'; +import { EFConnectableSide } from '../../../../f-connection-v2'; const SNAP_EPS = 2; diff --git a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/utils/is-calculate-mode.ts b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/utils/is-calculate-mode.ts index 3f605fcc..b3e220a0 100644 --- a/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/utils/is-calculate-mode.ts +++ b/projects/f-flow/src/domain/f-node/calculate-connectors-connectable-sides/utils/is-calculate-mode.ts @@ -1,4 +1,4 @@ -import { EFConnectableSide } from '../../../../f-connectors'; +import { EFConnectableSide } from '../../../../f-connection-v2'; /** * Checks if the given side is one of the calculate modes. diff --git a/projects/f-flow/src/domain/f-node/calculate-input-connections/calculate-input-connections.ts b/projects/f-flow/src/domain/f-node/calculate-input-connections/calculate-input-connections.ts index 80883bf2..f613e7fd 100644 --- a/projects/f-flow/src/domain/f-node/calculate-input-connections/calculate-input-connections.ts +++ b/projects/f-flow/src/domain/f-node/calculate-input-connections/calculate-input-connections.ts @@ -3,7 +3,7 @@ import { FExecutionRegister, IExecution } from '@foblex/mediator'; import { CalculateInputConnectionsRequest } from './calculate-input-connections-request'; import { FComponentsStore } from '../../../f-storage'; import { FNodeBase } from '../../../f-node'; -import { FConnectionBase } from '../../../f-connection'; +import { FConnectionBase } from '../../../f-connection-v2'; /** * Execution that calculates input connections for a given FNode. diff --git a/projects/f-flow/src/domain/f-node/calculate-output-connections/calculate-output-connections.ts b/projects/f-flow/src/domain/f-node/calculate-output-connections/calculate-output-connections.ts index 72519014..04e17f43 100644 --- a/projects/f-flow/src/domain/f-node/calculate-output-connections/calculate-output-connections.ts +++ b/projects/f-flow/src/domain/f-node/calculate-output-connections/calculate-output-connections.ts @@ -3,7 +3,7 @@ import { FExecutionRegister, IExecution } from '@foblex/mediator'; import { CalculateOutputConnectionsRequest } from './calculate-output-connections-request'; import { FComponentsStore } from '../../../f-storage'; import { FNodeBase } from '../../../f-node'; -import { FConnectionBase } from '../../../f-connection'; +import { FConnectionBase } from '../../../f-connection-v2'; /** * Execution that calculates output connections for a given FNode. diff --git a/projects/f-flow/src/domain/f-node/update-node-when-state-or-size-changed/update-node-when-state-or-size-changed.ts b/projects/f-flow/src/domain/f-node/update-node-when-state-or-size-changed/update-node-when-state-or-size-changed.ts index 157d7473..ff2ce4e8 100644 --- a/projects/f-flow/src/domain/f-node/update-node-when-state-or-size-changed/update-node-when-state-or-size-changed.ts +++ b/projects/f-flow/src/domain/f-node/update-node-when-state-or-size-changed/update-node-when-state-or-size-changed.ts @@ -27,7 +27,7 @@ export class UpdateNodeWhenStateOrSizeChanged const { hostElement, stateChanges } = nodeOrGroup; new FChannelHub(new FResizeChannel(hostElement), stateChanges) - .pipe(notifyOnStart(), debounceTime(10)) + .pipe(notifyOnStart(), debounceTime(1)) .listen(destroyRef, () => { this._mediator.execute(new NotifyDataChangedRequest()); diff --git a/projects/f-flow/src/domain/f-selection/calculate-selectable-items/calculate-selectable-items-request.ts b/projects/f-flow/src/domain/f-selection/calculate-selectable-items/calculate-selectable-items-request.ts new file mode 100644 index 00000000..43e33f8a --- /dev/null +++ b/projects/f-flow/src/domain/f-selection/calculate-selectable-items/calculate-selectable-items-request.ts @@ -0,0 +1,3 @@ +export class CalculateSelectableItemsRequest { + static readonly fToken = Symbol('CalculateSelectableItemsRequest'); +} diff --git a/projects/f-flow/src/domain/f-selection/calculate-selectable-items/calculate-selectable-items.ts b/projects/f-flow/src/domain/f-selection/calculate-selectable-items/calculate-selectable-items.ts new file mode 100644 index 00000000..cf683567 --- /dev/null +++ b/projects/f-flow/src/domain/f-selection/calculate-selectable-items/calculate-selectable-items.ts @@ -0,0 +1,76 @@ +import { inject, Injectable } from '@angular/core'; +import { IRect, ITransformModel, RectExtensions } from '@foblex/2d'; +import { ICanBeSelectedElementAndRect } from './i-can-be-selected-element-and-rect'; +import { CalculateSelectableItemsRequest } from './calculate-selectable-items-request'; +import { FNodeBase } from '../../../f-node'; +import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; +import { FComponentsStore } from '../../../f-storage'; +import { FDraggableDataContext } from '../../../f-draggable'; +import { GetNormalizedElementRectRequest } from '../../get-normalized-element-rect'; +import { FConnectionBase } from '../../../f-connection-v2'; + +/** + * Execution that retrieves elements that can be selected in the Flow, along with their bounding rectangles. + * It filters out elements that are already selected in the FDraggableDataContext. + */ +@Injectable() +@FExecutionRegister(CalculateSelectableItemsRequest) +export class CalculateSelectableItems implements IExecution { + private readonly _mediator = inject(FMediator); + private readonly _store = inject(FComponentsStore); + private readonly _dragContext = inject(FDraggableDataContext); + + private get _nodes(): FNodeBase[] { + return this._store.fNodes; + } + + private get _connections(): FConnectionBase[] { + return this._store.fConnections; + } + + private get _transform(): ITransformModel { + return this._store.fCanvas?.transform as ITransformModel; + } + + public handle(): ICanBeSelectedElementAndRect[] { + return [...this._nodeRects(), ...this._connectionRects()].filter((x) => { + return !this._dragContext.selectedItems.includes(x.element); + }); + } + + /** + * Retrieves nodes with their bounding rectangles that can be selected. + * @private + */ + private _nodeRects(): ICanBeSelectedElementAndRect[] { + return this._nodes + .filter((x) => !x.fSelectionDisabled()) + .map((x) => { + return { + element: x, + fRect: RectExtensions.mult( + this._mediator.execute(new GetNormalizedElementRectRequest(x.hostElement)), + this._transform.scale, + ), + }; + }); + } + + /** + * Retrieves connections with their bounding rectangles that can be selected. + * @private + */ + private _connectionRects(): ICanBeSelectedElementAndRect[] { + return this._connections + .filter((x) => !x.fSelectionDisabled()) + .map((x) => { + return { + element: x, + fRect: RectExtensions.mult( + this._mediator.execute(new GetNormalizedElementRectRequest(x.boundingElement)), + this._transform.scale, + ), + }; + }); + } +} diff --git a/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/i-can-be-selected-element-and-rect.ts b/projects/f-flow/src/domain/f-selection/calculate-selectable-items/i-can-be-selected-element-and-rect.ts similarity index 99% rename from projects/f-flow/src/domain/f-selection/get-can-be-selected-items/i-can-be-selected-element-and-rect.ts rename to projects/f-flow/src/domain/f-selection/calculate-selectable-items/i-can-be-selected-element-and-rect.ts index 2e99399b..e6f715ff 100644 --- a/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/i-can-be-selected-element-and-rect.ts +++ b/projects/f-flow/src/domain/f-selection/calculate-selectable-items/i-can-be-selected-element-and-rect.ts @@ -2,7 +2,6 @@ import { IRect } from '@foblex/2d'; import { ISelectable } from '../../../mixins'; export interface ICanBeSelectedElementAndRect { - element: ISelectable; fRect: IRect; diff --git a/projects/f-flow/src/domain/f-selection/calculate-selectable-items/index.ts b/projects/f-flow/src/domain/f-selection/calculate-selectable-items/index.ts new file mode 100644 index 00000000..61748cdb --- /dev/null +++ b/projects/f-flow/src/domain/f-selection/calculate-selectable-items/index.ts @@ -0,0 +1,5 @@ +export * from './calculate-selectable-items'; + +export * from './calculate-selectable-items-request'; + +export * from './i-can-be-selected-element-and-rect'; diff --git a/projects/f-flow/src/domain/f-selection/clear-selection/clear-selection.request.ts b/projects/f-flow/src/domain/f-selection/clear-selection/clear-selection.request.ts index 14db7685..a148bb8d 100644 --- a/projects/f-flow/src/domain/f-selection/clear-selection/clear-selection.request.ts +++ b/projects/f-flow/src/domain/f-selection/clear-selection/clear-selection.request.ts @@ -1,4 +1,3 @@ export class ClearSelectionRequest { static readonly fToken = Symbol('ClearSelectionRequest'); - } diff --git a/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/get-can-be-selected-items-request.ts b/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/get-can-be-selected-items-request.ts deleted file mode 100644 index 39f671bd..00000000 --- a/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/get-can-be-selected-items-request.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class GetCanBeSelectedItemsRequest { - static readonly fToken = Symbol('GetCanBeSelectedItemsRequest'); -} diff --git a/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/get-can-be-selected-items.execution.ts b/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/get-can-be-selected-items.execution.ts deleted file mode 100644 index f4afce03..00000000 --- a/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/get-can-be-selected-items.execution.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { inject, Injectable } from '@angular/core'; -import { IRect, ITransformModel, RectExtensions } from '@foblex/2d'; -import { ICanBeSelectedElementAndRect } from './i-can-be-selected-element-and-rect'; -import { GetCanBeSelectedItemsRequest } from './get-can-be-selected-items-request'; -import { FNodeBase } from '../../../f-node'; -import { FConnectionBase } from '../../../f-connection'; -import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; -import { FComponentsStore } from '../../../f-storage'; -import { FDraggableDataContext } from '../../../f-draggable'; -import { GetNormalizedElementRectRequest } from '../../get-normalized-element-rect'; - -/** - * Execution that retrieves elements that can be selected in the Flow, along with their bounding rectangles. - * It filters out elements that are already selected in the FDraggableDataContext. - */ -@Injectable() -@FExecutionRegister(GetCanBeSelectedItemsRequest) -export class GetCanBeSelectedItemsExecution implements IExecution { - - private readonly _mediator = inject(FMediator); - private readonly _store = inject(FComponentsStore); - private readonly _dragContext = inject(FDraggableDataContext); - - private get fNodes(): FNodeBase[] { - return this._store.fNodes; - } - - private get fConnections(): FConnectionBase[] { - return this._store.fConnections; - } - - private get transform(): ITransformModel { - return this._store.fCanvas!.transform; - } - - public handle(): ICanBeSelectedElementAndRect[] { - return [ ...this.getNodesWithRects(), ...this.getConnectionsWithRects() ].filter((x) => { - return !this._dragContext.selectedItems.includes(x.element); - }); - } - - /** - * Retrieves nodes with their bounding rectangles that can be selected. - * @private - */ - private getNodesWithRects(): ICanBeSelectedElementAndRect[] { - return this.fNodes.filter((x) => !x.fSelectionDisabled()).map((x) => { - return { - element: x, - fRect: RectExtensions.mult( - this._mediator.execute(new GetNormalizedElementRectRequest(x.hostElement)), - this.transform.scale, - ), - }; - }); - } - - /** - * Retrieves connections with their bounding rectangles that can be selected. - * @private - */ - private getConnectionsWithRects(): ICanBeSelectedElementAndRect[] { - return this.fConnections.filter((x) => !x.fSelectionDisabled()).map((x) => { - return { - element: x, - fRect: RectExtensions.mult( - this._mediator.execute(new GetNormalizedElementRectRequest(x.boundingElement)), - this.transform.scale, - ), - }; - }); - } -} diff --git a/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/index.ts b/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/index.ts deleted file mode 100644 index 563ccd3e..00000000 --- a/projects/f-flow/src/domain/f-selection/get-can-be-selected-items/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './get-can-be-selected-items.execution'; - -export * from './get-can-be-selected-items-request'; - -export * from './i-can-be-selected-element-and-rect'; diff --git a/projects/f-flow/src/domain/f-selection/index.ts b/projects/f-flow/src/domain/f-selection/index.ts index bee5b7b9..37f1b9b3 100644 --- a/projects/f-flow/src/domain/f-selection/index.ts +++ b/projects/f-flow/src/domain/f-selection/index.ts @@ -1,6 +1,6 @@ export * from './clear-selection'; -export * from './get-can-be-selected-items'; +export * from './calculate-selectable-items'; export * from './get-current-selection'; diff --git a/projects/f-flow/src/domain/f-selection/providers.ts b/projects/f-flow/src/domain/f-selection/providers.ts index 307abd3c..328b648f 100644 --- a/projects/f-flow/src/domain/f-selection/providers.ts +++ b/projects/f-flow/src/domain/f-selection/providers.ts @@ -3,16 +3,15 @@ import { GetCurrentSelectionExecution } from './get-current-selection'; import { SelectExecution } from './select'; import { SelectAllExecution } from './select-all'; import { SelectAndUpdateNodeLayerExecution } from './select-and-update-node-layer'; -import { GetCanBeSelectedItemsExecution } from './get-can-be-selected-items'; +import { CalculateSelectableItems } from './calculate-selectable-items'; /** * This module provides a collection of executions related to selection features in the FFlow domain. */ export const F_SELECTION_FEATURES = [ - ClearSelectionExecution, - GetCanBeSelectedItemsExecution, + CalculateSelectableItems, GetCurrentSelectionExecution, diff --git a/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store-request.ts b/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store-request.ts index dfcbc499..e0c05e72 100644 --- a/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store-request.ts +++ b/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store-request.ts @@ -2,8 +2,5 @@ import { FZoomBase } from '../../../f-zoom'; export class AddZoomToStoreRequest { static readonly fToken = Symbol('AddZoomToStoreRequest'); - constructor( - public fComponent: FZoomBase, - ) { - } + constructor(public readonly component: FZoomBase) {} } diff --git a/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store.execution.ts b/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store.ts similarity index 72% rename from projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store.execution.ts rename to projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store.ts index c79a8920..df67d82d 100644 --- a/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store.execution.ts +++ b/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/add-zoom-to-store.ts @@ -9,14 +9,12 @@ import { F_ZOOM_TAG } from '../f-zoom-tag'; */ @Injectable() @FExecutionRegister(AddZoomToStoreRequest) -export class AddZoomToStoreExecution - implements IExecution { - +export class AddZoomToStore implements IExecution { private readonly _store = inject(FComponentsStore); - public handle(request: AddZoomToStoreRequest): void { + public handle({ component }: AddZoomToStoreRequest): void { this._store.fComponents = { - [F_ZOOM_TAG]: request.fComponent, + [F_ZOOM_TAG]: component, }; } } diff --git a/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/index.ts b/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/index.ts index 35ce82f5..93dbbb23 100644 --- a/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/index.ts +++ b/projects/f-flow/src/domain/f-zoom/add-zoom-to-store/index.ts @@ -1,3 +1,3 @@ -export * from './add-zoom-to-store.execution'; +export * from './add-zoom-to-store'; export * from './add-zoom-to-store-request'; diff --git a/projects/f-flow/src/domain/f-zoom/providers.ts b/projects/f-flow/src/domain/f-zoom/providers.ts index 77b65089..601fe50d 100644 --- a/projects/f-flow/src/domain/f-zoom/providers.ts +++ b/projects/f-flow/src/domain/f-zoom/providers.ts @@ -1,20 +1,11 @@ -import { AddZoomToStoreExecution } from './add-zoom-to-store'; -import { RemoveZoomFromStoreExecution } from './remove-zoom-from-store'; -import { SetZoomExecution } from './set-zoom'; -import { ResetZoomExecution } from './reset-zoom'; +import { AddZoomToStore } from './add-zoom-to-store'; +import { RemoveZoomFromStore } from './remove-zoom-from-store'; +import { SetZoom } from './set-zoom'; +import { ResetZoom } from './reset-zoom'; /** * Collection of all FZoom feature executions. * These executions handle the addition, removal, and resetting of zoom levels * in the FComponentsStore. */ -export const F_ZOOM_FEATURES = [ - - AddZoomToStoreExecution, - - RemoveZoomFromStoreExecution, - - ResetZoomExecution, - - SetZoomExecution, -]; +export const F_ZOOM_FEATURES = [AddZoomToStore, RemoveZoomFromStore, ResetZoom, SetZoom]; diff --git a/projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/index.ts b/projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/index.ts index 49ab1600..c60afacd 100644 --- a/projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/index.ts +++ b/projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/index.ts @@ -1,3 +1,3 @@ -export * from './remove-zoom-from-store.execution'; +export * from './remove-zoom-from-store'; export * from './remove-zoom-from-store-request'; diff --git a/projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/remove-zoom-from-store.execution.ts b/projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/remove-zoom-from-store.ts similarity index 86% rename from projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/remove-zoom-from-store.execution.ts rename to projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/remove-zoom-from-store.ts index 7ad60894..61db5bb6 100644 --- a/projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/remove-zoom-from-store.execution.ts +++ b/projects/f-flow/src/domain/f-zoom/remove-zoom-from-store/remove-zoom-from-store.ts @@ -9,7 +9,7 @@ import { F_ZOOM_TAG } from '../f-zoom-tag'; */ @Injectable() @FExecutionRegister(RemoveZoomFromStoreRequest) -export class RemoveZoomFromStoreExecution implements IExecution { +export class RemoveZoomFromStore implements IExecution { private readonly _store = inject(FComponentsStore); public handle(_request: RemoveZoomFromStoreRequest): void { diff --git a/projects/f-flow/src/domain/f-zoom/reset-zoom/index.ts b/projects/f-flow/src/domain/f-zoom/reset-zoom/index.ts index 6d87db09..37b71160 100644 --- a/projects/f-flow/src/domain/f-zoom/reset-zoom/index.ts +++ b/projects/f-flow/src/domain/f-zoom/reset-zoom/index.ts @@ -1,3 +1,3 @@ -export * from './reset-zoom.execution'; +export * from './reset-zoom'; export * from './reset-zoom-request'; diff --git a/projects/f-flow/src/domain/f-zoom/reset-zoom/reset-zoom.execution.ts b/projects/f-flow/src/domain/f-zoom/reset-zoom/reset-zoom.ts similarity index 59% rename from projects/f-flow/src/domain/f-zoom/reset-zoom/reset-zoom.execution.ts rename to projects/f-flow/src/domain/f-zoom/reset-zoom/reset-zoom.ts index cc72a585..6b7c41ed 100644 --- a/projects/f-flow/src/domain/f-zoom/reset-zoom/reset-zoom.execution.ts +++ b/projects/f-flow/src/domain/f-zoom/reset-zoom/reset-zoom.ts @@ -9,17 +9,16 @@ import { FCanvasBase } from '../../../f-canvas'; */ @Injectable() @FExecutionRegister(ResetZoomRequest) -export class ResetZoomExecution implements IExecution { - +export class ResetZoom implements IExecution { private readonly _store = inject(FComponentsStore); - private get _fCanvas(): FCanvasBase { - return this._store.fCanvas!; + private get _canvas(): FCanvasBase { + return this._store.fCanvas as FCanvasBase; } - public handle(request: ResetZoomRequest): void { - this._fCanvas.resetScale(); - this._fCanvas.redraw(); - this._fCanvas.emitCanvasChangeEvent(); + public handle(_request: ResetZoomRequest): void { + this._canvas.resetScale(); + this._canvas.redraw(); + this._canvas.emitCanvasChangeEvent(); } } diff --git a/projects/f-flow/src/domain/f-zoom/set-zoom/index.ts b/projects/f-flow/src/domain/f-zoom/set-zoom/index.ts index fb44a7b4..94784d99 100644 --- a/projects/f-flow/src/domain/f-zoom/set-zoom/index.ts +++ b/projects/f-flow/src/domain/f-zoom/set-zoom/index.ts @@ -1,3 +1,3 @@ -export * from './set-zoom.execution'; +export * from './set-zoom'; export * from './set-zoom-request'; diff --git a/projects/f-flow/src/domain/f-zoom/set-zoom/set-zoom.execution.ts b/projects/f-flow/src/domain/f-zoom/set-zoom/set-zoom.ts similarity index 56% rename from projects/f-flow/src/domain/f-zoom/set-zoom/set-zoom.execution.ts rename to projects/f-flow/src/domain/f-zoom/set-zoom/set-zoom.ts index dda2ec95..ca877482 100644 --- a/projects/f-flow/src/domain/f-zoom/set-zoom/set-zoom.execution.ts +++ b/projects/f-flow/src/domain/f-zoom/set-zoom/set-zoom.ts @@ -14,20 +14,20 @@ import { IsDragStartedRequest } from '../../f-draggable'; */ @Injectable() @FExecutionRegister(SetZoomRequest) -export class SetZoomExecution implements IExecution { +export class SetZoom implements IExecution { private readonly _mediator = inject(FMediator); private readonly _store = inject(FComponentsStore); - private get _fHost(): HTMLElement { - return this._store.fFlow?.hostElement!; + private get _flowHost(): HTMLElement { + return this._store.fFlow?.hostElement as HTMLElement; } - private get _fCanvas(): FCanvasBase { - return this._store.fCanvas!; + private get _canvas(): FCanvasBase { + return this._store.fCanvas as FCanvasBase; } - private get _fZoomComponent(): FZoomBase { - return this._store.fComponents[F_ZOOM_TAG]! as FZoomBase; + private get _zoomComponent(): FZoomBase { + return this._store.fComponents[F_ZOOM_TAG] as FZoomBase; } private get _isDragStarted(): boolean { @@ -35,22 +35,22 @@ export class SetZoomExecution implements IExecution { } public handle(request: SetZoomRequest): void { - if (this._isDragStarted || !this._fZoomComponent) { + if (this._isDragStarted || !this._zoomComponent) { return; } - const result = this._fCanvas.transform.scale + request.step * request.direction; + const result = this._canvas.transform.scale + request.step * request.direction; - this._fCanvas.setScale(this._clamp(result), this._castPositionToFlow(request.position)); - request.animate ? this._fCanvas.redrawWithAnimation() : this._fCanvas.redraw(); - this._fCanvas.emitCanvasChangeEvent(); + this._canvas.setScale(this._clamp(result), this._castPositionToFlow(request.position)); + request.animate ? this._canvas.redrawWithAnimation() : this._canvas.redraw(); + this._canvas.emitCanvasChangeEvent(); } private _clamp(value: number): number { - return Math.max(this._fZoomComponent.minimum, Math.min(value, this._fZoomComponent.maximum)); + return Math.max(this._zoomComponent.minimum, Math.min(value, this._zoomComponent.maximum)); } private _castPositionToFlow(position: IPoint): IPoint { - return Point.fromPoint(position).elementTransform(this._fHost); + return Point.fromPoint(position).elementTransform(this._flowHost); } } diff --git a/projects/f-flow/src/domain/i-map.ts b/projects/f-flow/src/domain/i-map.ts deleted file mode 100644 index 03f64932..00000000 --- a/projects/f-flow/src/domain/i-map.ts +++ /dev/null @@ -1 +0,0 @@ -export type IMap = Record; diff --git a/projects/f-flow/src/domain/index.ts b/projects/f-flow/src/domain/index.ts index 40cae2f2..c3dc366c 100644 --- a/projects/f-flow/src/domain/index.ts +++ b/projects/f-flow/src/domain/index.ts @@ -38,8 +38,6 @@ export * from './css-cls'; export * from './f-event-trigger'; -export * from './i-map'; - export * from './is-mobile'; export * from './log-deprecated'; diff --git a/projects/f-flow/src/drag-toolkit/pointer-events/i-pointer-event.ts b/projects/f-flow/src/drag-toolkit/pointer-events/i-pointer-event.ts index 63ccb25f..b1f056c7 100644 --- a/projects/f-flow/src/drag-toolkit/pointer-events/i-pointer-event.ts +++ b/projects/f-flow/src/drag-toolkit/pointer-events/i-pointer-event.ts @@ -1,19 +1,27 @@ export abstract class IPointerEvent { - - public get originalEvent(): (MouseEvent | TouchEvent) { - return this.event; + public get originalEvent(): MouseEvent | TouchEvent { + return this._event; } public get targetElement(): HTMLElement { - return this.target || this.originalEvent.target as HTMLElement; + return this._target || (this.originalEvent.target as HTMLElement); + } + + public get touchEvent(): TouchEvent { + return this._event as TouchEvent; } - protected constructor(private readonly event: (MouseEvent | TouchEvent), private target?: HTMLElement) { - this.event = event; + public get touches(): TouchList { + return this.touchEvent.touches; } + protected constructor( + private readonly _event: MouseEvent | TouchEvent, + private _target?: HTMLElement, + ) {} + public setTarget(target: HTMLElement): void { - this.target = target; + this._target = target; } public abstract isMouseLeftButton(): boolean; @@ -24,7 +32,7 @@ export abstract class IPointerEvent { this.originalEvent.preventDefault(); } - public abstract getPosition(): { x: number, y: number }; + public abstract getPosition(): { x: number; y: number }; public get isEventInLockedContext(): boolean { return this.targetElement.closest('[fLockedContext]') !== null; diff --git a/projects/f-flow/src/f-canvas/domain/f-canvas-change.event.ts b/projects/f-flow/src/f-canvas/domain/f-canvas-change-event.ts similarity index 94% rename from projects/f-flow/src/f-canvas/domain/f-canvas-change.event.ts rename to projects/f-flow/src/f-canvas/domain/f-canvas-change-event.ts index 7fea62ca..2f25871e 100644 --- a/projects/f-flow/src/f-canvas/domain/f-canvas-change.event.ts +++ b/projects/f-flow/src/f-canvas/domain/f-canvas-change-event.ts @@ -4,6 +4,5 @@ export class FCanvasChangeEvent { constructor( public readonly position: IPoint, public readonly scale: number, - ) { - } + ) {} } diff --git a/projects/f-flow/src/f-canvas/domain/index.ts b/projects/f-flow/src/f-canvas/domain/index.ts index 9020d632..c9d488fe 100644 --- a/projects/f-flow/src/f-canvas/domain/index.ts +++ b/projects/f-flow/src/f-canvas/domain/index.ts @@ -1 +1 @@ -export * from './f-canvas-change.event'; +export * from './f-canvas-change-event'; diff --git a/projects/f-flow/src/f-canvas/f-canvas-base.ts b/projects/f-flow/src/f-canvas/f-canvas-base.ts index dc7f42f4..f3e2c9f8 100644 --- a/projects/f-flow/src/f-canvas/f-canvas-base.ts +++ b/projects/f-flow/src/f-canvas/f-canvas-base.ts @@ -1,14 +1,21 @@ import { PointExtensions, TransformModelExtensions, IPoint } from '@foblex/2d'; -import { DestroyRef, Directive, ElementRef, inject, InjectionToken, OutputEmitterRef, Signal } from '@angular/core'; +import { + DestroyRef, + Directive, + ElementRef, + inject, + InjectionToken, + OutputEmitterRef, + Signal, +} from '@angular/core'; import { FCanvasChangeEvent } from './domain'; import { IHasHostElement } from '../i-has-host-element'; -import { debounceTime, FChannel, FChannelHub } from "../reactivity"; +import { debounceTime, FChannel, FChannelHub } from '../reactivity'; export const F_CANVAS = new InjectionToken('F_CANVAS'); @Directive() export abstract class FCanvasBase implements IHasHostElement { - public abstract fCanvasChange: OutputEmitterRef; public abstract hostElement: HTMLElement; @@ -24,7 +31,7 @@ export abstract class FCanvasBase implements IHasHostElement { public abstract debounce: Signal; private readonly _fCanvasChange = new FChannel(); - private readonly _destroyRef = inject(DestroyRef); + protected readonly destroyRef = inject(DestroyRef); public abstract redraw(): void; @@ -47,12 +54,15 @@ export abstract class FCanvasBase implements IHasHostElement { } protected subscribeOnCanvasChange(): void { - new FChannelHub( - this._fCanvasChange, - ).pipe(debounceTime(this.debounce())).listen(this._destroyRef, () => { - this.fCanvasChange.emit( - new FCanvasChangeEvent(PointExtensions.sum(this.transform.position, this.transform.scaledPosition), this.transform.scale), - ); - }); + new FChannelHub(this._fCanvasChange) + .pipe(debounceTime(this.debounce())) + .listen(this.destroyRef, () => { + this.fCanvasChange.emit( + new FCanvasChangeEvent( + PointExtensions.sum(this.transform.position, this.transform.scaledPosition), + this.transform.scale, + ), + ); + }); } } diff --git a/projects/f-flow/src/f-canvas/f-canvas.component.ts b/projects/f-flow/src/f-canvas/f-canvas.component.ts index 20aff58b..e66734b9 100644 --- a/projects/f-flow/src/f-canvas/f-canvas.component.ts +++ b/projects/f-flow/src/f-canvas/f-canvas.component.ts @@ -12,7 +12,7 @@ import { OnInit, output, viewChild, -} from "@angular/core"; +} from '@angular/core'; import { F_CANVAS, FCanvasBase } from './f-canvas-base'; import { IPoint, PointExtensions, TransformModelExtensions } from '@foblex/2d'; import { FCanvasChangeEvent } from './domain'; @@ -20,7 +20,6 @@ import { FMediator } from '@foblex/mediator'; import { AddCanvasToStoreRequest, CenterGroupOrNodeRequest, - Deprecated, FitToFlowRequest, GetFlowRequest, InputCanvasPositionRequest, @@ -33,8 +32,9 @@ import { transitionEnd, UpdateScaleRequest, } from '../domain'; -import { NotifyTransformChangedRequest } from '../f-storage'; +import { ListenDataChangesRequest, NotifyTransformChangedRequest } from '../f-storage'; import { FFlowBase } from '../f-flow'; +import { FChannelHub, takeOne } from '../reactivity'; /** * Component representing a canvas in the F-Flow framework. @@ -53,13 +53,10 @@ import { FFlowBase } from '../f-flow'; host: { 'class': 'f-component f-canvas', }, - providers: [ - { provide: F_CANVAS, useExisting: FCanvasComponent }, - ], + providers: [{ provide: F_CANVAS, useExisting: FCanvasComponent }], changeDetection: ChangeDetectionStrategy.OnPush, }) export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { - private readonly _mediator = inject(FMediator); private readonly _elementReference = inject(ElementRef); private readonly _injector = inject(Injector); @@ -68,18 +65,23 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { public override fCanvasChange = output(); - public readonly position = input(PointExtensions.initialize(), { transform: PointExtensions.castToPoint }); + public readonly position = input( + PointExtensions.initialize(), + { transform: PointExtensions.castToPoint }, + ); public readonly scale = input(1, { transform: numberAttribute }); public readonly debounceTime = input(0, { transform: numberAttribute }); - public override debounce = computed(() => this.debounceTime() < 0 ? 0 : this.debounceTime()); + public override debounce = computed(() => (this.debounceTime() < 0 ? 0 : this.debounceTime())); public override get hostElement(): HTMLElement { return this._elementReference.nativeElement; } - public override fGroupsContainer = viewChild.required>('fGroupsContainer'); + public override fGroupsContainer = + viewChild.required>('fGroupsContainer'); public override fNodesContainer = viewChild.required>('fNodesContainer'); - public override fConnectionsContainer = viewChild.required>('fConnectionsContainer'); + public override fConnectionsContainer = + viewChild.required>('fConnectionsContainer'); public get flowId(): string { return this._flowId!; @@ -94,15 +96,21 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { } private _positionChange(): void { - effect(() => { - this._mediator.execute(new InputCanvasPositionRequest(this.transform, this.position())); - }, { injector: this._injector }); + effect( + () => { + this._mediator.execute(new InputCanvasPositionRequest(this.transform, this.position())); + }, + { injector: this._injector }, + ); } private _scaleChange(): void { - effect(() => { - this._mediator.execute(new InputCanvasScaleRequest(this.transform, this.scale())); - }, { injector: this._injector }); + effect( + () => { + this._mediator.execute(new InputCanvasScaleRequest(this.transform, this.scale())); + }, + { injector: this._injector }, + ); } /** @@ -110,7 +118,10 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { */ public override redraw(): void { this._mediator.execute(new SetBackgroundTransformRequest(this.transform)); - this.hostElement.setAttribute("style", `transform: ${TransformModelExtensions.toString(this.transform)}`); + this.hostElement.setAttribute( + 'style', + `transform: ${TransformModelExtensions.toString(this.transform)}`, + ); this._mediator.execute(new NotifyTransformChangedRequest()); } @@ -121,7 +132,10 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { */ public override redrawWithAnimation(): void { this._mediator.execute(new SetBackgroundTransformRequest(this.transform)); - this.hostElement.setAttribute("style", `transition: transform ${isMobile() ? 80 : 150}ms ease-in-out; transform: ${TransformModelExtensions.toString(this.transform)}`); + this.hostElement.setAttribute( + 'style', + `transition: transform ${isMobile() ? 80 : 150}ms ease-in-out; transform: ${TransformModelExtensions.toString(this.transform)}`, + ); transitionEnd(this.hostElement, () => this.redraw()); } @@ -131,7 +145,9 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { * @param animated - If true, the centering will be animated; otherwise, it will be instantaneous. */ public centerGroupOrNode(groupOrNodeId: string, animated: boolean = true): void { - setTimeout(() => this._mediator.execute(new CenterGroupOrNodeRequest(groupOrNodeId, animated))); + this._afterRedraw(() => { + this._mediator.execute(new CenterGroupOrNodeRequest(groupOrNodeId, animated)); + }); } /** @@ -139,8 +155,13 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { * @param padding - paddings from the bounds of the canvas * @param animated - If true, the fit will be animated; otherwise, it will be instantaneous. */ - public fitToScreen(padding: IPoint = PointExtensions.initialize(), animated: boolean = true): void { - setTimeout(() => this._mediator.execute(new FitToFlowRequest(padding, animated))); + public fitToScreen( + padding: IPoint = PointExtensions.initialize(), + animated: boolean = true, + ): void { + this._afterRedraw(() => { + this._mediator.execute(new FitToFlowRequest(padding, animated)); + }); } /** @@ -151,7 +172,9 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { * This is useful for providing a smooth user experience when resetting the view. */ public resetScaleAndCenter(animated: boolean = true): void { - setTimeout(() => this._mediator.execute(new ResetScaleAndCenterRequest(animated))); + this._afterRedraw(() => { + this._mediator.execute(new ResetScaleAndCenterRequest(animated)); + }); } /** @@ -161,14 +184,6 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { return this.transform.scale || 1; } - /** - * @deprecated Method "setZoom" is deprecated. Use "setScale" instead. This method will be removed in version 18.0.0.`, - */ - @Deprecated('setScale') - public setZoom(scale: number, toPosition: IPoint = PointExtensions.initialize()): void { - this.setScale(scale, toPosition); - } - /** * Sets the scale of the canvas to a specified value. * This method is used to zoom in or out of the canvas, @@ -180,14 +195,6 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { this._mediator.execute(new UpdateScaleRequest(scale, toPosition)); } - /** - * @deprecated Method "resetZoom" is deprecated. Use "resetScale" instead. This method will be removed in version 18.0.0.`, - */ - @Deprecated('resetScale') - public resetZoom(): void { - this.resetScale(); - } - /** * Resets the scale of the canvas to its default value. * This method is used to restore the canvas to its original scale. @@ -199,4 +206,11 @@ export class FCanvasComponent extends FCanvasBase implements OnInit, OnDestroy { public ngOnDestroy(): void { this._mediator.execute(new RemoveCanvasFromStoreRequest()); } + + private _afterRedraw(callback: () => void): void { + this._mediator + .execute(new ListenDataChangesRequest()) + .pipe(takeOne()) + .listen(this.destroyRef, callback); + } } diff --git a/projects/f-flow/src/f-connection/f-connection-content/f-connection-content.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/f-connection-content.ts similarity index 54% rename from projects/f-flow/src/f-connection/f-connection-content/f-connection-content.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/f-connection-content.ts index c9b838ac..f99814c5 100644 --- a/projects/f-flow/src/f-connection/f-connection-content/f-connection-content.ts +++ b/projects/f-flow/src/f-connection-v2/components/connection-content/f-connection-content.ts @@ -1,18 +1,10 @@ -import { - Directive, - effect, - ElementRef, - inject, - Injector, - input, - OnInit, - untracked, -} from '@angular/core'; -import { NotifyDataChangedRequest } from '../../f-storage'; +import { Directive, effect, inject, Injector, input, OnInit, untracked } from '@angular/core'; +import { NotifyDataChangedRequest } from '../../../f-storage'; import { FMediator } from '@foblex/mediator'; import { castToEnum } from '@foblex/utils'; -import { IPolylineContent, PolylineContentAlign } from './polyline-content-engine'; import { coerceNumberProperty } from '@angular/cdk/coercion'; +import { F_CONNECTION_CONTENT, FConnectionContentBase } from './models'; +import { PolylineContentAlign } from './utils'; /** * Directive for placing custom user content (text, icons, buttons, etc.) @@ -47,26 +39,13 @@ import { coerceNumberProperty } from '@angular/cdk/coercion'; host: { 'class': 'f-connection-content', }, + providers: [{ provide: F_CONNECTION_CONTENT, useExisting: FConnectionContent, multi: true }], }) -export class FConnectionContent implements OnInit, IPolylineContent { +export class FConnectionContent extends FConnectionContentBase implements OnInit { private readonly _mediator = inject(FMediator); private readonly _injector = inject(Injector); - /** - * The host DOM element to which the directive is applied. - * Used internally for positioning calculations. - */ - public readonly hostElement = inject(ElementRef).nativeElement; - - /** - * Position along the connection. - * - * A normalized value in the range `0..1`: - * - `0` — at the start of the connection, - * - `1` — at the end of the connection, - * - `0.5` — at the middle of the connection (default). - */ - public readonly position = input(0.5, { + public override readonly position = input(0.5, { transform: (x) => { const v = coerceNumberProperty(x); @@ -74,26 +53,11 @@ export class FConnectionContent implements OnInit, IPolylineContent { }, }); - /** - * Perpendicular offset from the connection line (in pixels). - * - * - Positive values shift the element to the right - * relative to the line direction. - * - Negative values shift it to the left. - * - Default: `0` (no shift). - */ - public readonly offset = input(0, { + public override readonly offset = input(0, { transform: (x) => coerceNumberProperty(x), }); - /** - * Controls the orientation (rotation) of the content relative to the connection. - * - * Possible values: - * - `'none'` — no rotation (default). - * - `'along'` — aligned along the path (tangent). - */ - public readonly align = input(PolylineContentAlign.NONE, { + public override readonly align = input(PolylineContentAlign.NONE, { transform: (x) => castToEnum(x, 'align', PolylineContentAlign), }); diff --git a/projects/f-flow/src/f-connection-v2/components/connection-content/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/index.ts new file mode 100644 index 00000000..dd1ee5df --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-content/index.ts @@ -0,0 +1,3 @@ +export * from './models'; +export * from './utils'; +export * from './f-connection-content'; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-content/models/f-connection-content-base.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/models/f-connection-content-base.ts new file mode 100644 index 00000000..df4df63d --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-content/models/f-connection-content-base.ts @@ -0,0 +1,43 @@ +import { ElementRef, inject, InjectionToken, Signal } from '@angular/core'; +import { IPolylineContent, PolylineContentAlign } from '../utils'; + +export const F_CONNECTION_CONTENT = new InjectionToken( + 'F_CONNECTION_CONTENT', +); + +export abstract class FConnectionContentBase implements IPolylineContent { + /** + * The host DOM element to which the directive is applied. + * Used internally for positioning calculations. + */ + public readonly hostElement = inject(ElementRef).nativeElement; + + /** + * Position along the connection. + * + * A normalized value in the range `0..1`: + * - `0` — at the start of the connection, + * - `1` — at the end of the connection, + * - `0.5` — at the middle of the connection (default). + */ + public abstract position: Signal; + + /** + * Perpendicular offset from the connection line (in pixels). + * + * - Positive values shift the element to the right + * relative to the line direction. + * - Negative values shift it to the left. + * - Default: `0` (no shift). + */ + public abstract offset: Signal; + + /** + * Controls the orientation (rotation) of the content relative to the connection. + * + * Possible values: + * - `'none'` — no rotation (default). + * - `'along'` — aligned along the path (tangent). + */ + public abstract align: Signal; +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-content/models/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/models/index.ts new file mode 100644 index 00000000..455b1ccb --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-content/models/index.ts @@ -0,0 +1 @@ +export * from './f-connection-content-base'; diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/i-polyline-content.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/i-polyline-content.ts similarity index 100% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/i-polyline-content.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/i-polyline-content.ts diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/index.ts similarity index 100% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/index.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/index.ts diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-content-align.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-content-align.ts similarity index 100% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-content-align.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-content-align.ts diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-content-layout-engine.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-content-layout-engine.ts similarity index 53% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-content-layout-engine.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-content-layout-engine.ts index dc2aceee..776dba4c 100644 --- a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-content-layout-engine.ts +++ b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-content-layout-engine.ts @@ -1,4 +1,4 @@ -import { ILine, IPoint } from '@foblex/2d'; +import { IPoint } from '@foblex/2d'; import { PolylineSampler } from './polyline-sampler'; import { IPolylineContent } from './i-polyline-content'; import { PolylineContentPlace } from './polyline-content-place'; @@ -10,19 +10,8 @@ import { PolylineContentPlace } from './polyline-content-place'; export class ConnectionContentLayoutEngine { constructor(private readonly _placement = new PolylineContentPlace()) {} - public layout( - line: { point1: IPoint; point2: IPoint }, - builderResult: { - points?: IPoint[]; - secondPoint: IPoint; - connectionCenter: IPoint; - penultimatePoint: IPoint; - }, - contents: Iterable, - ): number { - const sampler = new PolylineSampler( - builderResult.points ?? calculatePathPointsIfEmpty(line, builderResult), - ); + public layout(points: IPoint[], contents: Iterable): number { + const sampler = new PolylineSampler(points); const total = sampler.totalLength; for (const item of contents) { @@ -37,14 +26,3 @@ export class ConnectionContentLayoutEngine { return `translate3d(${position.x}px, ${position.y}px, 0) translate(-50%, -50%) rotate(${rotationDeg}deg)`; } } - -function calculatePathPointsIfEmpty( - { point1, point2 }: ILine, - { - secondPoint, - connectionCenter, - penultimatePoint, - }: { secondPoint: IPoint; connectionCenter: IPoint; penultimatePoint: IPoint }, -): IPoint[] { - return [point1, secondPoint || point1, connectionCenter, penultimatePoint || point2, point2]; -} diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-content-place.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-content-place.ts similarity index 98% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-content-place.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-content-place.ts index fc48bc4a..9449546e 100644 --- a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-content-place.ts +++ b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-content-place.ts @@ -2,7 +2,7 @@ import { IPolylineContent } from './i-polyline-content'; import { IPoint } from '@foblex/2d'; import { PolylineSampler } from './polyline-sampler'; import { PolylineContentAlign } from './polyline-content-align'; -import { DragRectCache } from '../../../domain'; +import { DragRectCache } from '../../../../domain'; /** * Encapsulates placement logic for a single content item along the path. diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/i-sampler-result.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/i-sampler-result.ts similarity index 100% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/i-sampler-result.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/i-sampler-result.ts diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/i-tangent.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/i-tangent.ts similarity index 100% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/i-tangent.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/i-tangent.ts diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/index.ts similarity index 100% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/index.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/index.ts diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/polyline-sampler.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/polyline-sampler.ts similarity index 100% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/polyline-sampler.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/polyline-sampler.ts diff --git a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/polyline.ts b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/polyline.ts similarity index 94% rename from projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/polyline.ts rename to projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/polyline.ts index cafe7e65..df793fed 100644 --- a/projects/f-flow/src/f-connection/f-connection-content/polyline-content-engine/polyline-sampler/polyline.ts +++ b/projects/f-flow/src/f-connection-v2/components/connection-content/utils/polyline-sampler/polyline.ts @@ -25,7 +25,10 @@ export class Polyline { if (cleaned.length < 2) { // Fallback to a unit segment to avoid zero-length edge cases. - this.points = [{ x: 0, y: 0 }, { x: 1, y: 0 }]; + this.points = [ + { x: 0, y: 0 }, + { x: 1, y: 0 }, + ]; this.cumulativeLengths = [0, 1]; this.segmentTangents = [{ x: 1, y: 0 }]; this.totalLength = 1; @@ -65,7 +68,7 @@ export class Polyline { * @param points Raw input points; they will be copied and cleaned. */ public static from(points: IPoint[]): Polyline { - return new Polyline(points.map(p => ({ x: p.x, y: p.y }))); + return new Polyline(points.map((p) => ({ x: p.x, y: p.y }))); } /** Remove consecutive duplicate points. */ diff --git a/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/f-connection-drag-handle-end.ts b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/f-connection-drag-handle-end.ts new file mode 100644 index 00000000..be25630a --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/f-connection-drag-handle-end.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { IPoint } from '@foblex/2d'; +import { F_CONNECTION_DRAG_HANDLE_END, FConnectionDragHandleBase } from './models'; + +@Component({ + selector: 'circle[f-connection-drag-handle-end]', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class]': 'class', + }, + providers: [ + { + provide: F_CONNECTION_DRAG_HANDLE_END, + useExisting: FConnectionDragHandleEnd, + }, + ], +}) +export class FConnectionDragHandleEnd extends FConnectionDragHandleBase { + public override redraw(penultimatePoint: IPoint, endPoint: IPoint): void { + this.point = this.calculateCircleCenter(penultimatePoint, endPoint, 8); + this.hostElement.setAttribute('cx', this.point.x.toString()); + this.hostElement.setAttribute('cy', this.point.y.toString()); + } +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/f-connection-drag-handle-start.ts b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/f-connection-drag-handle-start.ts new file mode 100644 index 00000000..93f30237 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/f-connection-drag-handle-start.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { IPoint } from '@foblex/2d'; +import { F_CONNECTION_DRAG_HANDLE_START, FConnectionDragHandleBase } from './models'; + +@Component({ + selector: 'circle[f-connection-drag-handle-start]', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class]': 'class', + }, + providers: [ + { + provide: F_CONNECTION_DRAG_HANDLE_START, + useExisting: FConnectionDragHandleStart, + }, + ], +}) +export class FConnectionDragHandleStart extends FConnectionDragHandleBase { + public override redraw(penultimatePoint: IPoint, startPoint: IPoint): void { + this.point = this.calculateCircleCenter(penultimatePoint, startPoint, 8); + this.hostElement.setAttribute('cx', this.point.x.toString()); + this.hostElement.setAttribute('cy', this.point.y.toString()); + } +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/index.ts new file mode 100644 index 00000000..3fc3b7ed --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/index.ts @@ -0,0 +1,5 @@ +export * from './models'; + +export * from './f-connection-drag-handle-start'; + +export * from './f-connection-drag-handle-end'; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/models/f-connection-drag-handle-base.ts b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/models/f-connection-drag-handle-base.ts new file mode 100644 index 00000000..efa728e8 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/models/f-connection-drag-handle-base.ts @@ -0,0 +1,29 @@ +import { ElementRef, inject, InjectionToken } from '@angular/core'; +import { IPoint, PointExtensions } from '@foblex/2d'; + +export const F_CONNECTION_DRAG_HANDLE_END = new InjectionToken( + 'F_CONNECTION_DRAG_HANDLE_END', +); + +export const F_CONNECTION_DRAG_HANDLE_START = new InjectionToken( + 'F_CONNECTION_DRAG_HANDLE_START', +); + +export abstract class FConnectionDragHandleBase { + public readonly hostElement = inject(ElementRef).nativeElement; + + public point = PointExtensions.initialize(); + + protected readonly class = 'f-connection-drag-handle'; + + public abstract redraw(penultimatePoint: IPoint, startPoint: IPoint): void; + + protected calculateCircleCenter(start: IPoint, end: IPoint, radius: number): IPoint { + const direction = { x: end.x - start.x, y: end.y - start.y }; + const length = Math.sqrt(direction.x * direction.x + direction.y * direction.y) || 1; + const unitDirection = { x: direction.x / length, y: direction.y / length }; + const scaledDirection = { x: unitDirection.x * radius, y: unitDirection.y * radius }; + + return { x: end.x - scaledDirection.x, y: end.y - scaledDirection.y }; + } +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/models/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/models/index.ts new file mode 100644 index 00000000..a3d33f1d --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-drag-handles/models/index.ts @@ -0,0 +1 @@ +export * from './f-connection-drag-handle-base'; diff --git a/projects/f-flow/src/f-connection/common/f-gradient/f-connection-gradient.component.html b/projects/f-flow/src/f-connection-v2/components/connection-gradient/f-connection-gradient.html similarity index 68% rename from projects/f-flow/src/f-connection/common/f-gradient/f-connection-gradient.component.html rename to projects/f-flow/src/f-connection-v2/components/connection-gradient/f-connection-gradient.html index f2060dd3..725fceff 100644 --- a/projects/f-flow/src/f-connection/common/f-gradient/f-connection-gradient.component.html +++ b/projects/f-flow/src/f-connection-v2/components/connection-gradient/f-connection-gradient.html @@ -1,4 +1,4 @@ - - + + diff --git a/projects/f-flow/src/f-connection/common/f-gradient/f-connection-gradient.component.ts b/projects/f-flow/src/f-connection-v2/components/connection-gradient/f-connection-gradient.ts similarity index 59% rename from projects/f-flow/src/f-connection/common/f-gradient/f-connection-gradient.component.ts rename to projects/f-flow/src/f-connection-v2/components/connection-gradient/f-connection-gradient.ts index 957ebd00..1a938fc5 100644 --- a/projects/f-flow/src/f-connection/common/f-gradient/f-connection-gradient.component.ts +++ b/projects/f-flow/src/f-connection-v2/components/connection-gradient/f-connection-gradient.ts @@ -1,28 +1,27 @@ -import { ChangeDetectionStrategy, Component, ElementRef, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { ILine, Point } from '@foblex/2d'; -import { F_CONNECTION_IDENTIFIERS } from '../f-connection-identifiers'; -import { IHasConnectionFromTo } from '../i-has-connection-from-to'; -import { IHasConnectionColor } from '../i-has-connection-color'; -import { F_CONNECTION } from '../f-connection.injection-token'; -import { CONNECTION_GRADIENT, IConnectionGradient } from './i-connection-gradient'; +import { F_CONNECTION_GRADIENT, FConnectionGradientBase } from './models'; +import { F_CONNECTION_COMPONENTS_PARENT } from '../../models'; +import { createGradientDomIdentifier } from '../../utils'; @Component({ selector: 'linearGradient[fConnectionGradient]', - templateUrl: './f-connection-gradient.component.html', + templateUrl: './f-connection-gradient.html', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'f-component f-connection-gradient', '[attr.id]': 'gradientId', }, - providers: [{ provide: CONNECTION_GRADIENT, useExisting: FConnectionGradientComponent }], + providers: [{ provide: F_CONNECTION_GRADIENT, useExisting: FConnectionGradient }], }) -export class FConnectionGradientComponent implements IConnectionGradient { - public readonly hostElement = inject(ElementRef).nativeElement; - private readonly _base = inject(F_CONNECTION) as IHasConnectionColor & IHasConnectionFromTo; +export class FConnectionGradient extends FConnectionGradientBase { + private readonly _connection = inject(F_CONNECTION_COMPONENTS_PARENT); public get gradientId(): string { - return F_CONNECTION_IDENTIFIERS.gradientId( - this._base.fId() + this._base.fOutputId() + this._base.fInputId(), + return createGradientDomIdentifier( + this._connection.fId(), + this._connection.fOutputId(), + this._connection.fInputId(), ); } @@ -40,8 +39,8 @@ export class FConnectionGradientComponent implements IConnectionGradient { } private _updateGradient(): void { - this._setFromColor(this._base.fStartColor()); - this._setToColor(this._base.fEndColor()); + this._setFromColor(this._connection.fStartColor()); + this._setToColor(this._connection.fEndColor()); } private _setFromColor(color: string | undefined): void { @@ -52,7 +51,7 @@ export class FConnectionGradientComponent implements IConnectionGradient { this.stop2Element.setAttribute('stop-color', color || 'transparent'); } - public redraw(line: ILine): void { + public override redraw(line: ILine): void { const x: number = line.point2.x - line.point1.x; const y: number = line.point2.y - line.point1.y; const distance: number = Math.sqrt(x * x + y * y) || 0.01; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-gradient/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-gradient/index.ts new file mode 100644 index 00000000..903cc7d5 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-gradient/index.ts @@ -0,0 +1,2 @@ +export * from './models'; +export * from './f-connection-gradient'; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-gradient/models/f-connection-gradient-base.ts b/projects/f-flow/src/f-connection-v2/components/connection-gradient/models/f-connection-gradient-base.ts new file mode 100644 index 00000000..a8ef148a --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-gradient/models/f-connection-gradient-base.ts @@ -0,0 +1,14 @@ +import { ILine } from '@foblex/2d'; +import { ElementRef, inject, InjectionToken } from '@angular/core'; + +export const F_CONNECTION_GRADIENT = new InjectionToken( + 'F_CONNECTION_GRADIENT', +); + +export abstract class FConnectionGradientBase { + public readonly hostElement = inject(ElementRef).nativeElement; + + public abstract initialize(): void; + + public abstract redraw(line: ILine): void; +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-gradient/models/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-gradient/models/index.ts new file mode 100644 index 00000000..e41442a9 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-gradient/models/index.ts @@ -0,0 +1 @@ +export * from './f-connection-gradient-base'; diff --git a/projects/f-flow/src/f-connection/f-marker/e-f-marker-type.ts b/projects/f-flow/src/f-connection-v2/components/connection-marker/enums/e-f-marker-type.ts similarity index 99% rename from projects/f-flow/src/f-connection/f-marker/e-f-marker-type.ts rename to projects/f-flow/src/f-connection-v2/components/connection-marker/enums/e-f-marker-type.ts index dbcddc74..03df8c4c 100644 --- a/projects/f-flow/src/f-connection/f-marker/e-f-marker-type.ts +++ b/projects/f-flow/src/f-connection-v2/components/connection-marker/enums/e-f-marker-type.ts @@ -1,5 +1,4 @@ export enum EFMarkerType { - START = 'f-connection-marker-start', END = 'f-connection-marker-end', diff --git a/projects/f-flow/src/f-connection-v2/components/connection-marker/enums/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-marker/enums/index.ts new file mode 100644 index 00000000..8524f6bf --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-marker/enums/index.ts @@ -0,0 +1 @@ +export * from './e-f-marker-type'; diff --git a/projects/f-flow/src/f-connection/f-marker/f-marker.directive.ts b/projects/f-flow/src/f-connection-v2/components/connection-marker/f-connection-marker.ts similarity index 69% rename from projects/f-flow/src/f-connection/f-marker/f-marker.directive.ts rename to projects/f-flow/src/f-connection-v2/components/connection-marker/f-connection-marker.ts index c81e0b8d..76a6ba60 100644 --- a/projects/f-flow/src/f-connection/f-marker/f-marker.directive.ts +++ b/projects/f-flow/src/f-connection-v2/components/connection-marker/f-connection-marker.ts @@ -1,21 +1,20 @@ -import { Directive, ElementRef, inject, Input, OnDestroy, OnInit } from '@angular/core'; -import { F_MARKER, FMarkerBase } from './f-marker-base'; -import { EFMarkerType } from './e-f-marker-type'; +import { Directive, inject, Input, OnDestroy, OnInit } from '@angular/core'; import { FMediator } from '@foblex/mediator'; +import { F_CONNECTION_MARKER, FConnectionMarkerBase } from './models'; +import { EFMarkerType } from './enums'; import { AddConnectionMarkerToStoreRequest, RemoveConnectionMarkerFromStoreRequest, -} from '../../domain'; +} from '../../../domain'; @Directive({ selector: 'svg[fMarker]', host: { class: 'f-component f-marker', }, - providers: [{ provide: F_MARKER, useExisting: FMarkerDirective }], + providers: [{ provide: F_CONNECTION_MARKER, useExisting: FConnectionMarker }], }) -export class FMarkerDirective extends FMarkerBase implements OnInit, OnDestroy { - public readonly hostElement = inject(ElementRef).nativeElement; +export class FConnectionMarker extends FConnectionMarkerBase implements OnInit, OnDestroy { private readonly _mediator = inject(FMediator); @Input() diff --git a/projects/f-flow/src/f-connection-v2/components/connection-marker/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-marker/index.ts new file mode 100644 index 00000000..41545251 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-marker/index.ts @@ -0,0 +1,5 @@ +export * from './enums'; + +export * from './models'; + +export * from './f-connection-marker'; diff --git a/projects/f-flow/src/f-connection/f-marker/f-marker-base.ts b/projects/f-flow/src/f-connection-v2/components/connection-marker/models/f-connection-marker-base.ts similarity index 52% rename from projects/f-flow/src/f-connection/f-marker/f-marker-base.ts rename to projects/f-flow/src/f-connection-v2/components/connection-marker/models/f-connection-marker-base.ts index 908c4955..e1fe3558 100644 --- a/projects/f-flow/src/f-connection/f-marker/f-marker-base.ts +++ b/projects/f-flow/src/f-connection-v2/components/connection-marker/models/f-connection-marker-base.ts @@ -1,10 +1,9 @@ -import { InjectionToken } from '@angular/core'; -import { IHasHostElement } from '../../i-has-host-element'; +import { ElementRef, inject, InjectionToken } from '@angular/core'; -export const F_MARKER = new InjectionToken('F_MARKER'); +export const F_CONNECTION_MARKER = new InjectionToken('F_CONNECTION_MARKER'); -export abstract class FMarkerBase implements IHasHostElement { - public abstract hostElement: HTMLElement; +export abstract class FConnectionMarkerBase { + public readonly hostElement = inject(ElementRef).nativeElement; public abstract width: number; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-marker/models/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-marker/models/index.ts new file mode 100644 index 00000000..8904d236 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-marker/models/index.ts @@ -0,0 +1 @@ +export * from './f-connection-marker-base'; diff --git a/projects/f-flow/src/f-connection/common/f-path/f-connection-path.component.scss b/projects/f-flow/src/f-connection-v2/components/connection-path/f-connection-path.scss similarity index 100% rename from projects/f-flow/src/f-connection/common/f-path/f-connection-path.component.scss rename to projects/f-flow/src/f-connection-v2/components/connection-path/f-connection-path.scss diff --git a/projects/f-flow/src/f-connection-v2/components/connection-path/f-connection-path.ts b/projects/f-flow/src/f-connection-v2/components/connection-path/f-connection-path.ts new file mode 100644 index 00000000..7f09ce79 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-path/f-connection-path.ts @@ -0,0 +1,90 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { normalizeDomElementId } from '@foblex/utils'; +import { F_CONNECTION_PATH, FConnectionPathBase } from './models'; +import { createConnectionDomIdentifier, createGradientDomUrl } from '../../utils'; +import { F_CONNECTION_COMPONENTS_PARENT } from '../../models'; + +@Component({ + selector: 'path[f-connection-path]', + template: '', + styleUrls: ['./f-connection-path.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'f-component f-connection-path', + '[attr.id]': 'attrConnectionId', + '[attr.data-f-path-id]': 'fPathId', + '[attr.stroke]': 'linkToGradient', + }, + providers: [ + { + provide: F_CONNECTION_PATH, + useExisting: FConnectionPath, + }, + ], +}) +export class FConnectionPath extends FConnectionPathBase { + private readonly _connection = inject(F_CONNECTION_COMPONENTS_PARENT); + + public get fPathId(): string { + return this._connection.fId(); + } + + public get linkToGradient(): string { + return createGradientDomUrl( + this._connection.fId(), + this._connection.fOutputId(), + this._connection.fInputId(), + ); + } + + public get attrConnectionId(): string { + return createConnectionDomIdentifier( + this._connection.fId(), + this._connection.fOutputId(), + this._connection.fInputId(), + ); + } + + public override initialize(): void { + this.deselect(); + } + + public override setPath(path: string): void { + this.hostElement.setAttribute('d', `${path}`); + } + + public override select(): void { + this.hostElement.setAttribute( + 'marker-start', + `url(#${getMarkerSelectedStartId(this._connection.fId())})`, + ); + this.hostElement.setAttribute( + 'marker-end', + `url(#${getMarkerSelectedEndId(this._connection.fId())})`, + ); + } + + public override deselect(): void { + this.hostElement.setAttribute( + 'marker-start', + `url(#${getMarkerStartId(this._connection.fId())})`, + ); + this.hostElement.setAttribute('marker-end', `url(#${getMarkerEndId(this._connection.fId())})`); + } +} + +function getMarkerStartId(fConnectionId: string): string { + return normalizeDomElementId(`f-connection-marker-start-${fConnectionId}`); +} + +function getMarkerEndId(fConnectionId: string): string { + return normalizeDomElementId(`f-connection-marker-end-${fConnectionId}`); +} + +function getMarkerSelectedStartId(fConnectionId: string): string { + return normalizeDomElementId(`f-connection-selected-marker-start-${fConnectionId}`); +} + +function getMarkerSelectedEndId(fConnectionId: string): string { + return normalizeDomElementId(`f-connection-selected-marker-end-${fConnectionId}`); +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-path/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-path/index.ts new file mode 100644 index 00000000..82e5d369 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-path/index.ts @@ -0,0 +1,3 @@ +export * from './models'; + +export * from './f-connection-path'; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-path/models/f-connection-path-base.ts b/projects/f-flow/src/f-connection-v2/components/connection-path/models/f-connection-path-base.ts new file mode 100644 index 00000000..5acdcc9e --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-path/models/f-connection-path-base.ts @@ -0,0 +1,15 @@ +import { ElementRef, inject, InjectionToken } from '@angular/core'; + +export const F_CONNECTION_PATH = new InjectionToken('F_CONNECTION_PATH'); + +export abstract class FConnectionPathBase { + public readonly hostElement = inject(ElementRef).nativeElement; + + public abstract initialize(): void; + + public abstract setPath(path: string): void; + + public abstract select(): void; + + public abstract deselect(): void; +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-path/models/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-path/models/index.ts new file mode 100644 index 00000000..d16632a7 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-path/models/index.ts @@ -0,0 +1 @@ +export * from './f-connection-path-base'; diff --git a/projects/f-flow/src/f-connection/common/f-selection/f-connection-selection.component.scss b/projects/f-flow/src/f-connection-v2/components/connection-selection/f-connection-selection.scss similarity index 100% rename from projects/f-flow/src/f-connection/common/f-selection/f-connection-selection.component.scss rename to projects/f-flow/src/f-connection-v2/components/connection-selection/f-connection-selection.scss diff --git a/projects/f-flow/src/f-connection-v2/components/connection-selection/f-connection-selection.ts b/projects/f-flow/src/f-connection-v2/components/connection-selection/f-connection-selection.ts new file mode 100644 index 00000000..cfdfd5c9 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-selection/f-connection-selection.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { F_CONNECTION_SELECTION, FConnectionSelectionBase } from './models'; +import { createConnectionSelectionDomIdentifier } from '../../utils'; +import { F_CONNECTION_COMPONENTS_PARENT } from '../../models'; + +@Component({ + selector: 'path[fConnectionSelection]', + template: '', + styleUrls: ['./f-connection-selection.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'f-component f-connection-selection', + '[attr.id]': 'connectionForSelectionId', + }, + providers: [ + { + provide: F_CONNECTION_SELECTION, + useExisting: FConnectionSelection, + }, + ], +}) +export class FConnectionSelection extends FConnectionSelectionBase { + private readonly _connection = inject(F_CONNECTION_COMPONENTS_PARENT); + + public get connectionForSelectionId(): string { + return createConnectionSelectionDomIdentifier( + this._connection.fId(), + this._connection.fOutputId(), + this._connection.fInputId(), + ); + } + + public override setPath(path: string) { + this.hostElement.setAttribute('d', `${path}`); + } +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-selection/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-selection/index.ts new file mode 100644 index 00000000..b817f76c --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-selection/index.ts @@ -0,0 +1,2 @@ +export * from './models'; +export * from './f-connection-selection'; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-selection/models/f-connection-selection-base.ts b/projects/f-flow/src/f-connection-v2/components/connection-selection/models/f-connection-selection-base.ts new file mode 100644 index 00000000..4fd8e127 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-selection/models/f-connection-selection-base.ts @@ -0,0 +1,11 @@ +import { ElementRef, inject, InjectionToken } from '@angular/core'; + +export const F_CONNECTION_SELECTION = new InjectionToken( + 'F_CONNECTION_SELECTION', +); + +export abstract class FConnectionSelectionBase { + public readonly hostElement = inject(ElementRef).nativeElement; + + public abstract setPath(path: string): void; +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-selection/models/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-selection/models/index.ts new file mode 100644 index 00000000..29f781e4 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-selection/models/index.ts @@ -0,0 +1 @@ +export * from './f-connection-selection-base'; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/f-connection-waypoints.html b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/f-connection-waypoints.html new file mode 100644 index 00000000..7e5a9cc0 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/f-connection-waypoints.html @@ -0,0 +1,24 @@ +@if (visibility()) { + + + @for (candidate of candidates(); track $index) { + + } + @for (point of waypoints(); track $index) { + + } + + +} + diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/f-connection-waypoints.scss b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/f-connection-waypoints.scss new file mode 100644 index 00000000..7b75a17d --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/f-connection-waypoints.scss @@ -0,0 +1,15 @@ +:host { + pointer-events: none; + position: absolute; +} + +svg { + display: block; + vertical-align: middle; + position: absolute; + overflow: visible; +} + +circle { + pointer-events: visible; +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/f-connection-waypoints.ts b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/f-connection-waypoints.ts new file mode 100644 index 00000000..f285635a --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/f-connection-waypoints.ts @@ -0,0 +1,76 @@ +import { + booleanAttribute, + ChangeDetectionStrategy, + Component, + effect, + inject, + Injector, + input, + model, + numberAttribute, + OnDestroy, + OnInit, + untracked, +} from '@angular/core'; +import { F_CONNECTION_WAYPOINTS, FConnectionWaypointsBase } from './models'; +import { IPoint } from '@foblex/2d'; +import { NotifyDataChangedRequest } from '../../../f-storage'; +import { FMediator } from '@foblex/mediator'; +import { F_CONNECTION_COMPONENTS_PARENT } from '../../models'; +import { RemoveConnectionWaypointRequest } from '../../../domain'; + +@Component({ + selector: 'f-connection-waypoints', + templateUrl: './f-connection-waypoints.html', + styleUrls: ['./f-connection-waypoints.scss'], + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'f-component f-connection-waypoints', + }, + providers: [{ provide: F_CONNECTION_WAYPOINTS, useExisting: FConnectionWaypoints }], +}) +export class FConnectionWaypoints extends FConnectionWaypointsBase implements OnInit, OnDestroy { + private readonly _mediator = inject(FMediator); + private readonly _injector = inject(Injector); + private readonly _connection = inject(F_CONNECTION_COMPONENTS_PARENT); + + public override readonly radius = input(4, { + transform: numberAttribute, + }); + public override readonly waypoints = model([]); + + public override readonly visibility = input(true, { + transform: booleanAttribute, + }); + + public ngOnInit(): void { + this._listenChanges(); + } + + private _listenChanges(): void { + effect( + () => { + this.radius(); + this.waypoints(); + this.visibility(); + untracked(() => this._notifyDataChanged()); + }, + { injector: this._injector }, + ); + } + + private _notifyDataChanged(): void { + this._mediator.execute(new NotifyDataChangedRequest()); + } + + protected remove(index: number, event: MouseEvent): void { + this._mediator.execute(new RemoveConnectionWaypointRequest(index, this._connection.fId())); + event.stopPropagation(); + event.preventDefault(); + } + + public ngOnDestroy(): void { + this._notifyDataChanged(); + } +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/index.ts new file mode 100644 index 00000000..ee36845a --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/index.ts @@ -0,0 +1,3 @@ +export * from './models'; +export * from './utils'; +export * from './f-connection-waypoints'; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/models/f-connection-waypoints-base.ts b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/models/f-connection-waypoints-base.ts new file mode 100644 index 00000000..ff189d1e --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/models/f-connection-waypoints-base.ts @@ -0,0 +1,42 @@ +import { ElementRef, inject, InjectionToken, ModelSignal, signal, Signal } from '@angular/core'; +import { IPoint, PointExtensions } from '@foblex/2d'; + +export const F_CONNECTION_WAYPOINTS = new InjectionToken( + 'F_CONNECTION_WAYPOINTS', +); + +export abstract class FConnectionWaypointsBase { + public readonly hostElement = inject(ElementRef).nativeElement; + + public readonly candidates = signal([]); + + public abstract waypoints: ModelSignal; + public abstract radius: Signal; + public abstract visibility: Signal; + + private _activeIndex = 0; + private _waypoints: IPoint[] = []; + + public insert(candidate: IPoint): void { + const current = this.waypoints().slice(); + + this._activeIndex = Math.max(0, Math.min(this.candidates().indexOf(candidate), current.length)); + + current.splice(this._activeIndex, 0, { ...candidate }); + this.waypoints.set(current); + this._waypoints = current; + } + + public select(waypoint: IPoint): void { + this._activeIndex = this.waypoints().findIndex((x) => PointExtensions.isEqual(waypoint, x)); + this._waypoints = this.waypoints(); + } + + public move(point: IPoint): void { + this._waypoints[this._activeIndex] = { ...point }; + } + + public update(): void { + this.waypoints.set([...this._waypoints]); + } +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/models/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/models/index.ts new file mode 100644 index 00000000..7b566b47 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/models/index.ts @@ -0,0 +1 @@ +export * from './f-connection-waypoints-base'; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/find-existing-waypoint.ts b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/find-existing-waypoint.ts new file mode 100644 index 00000000..a2ad4677 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/find-existing-waypoint.ts @@ -0,0 +1,14 @@ +import { IPoint } from '@foblex/2d'; +import { FConnectionWaypointsBase } from '../models'; +import { Signal } from '@angular/core'; +import { isPointerInsidePoint } from './is-pointer-inside-point'; + +export function findExistingWaypoint( + connection: { fWaypoints: Signal }, + position: IPoint, +): IPoint | undefined { + const component = connection.fWaypoints(); + const radius = component?.radius() || 8; + + return component?.waypoints().find((x) => isPointerInsidePoint(position, x, radius)); +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/find-waypoint-candidate.ts b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/find-waypoint-candidate.ts new file mode 100644 index 00000000..243d515c --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/find-waypoint-candidate.ts @@ -0,0 +1,14 @@ +import { Signal } from '@angular/core'; +import { IPoint } from '@foblex/2d'; +import { isPointerInsidePoint } from './is-pointer-inside-point'; +import { FConnectionWaypointsBase } from '../models'; + +export function findWaypointCandidate( + connection: { fWaypoints: Signal }, + position: IPoint, +): IPoint | undefined { + const component = connection.fWaypoints(); + const radius = component?.radius() || 8; + + return component?.candidates().find((x) => isPointerInsidePoint(position, x, radius)); +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/index.ts b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/index.ts new file mode 100644 index 00000000..1f7f5ac1 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/index.ts @@ -0,0 +1,4 @@ +export * from './find-waypoint-candidate'; +export * from './pick-waypoint'; +export * from './find-existing-waypoint'; +export * from './is-pointer-inside-point'; diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/is-pointer-inside-point.ts b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/is-pointer-inside-point.ts new file mode 100644 index 00000000..c7743571 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/is-pointer-inside-point.ts @@ -0,0 +1,5 @@ +import { IPoint } from '@foblex/2d'; + +export function isPointerInsidePoint(point: IPoint, circleCenter: IPoint, radius: number): boolean { + return (point.x - circleCenter.x) ** 2 + (point.y - circleCenter.y) ** 2 <= radius ** 2; +} diff --git a/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/pick-waypoint.ts b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/pick-waypoint.ts new file mode 100644 index 00000000..002ac818 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/connection-waypoints/utils/pick-waypoint.ts @@ -0,0 +1,33 @@ +import { IPoint } from '@foblex/2d'; +import { Signal } from '@angular/core'; + +import { FConnectionWaypointsBase } from '../models'; +import { findWaypointCandidate } from './find-waypoint-candidate'; +import { findExistingWaypoint } from './find-existing-waypoint'; + +export type WaypointPick = + | { connection: T; waypoint: IPoint; candidate?: never } + | { connection: T; candidate: IPoint; waypoint?: never }; + +type HasWaypoints = { + fWaypoints: Signal; +}; + +export function pickWaypoint( + connections: readonly T[], + position: IPoint, +): WaypointPick | undefined { + for (const connection of connections) { + const waypoint = findExistingWaypoint(connection, position); + if (waypoint && connection.fWaypoints()?.visibility()) { + return { connection, waypoint }; + } + + const candidate = findWaypointCandidate(connection, position); + if (candidate && connection.fWaypoints()?.visibility()) { + return { connection, candidate }; + } + } + + return undefined; +} diff --git a/projects/f-flow/src/f-connection/common/f-connection-base.ts b/projects/f-flow/src/f-connection-v2/components/f-connection-base.ts similarity index 51% rename from projects/f-flow/src/f-connection/common/f-connection-base.ts rename to projects/f-flow/src/f-connection-v2/components/f-connection-base.ts index 5a379a8e..a4ea2e4f 100644 --- a/projects/f-flow/src/f-connection/common/f-connection-base.ts +++ b/projects/f-flow/src/f-connection-v2/components/f-connection-base.ts @@ -1,4 +1,5 @@ import { + contentChild, contentChildren, Directive, ElementRef, @@ -9,31 +10,27 @@ import { viewChild, } from '@angular/core'; import { ILine, IPoint, LineExtensions, PointExtensions } from '@foblex/2d'; -import { EFConnectionBehavior } from './e-f-connection-behavior'; -import { EFConnectionType } from './e-f-connection-type'; -import { IHasConnectionColor } from './i-has-connection-color'; -import { IHasConnectionFromTo } from './i-has-connection-from-to'; -import { IHasConnectionText } from './i-has-connection-text'; -import { CONNECTION_PATH, IConnectionPath } from './f-path'; -import { CONNECTION_GRADIENT, IConnectionGradient } from './f-gradient'; import { - FConnectionDragHandleEndComponent, - FConnectionDragHandleStartComponent, -} from './f-drag-handle'; -import { FConnectionSelectionComponent } from './f-selection'; -import { CONNECTION_TEXT, IConnectionText } from './f-connection-text'; -import { EFConnectableSide } from '../../f-connectors'; -import { FConnectionFactory } from '../f-connection-builder'; -import { IHasHostElement } from '../../i-has-host-element'; -import { - ISelectable, ICanChangeVisibility, + ISelectable, mixinChangeSelection, mixinChangeVisibility, } from '../../mixins'; -import { FConnectionCenterDirective } from '../f-connection-center'; -import { ConnectionContentLayoutEngine, FConnectionContent } from '../f-connection-content'; -import { EFConnectionConnectableSide } from './e-f-connection-connectable-side'; +import { ConnectionLineBuilder, EFConnectionBehavior, EFConnectionConnectableSide } from '../utils'; +import { EFConnectableSide, EFConnectionType } from '../enums'; +import { F_CONNECTION_PATH } from './connection-path'; +import { F_CONNECTION_GRADIENT } from './connection-gradient'; +import { + F_CONNECTION_DRAG_HANDLE_END, + F_CONNECTION_DRAG_HANDLE_START, +} from './connection-drag-handles'; +import { F_CONNECTION_SELECTION } from './connection-selection'; +import { + ConnectionContentLayoutEngine, + F_CONNECTION_CONTENT, + FConnectionContentBase, +} from './connection-content'; +import { F_CONNECTION_WAYPOINTS } from './connection-waypoints'; const MIXIN_BASE = mixinChangeSelection( mixinChangeVisibility( @@ -46,15 +43,9 @@ const MIXIN_BASE = mixinChangeSelection( @Directive() export abstract class FConnectionBase extends MIXIN_BASE - implements - IHasHostElement, - ISelectable, - ICanChangeVisibility, - IHasConnectionColor, - IHasConnectionFromTo, - IHasConnectionText + implements ISelectable, ICanChangeVisibility { - private readonly _connectionFactory = inject(FConnectionFactory); + private readonly _fConnectionBuilder = inject(ConnectionLineBuilder); public abstract override fId: Signal; @@ -88,31 +79,21 @@ export abstract class FConnectionBase public readonly fDefs = viewChild.required>('defs'); - public readonly fPath = viewChild.required(CONNECTION_PATH); - - public readonly fGradient = viewChild.required(CONNECTION_GRADIENT); - - public readonly fDragHandleStart = viewChild(FConnectionDragHandleStartComponent); - - public readonly fDragHandleEnd = viewChild.required(FConnectionDragHandleEndComponent); - - public readonly fSelection = viewChild.required(FConnectionSelectionComponent); + public readonly fPath = viewChild.required(F_CONNECTION_PATH); - public readonly fTextComponent = viewChild(CONNECTION_TEXT); + public readonly fGradient = viewChild.required(F_CONNECTION_GRADIENT); - public abstract fText: string; + public readonly fDragHandleStart = viewChild(F_CONNECTION_DRAG_HANDLE_START); - public abstract fTextStartOffset: string; + public readonly fDragHandleEnd = viewChild.required(F_CONNECTION_DRAG_HANDLE_END); - public readonly fConnectionCenter = viewChild>('fConnectionCenter'); + public readonly fSelection = viewChild.required(F_CONNECTION_SELECTION); - public readonly fConnectionCenters = contentChildren(FConnectionCenterDirective, { + public readonly fContents = contentChildren(F_CONNECTION_CONTENT, { descendants: true, }); - public readonly fConnectionContents = contentChildren(FConnectionContent, { - descendants: true, - }); + public readonly fWaypoints = contentChild(F_CONNECTION_WAYPOINTS); public readonly fInputSide: Signal = signal( EFConnectionConnectableSide.DEFAULT, @@ -140,51 +121,48 @@ export abstract class FConnectionBase } public isContains(element: HTMLElement | SVGElement): boolean { - return (this.hostElement.firstChild?.lastChild as HTMLElement).contains(element); + return ( + (this.hostElement.firstChild?.lastChild as HTMLElement).contains(element) || + Array.from(this.fContents()?.values() ?? []).some((x) => x.hostElement?.contains(element)) || + this.fWaypoints()?.hostElement?.contains(element) + ); } public setLine({ point1, point2 }: ILine): void { this.line = LineExtensions.initialize(point1, point2); - const pathResult = this._getPathResult(point1, point2); - - this.path = pathResult.path; - this._penultimatePoint = pathResult.penultimatePoint || point1; - this._secondPoint = pathResult.secondPoint || point2; + const { path, points, penultimatePoint, secondPoint, candidates } = this._getPathResult( + point1, + point2, + ); - new ConnectionContentLayoutEngine().layout(this.line, pathResult, this._contents()); + this.path = path; + this._penultimatePoint = penultimatePoint || point1; + this._secondPoint = secondPoint || point2; + this.fWaypoints()?.candidates.set(candidates || []); - this.fConnectionCenter()?.nativeElement?.setAttribute( - 'style', - this._createTransformString(pathResult.connectionCenter), - ); + new ConnectionContentLayoutEngine().layout(points || [], this._contents()); } - private _contents(): FConnectionContent[] { - return Array.from(this.fConnectionContents()?.values() ?? []); + private _contents(): FConnectionContentBase[] { + return Array.from(this.fContents()?.values() ?? []); } private _getPathResult(source: IPoint, target: IPoint) { - const radius = this.fRadius > 0 ? this.fRadius : 0; - const offset = this.fOffset > 0 ? this.fOffset : 1; - - return this._connectionFactory.handle({ + return this._fConnectionBuilder.handle({ type: this.fType, payload: { source, sourceSide: this._sourceSide, target, targetSide: this._targetSide, - radius, - offset, + radius: this.fRadius, + offset: this.fOffset, + waypoints: this.fWaypoints()?.waypoints() || [], }, }); } - private _createTransformString(position: IPoint, rotate: number = 0): string { - return `position: absolute; pointer-events: all; transform: translate(-50%, -50%) rotate(${rotate}deg); left: ${position.x}px; top: ${position.y}px`; - } - public override markChildrenAsSelected(): void { this.fPath().select(); } @@ -199,7 +177,6 @@ export abstract class FConnectionBase this.fGradient().redraw(this.line); this.fDragHandleEnd().redraw(this._penultimatePoint, this.line.point2); this.fDragHandleStart()?.redraw(this._secondPoint, this.line.point1); - this.fTextComponent()?.redraw(this.line); } /** diff --git a/projects/f-flow/src/f-connection-v2/components/index.ts b/projects/f-flow/src/f-connection-v2/components/index.ts new file mode 100644 index 00000000..15775418 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/components/index.ts @@ -0,0 +1,8 @@ +export * from './connection-content'; +export * from './connection-waypoints'; +export * from './connection-marker'; +export * from './connection-gradient'; +export * from './connection-path'; +export * from './connection-selection'; +export * from './connection-drag-handles'; +export * from './f-connection-base'; diff --git a/projects/f-flow/src/f-connectors/e-f-connectable-side.ts b/projects/f-flow/src/f-connection-v2/enums/e-f-connectable-side.ts similarity index 100% rename from projects/f-flow/src/f-connectors/e-f-connectable-side.ts rename to projects/f-flow/src/f-connection-v2/enums/e-f-connectable-side.ts diff --git a/projects/f-flow/src/f-connection/common/e-f-connection-type.ts b/projects/f-flow/src/f-connection-v2/enums/e-f-connection-type.ts similarity index 100% rename from projects/f-flow/src/f-connection/common/e-f-connection-type.ts rename to projects/f-flow/src/f-connection-v2/enums/e-f-connection-type.ts diff --git a/projects/f-flow/src/f-connection-v2/enums/index.ts b/projects/f-flow/src/f-connection-v2/enums/index.ts new file mode 100644 index 00000000..0a55cad1 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/enums/index.ts @@ -0,0 +1,3 @@ +export * from './e-f-connectable-side'; + +export * from './e-f-connection-type'; diff --git a/projects/f-flow/src/f-connection-v2/index.ts b/projects/f-flow/src/f-connection-v2/index.ts new file mode 100644 index 00000000..d5184f22 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/index.ts @@ -0,0 +1,4 @@ +export * from './components'; +export * from './enums'; +export * from './models'; +export * from './utils'; diff --git a/projects/f-flow/src/f-connection-v2/models/f-connection-components-parent.ts b/projects/f-flow/src/f-connection-v2/models/f-connection-components-parent.ts new file mode 100644 index 00000000..3163764f --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/models/f-connection-components-parent.ts @@ -0,0 +1,30 @@ +import { InjectionToken, Signal } from '@angular/core'; +import { EFConnectableSide } from '../enums'; +import { EFConnectionBehavior, EFConnectionConnectableSide } from '../utils'; + +export const F_CONNECTION_COMPONENTS_PARENT = new InjectionToken( + 'F_CONNECTION_COMPONENTS_PARENT', +); + +export abstract class FConnectionComponentsParent { + public abstract fId: Signal; + + public abstract fOutputId: Signal; + + public abstract fInputId: Signal; + + public abstract fStartColor: Signal; + + public abstract fEndColor: Signal; + + public abstract fBehavior: EFConnectionBehavior; + + public abstract _applyResolvedSidesToConnection( + sourceSide: EFConnectableSide, + targetSide: EFConnectableSide, + ): void; + + public abstract fInputSide: Signal; + + public abstract fOutputSide: Signal; +} diff --git a/projects/f-flow/src/f-connection-v2/models/f-connection-waypoints-changed-event.ts b/projects/f-flow/src/f-connection-v2/models/f-connection-waypoints-changed-event.ts new file mode 100644 index 00000000..f5d168eb --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/models/f-connection-waypoints-changed-event.ts @@ -0,0 +1,8 @@ +import { IPoint } from '@foblex/2d'; + +export class FConnectionWaypointsChangedEvent { + constructor( + public readonly connectionId: string, + public readonly waypoints: IPoint[], + ) {} +} diff --git a/projects/f-flow/src/f-connection-v2/models/index.ts b/projects/f-flow/src/f-connection-v2/models/index.ts new file mode 100644 index 00000000..8afd3803 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/models/index.ts @@ -0,0 +1,2 @@ +export * from './f-connection-components-parent'; +export * from './f-connection-waypoints-changed-event'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/connection-behaviour-builder-request.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/connection-behaviour-builder-request.ts new file mode 100644 index 00000000..1a90a973 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/connection-behaviour-builder-request.ts @@ -0,0 +1,13 @@ +import { IRoundedRect } from '@foblex/2d'; +import { EFConnectableSide } from '../../enums'; +import { FConnectionComponentsParent } from '../../models'; + +export class ConnectionBehaviourBuilderRequest { + constructor( + public readonly sourceRect: IRoundedRect, + public readonly targetRect: IRoundedRect, + public readonly connection: FConnectionComponentsParent, + public readonly sourceConnectableSide: EFConnectableSide, + public readonly targetConnectableSide: EFConnectableSide, + ) {} +} diff --git a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/calculate-connection-line-by-behavior.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/connection-behaviour-builder.ts similarity index 85% rename from projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/calculate-connection-line-by-behavior.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-behaviour/connection-behaviour-builder.ts index 18232662..845b4e08 100644 --- a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/calculate-connection-line-by-behavior.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/connection-behaviour-builder.ts @@ -1,13 +1,10 @@ -import { CalculateConnectionLineByBehaviorRequest } from './calculate-connection-line-by-behavior-request'; -import { Injectable } from '@angular/core'; -import { EFConnectionBehavior, EFConnectionConnectableSide } from '../../../f-connection'; -import { FExecutionRegister, IExecution } from '@foblex/mediator'; +import { ConnectionBehaviourBuilderRequest } from './connection-behaviour-builder-request'; import { ILine } from '@foblex/2d'; -import { floatingBehavior } from './utils/floating-behavior'; -import { fixedCenterBehavior } from './utils/fixed-center-behavior'; -import { fixedOutboundBehavior } from './utils/fixed-outbound-behavior'; -import { EFConnectableSide } from '../../../f-connectors'; -import { CalculateBehaviorRequest } from './models/calculate-behavior-request'; +import { fixedCenterBehavior, fixedOutboundBehavior, floatingBehavior } from './utils'; +import { EFConnectionBehavior, EFConnectionConnectableSide } from './enums'; +import { EFConnectableSide } from '../../enums'; +import { ICalculateBehaviorRequest } from './models'; +import { Injectable } from '@angular/core'; /** * Small epsilon to treat near-zero differences as negligible. @@ -17,7 +14,7 @@ const EPSILON = 0.5; /** * Function signature for behavior-specific connection line handlers. */ -type BehaviorHandler = (args: CalculateBehaviorRequest) => ILine; +type BehaviorHandler = (args: ICalculateBehaviorRequest) => ILine; /** * Registry of all connection behavior handlers. @@ -44,17 +41,14 @@ interface IDirectionalVectors { * then delegates to a registered behavior handler. */ @Injectable() -@FExecutionRegister(CalculateConnectionLineByBehaviorRequest) -export class CalculateConnectionLineByBehavior - implements IExecution -{ +export class ConnectionBehaviourBuilder { /** * Main execution entry point. * * @param request The request containing source, target, and connection details. * @returns A calculated connection line (ILine). */ - public handle(request: CalculateConnectionLineByBehaviorRequest): ILine { + public handle(request: ConnectionBehaviourBuilderRequest): ILine { const vectors = this._calculateDirectionalVectors( request.sourceRect.gravityCenter.x, request.sourceRect.gravityCenter.y, @@ -101,7 +95,7 @@ export class CalculateConnectionLineByBehavior * Determines the side for the source element. */ private _determineSourceSide( - request: CalculateConnectionLineByBehaviorRequest, + request: ConnectionBehaviourBuilderRequest, vectors: IDirectionalVectors, ): EFConnectableSide { return this._resolveConnectableSide( @@ -116,7 +110,7 @@ export class CalculateConnectionLineByBehavior * Determines the side for the target element. */ private _determineTargetSide( - request: CalculateConnectionLineByBehaviorRequest, + request: ConnectionBehaviourBuilderRequest, vectors: IDirectionalVectors, ): EFConnectableSide { return this._resolveConnectableSide( diff --git a/projects/f-flow/src/f-connection/common/e-f-connection-behavior.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/enums/e-f-connection-behavior.ts similarity index 99% rename from projects/f-flow/src/f-connection/common/e-f-connection-behavior.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-behaviour/enums/e-f-connection-behavior.ts index 65256d46..3dd443b0 100644 --- a/projects/f-flow/src/f-connection/common/e-f-connection-behavior.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/enums/e-f-connection-behavior.ts @@ -1,5 +1,4 @@ export enum EFConnectionBehavior { - FIXED = 'fixed', FIXED_CENTER = 'fixed_center', diff --git a/projects/f-flow/src/f-connection/common/e-f-connection-connectable-side.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/enums/e-f-connection-connectable-side.ts similarity index 100% rename from projects/f-flow/src/f-connection/common/e-f-connection-connectable-side.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-behaviour/enums/e-f-connection-connectable-side.ts diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/enums/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/enums/index.ts new file mode 100644 index 00000000..152dff33 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/enums/index.ts @@ -0,0 +1,2 @@ +export * from './e-f-connection-behavior'; +export * from './e-f-connection-connectable-side'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/index.ts new file mode 100644 index 00000000..6af85c77 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/index.ts @@ -0,0 +1,9 @@ +export * from './enums'; + +export * from './models'; + +export * from './utils'; + +export * from './connection-behaviour-builder'; + +export * from './connection-behaviour-builder-request'; diff --git a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/models/calculate-behavior-request.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/models/i-calculate-behavior-request.ts similarity index 64% rename from projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/models/calculate-behavior-request.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-behaviour/models/i-calculate-behavior-request.ts index 2b12d3b3..626c3ab8 100644 --- a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/models/calculate-behavior-request.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/models/i-calculate-behavior-request.ts @@ -1,7 +1,7 @@ import { IRoundedRect } from '@foblex/2d'; -import { EFConnectableSide } from '../../../../f-connectors'; +import { EFConnectableSide } from '../../../enums'; -export interface CalculateBehaviorRequest { +export interface ICalculateBehaviorRequest { sourceRect: IRoundedRect; targetRect: IRoundedRect; sourceConnectableSide: EFConnectableSide; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/models/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/models/index.ts new file mode 100644 index 00000000..2a4f85fe --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/models/index.ts @@ -0,0 +1 @@ +export * from './i-calculate-behavior-request'; diff --git a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/utils/fixed-center-behavior.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/fixed-center-behavior.ts similarity index 75% rename from projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/utils/fixed-center-behavior.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/fixed-center-behavior.ts index 14bbc853..5d232931 100644 --- a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/utils/fixed-center-behavior.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/fixed-center-behavior.ts @@ -1,12 +1,12 @@ import { ILine } from '@foblex/2d'; -import { CalculateBehaviorRequest } from '../models/calculate-behavior-request'; +import { ICalculateBehaviorRequest } from '../models'; /** * Fixed center behavior calculates the connection line * It constructs a line between the gravity centers of the connector rectangles * @param payload */ -export function fixedCenterBehavior({ sourceRect, targetRect }: CalculateBehaviorRequest): ILine { +export function fixedCenterBehavior({ sourceRect, targetRect }: ICalculateBehaviorRequest): ILine { return { point1: sourceRect.gravityCenter, point2: targetRect.gravityCenter, diff --git a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/utils/fixed-outbound-behavior.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/fixed-outbound-behavior.ts similarity index 87% rename from projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/utils/fixed-outbound-behavior.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/fixed-outbound-behavior.ts index ae7a1b87..386428f3 100644 --- a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/utils/fixed-outbound-behavior.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/fixed-outbound-behavior.ts @@ -1,6 +1,6 @@ import { ILine, IPoint, IRect } from '@foblex/2d'; -import { EFConnectableSide } from '../../../../f-connectors'; -import { CalculateBehaviorRequest } from '../models/calculate-behavior-request'; +import { ICalculateBehaviorRequest } from '../models'; +import { EFConnectableSide } from '../../../enums'; /** * Fixed outbound behavior calculates the connection line @@ -12,7 +12,7 @@ export function fixedOutboundBehavior({ sourceConnectableSide, targetRect, targetConnectableSide, -}: CalculateBehaviorRequest): ILine { +}: ICalculateBehaviorRequest): ILine { return { point1: _getPosition( sourceRect, diff --git a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/utils/floating-behavior.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/floating-behavior.ts similarity index 87% rename from projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/utils/floating-behavior.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/floating-behavior.ts index fe3caabd..fad3f064 100644 --- a/projects/f-flow/src/domain/f-connection/calculate-connection-line-by-behavior/utils/floating-behavior.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/floating-behavior.ts @@ -1,12 +1,12 @@ import { GetIntersections, ILine, IPoint, IRoundedRect } from '@foblex/2d'; -import { CalculateBehaviorRequest } from '../models/calculate-behavior-request'; +import { ICalculateBehaviorRequest } from '../models'; /** * Floating behavior calculates the connection line * It constructs a line between the intersections of the connectors rectangles and line from the centers of the connector rectangles * @param payload */ -export function floatingBehavior({ sourceRect, targetRect }: CalculateBehaviorRequest): ILine { +export function floatingBehavior({ sourceRect, targetRect }: ICalculateBehaviorRequest): ILine { return _getIntersectionsLine( _fromRoundedRectIntersections(sourceRect, targetRect), _toRoundedRectIntersections(sourceRect, targetRect), diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/index.ts new file mode 100644 index 00000000..1647f6bc --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-behaviour/utils/index.ts @@ -0,0 +1,3 @@ +export * from './fixed-center-behavior'; +export * from './fixed-outbound-behavior'; +export * from './floating-behavior'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/adaptive-curve/calculate-adaptive-curve-data.spec.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/adaptive-curve/calculate-adaptive-curve-data.spec.ts new file mode 100644 index 00000000..65a61478 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/adaptive-curve/calculate-adaptive-curve-data.spec.ts @@ -0,0 +1,158 @@ +import { + CalculateAdaptiveCurveData, + EFConnectableSide, + IFConnectionBuilderRequest, + IFConnectionBuilderResponse, +} from '@foblex/flow'; +import { IPoint } from '@foblex/2d'; + +describe('CalculateAdaptiveCurveData', () => { + let builder: CalculateAdaptiveCurveData; + + const SAMPLES = 12; + const ONE_SEG_POINTS = SAMPLES + 1; // 13 + + beforeEach(() => { + builder = new CalculateAdaptiveCurveData(); + }); + + function expectFinitePoint(p: IPoint) { + expect(p).toBeDefined(); + expect(Number.isFinite(p.x)).toBe(true); + expect(Number.isFinite(p.y)).toBe(true); + } + + function expectPointsArray(resp: IFConnectionBuilderResponse, expectedLen: number) { + expect(resp.points).toBeDefined(); + expect(Array.isArray(resp.points)).toBe(true); + expect(resp.points.length).toBe(expectedLen); + + for (let i = 0; i < resp.points.length; i++) { + expectFinitePoint(resp.points[i]); + } + } + + function expectCubicPath(resp: IFConnectionBuilderResponse, start: { x: number; y: number }) { + expect(resp.path).toBeDefined(); + expect(resp.path.startsWith(`M ${start.x} ${start.y}`)).toBe(true); + expect(resp.path).toContain(' C '); + } + + it('builds a cubic path for a horizontal connection (RIGHT -> LEFT)', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 0 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.LEFT, + offset: 20, + radius: 0, + waypoints: [], + }; + + const res = builder.handle(request); + + expectCubicPath(res, request.source); + expectPointsArray(res, ONE_SEG_POINTS); + + expectFinitePoint(res.secondPoint); + expectFinitePoint(res.penultimatePoint); + + expect(res.candidates).toBeDefined(); + expect(res.candidates.length).toBe(1); + }); + + it('builds a cubic path for a vertical connection (BOTTOM -> TOP)', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 0, y: 100 }, + sourceSide: EFConnectableSide.BOTTOM, + targetSide: EFConnectableSide.TOP, + offset: 20, + radius: 0, + waypoints: [], + }; + + const res = builder.handle(request); + + expectCubicPath(res, request.source); + expectPointsArray(res, ONE_SEG_POINTS); + + expectFinitePoint(res.secondPoint); + expectFinitePoint(res.penultimatePoint); + + expect(res.candidates).toBeDefined(); + expect(res.candidates.length).toBe(1); + }); + + it('builds a cubic path for a diagonal connection', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 100 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.BOTTOM, + offset: 20, + radius: 0, + waypoints: [], + }; + + const res = builder.handle(request); + + expectCubicPath(res, request.source); + expectPointsArray(res, ONE_SEG_POINTS); + + expectFinitePoint(res.secondPoint); + expectFinitePoint(res.penultimatePoint); + + expect(res.candidates).toBeDefined(); + expect(res.candidates.length).toBe(1); + }); + + it('builds multi-segment cubic path when pivots exist', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 0 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.LEFT, + offset: 20, + radius: 0, + waypoints: [{ x: 50, y: 50 }], + }; + + const res = builder.handle(request); + + expect(res.path).toBeDefined(); + expect(res.path.startsWith('M 0 0')).toBe(true); + + expect((res.path.match(/\sC\s/g) ?? []).length).toBe(2); + + expect(res.points).toBeDefined(); + expect(res.points.length).toBeGreaterThan(ONE_SEG_POINTS); + + expect(res.candidates).toBeDefined(); + expect(res.candidates.length).toBe(2); + }); + + it('ensures handles are not degenerate for typical input', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 10, y: 20 }, + target: { x: 110, y: 120 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.TOP, + offset: 16, + radius: 0, + waypoints: [], + }; + + const res = builder.handle(request); + + expectFinitePoint(res.secondPoint); + expectFinitePoint(res.penultimatePoint); + + expect( + !(res.secondPoint.x === request.source.x && res.secondPoint.y === request.source.y), + ).toBe(true); + expect( + !(res.penultimatePoint.x === request.target.x && res.penultimatePoint.y === request.target.y), + ).toBe(true); + }); +}); diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/adaptive-curve/calculate-adaptive-curve-data.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/adaptive-curve/calculate-adaptive-curve-data.ts new file mode 100644 index 00000000..f55cf7a0 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/adaptive-curve/calculate-adaptive-curve-data.ts @@ -0,0 +1,126 @@ +import { IPoint } from '@foblex/2d'; +import { + buildConnectionAnchors, + calculateCurveCandidates, + createMultiCubicPath, + ICubicSegment, + sampleMultiCubicUniform, +} from '../utils'; +import { + IFConnectionBuilder, + IFConnectionBuilderRequest, + IFConnectionBuilderResponse, +} from '../../models'; +import { EFConnectableSide } from '../../../../enums'; + +export class CalculateAdaptiveCurveData implements IFConnectionBuilder { + private static _dir(side: EFConnectableSide): IPoint { + switch (side) { + case EFConnectableSide.LEFT: + return { x: -1, y: 0 }; + case EFConnectableSide.RIGHT: + return { x: 1, y: 0 }; + case EFConnectableSide.TOP: + return { x: 0, y: -1 }; + case EFConnectableSide.BOTTOM: + return { x: 0, y: 1 }; + case EFConnectableSide.AUTO: + return { x: 0, y: 0 }; + } + + return { x: 0, y: 0 }; + } + + private static _isHorizontal(side: EFConnectableSide): boolean { + return side === EFConnectableSide.LEFT || side === EFConnectableSide.RIGHT; + } + + private static _handleLength( + p0: IPoint, + p3: IPoint, + side: EFConnectableSide, + offset: number, + ): number { + const dx = Math.abs(p3.x - p0.x); + const dy = Math.abs(p3.y - p0.y); + const d = Math.hypot(dx, dy); + + const along = + side === EFConnectableSide.AUTO ? Math.max(dx, dy) : this._isHorizontal(side) ? dx : dy; + + const MIN = Math.max(8, offset); + const MAX = offset + 0.5 * d; + const len = offset * 1.05 + along * 0.3; + + return Math.min(MAX, Math.max(MIN, len)); + } + + private static _softControl( + side: EFConnectableSide, + source: IPoint, + target: IPoint, + handle: number, + ): IPoint { + const v = this._dir(side); + + const dx = target.x - source.x; + const dy = target.y - source.y; + const dist = Math.hypot(dx, dy) || 1; + const tx = dx / dist; + const ty = dy / dist; + + if (side === EFConnectableSide.AUTO) { + return { x: source.x + tx * handle, y: source.y + ty * handle }; + } + + const dot = v.x * tx + v.y * ty; + + const baseBlend = 0.12; + const extra = Math.max(0, -dot) * 0.08; + const blend = Math.min(0.2, baseBlend + extra); + + const dirX = v.x * (1 - blend) + tx * blend; + const dirY = v.y * (1 - blend) + ty * blend; + const len = Math.hypot(dirX, dirY) || 1; + + return { x: source.x + (dirX / len) * handle, y: source.y + (dirY / len) * handle }; + } + + public handle({ + source, + sourceSide, + target, + targetSide, + offset, + waypoints, + }: IFConnectionBuilderRequest): IFConnectionBuilderResponse { + const clampedOffset = Math.max(0, offset ?? 0); + + const anchors = buildConnectionAnchors(source, target, waypoints); + + const segments: ICubicSegment[] = []; + + for (let i = 0; i < anchors.length - 1; i++) { + const a = anchors[i]; + const b = anchors[i + 1]; + + const h0 = CalculateAdaptiveCurveData._handleLength(a, b, sourceSide, clampedOffset); + const h3 = CalculateAdaptiveCurveData._handleLength(b, a, targetSide, clampedOffset); + + const c1 = CalculateAdaptiveCurveData._softControl(sourceSide, a, b, h0); + const c2 = CalculateAdaptiveCurveData._softControl(targetSide, b, a, h3); + + segments.push({ p0: a, c1, c2, p3: b, chainIndex: i }); + } + + const points = sampleMultiCubicUniform(segments, 12); + + return { + path: createMultiCubicPath(segments), + secondPoint: segments[0]?.c1 ?? source, + penultimatePoint: segments[segments.length - 1]?.c2 ?? target, + points, + candidates: calculateCurveCandidates(segments), + }; + } +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/adaptive-curve/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/adaptive-curve/index.ts new file mode 100644 index 00000000..d3e5ae52 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/adaptive-curve/index.ts @@ -0,0 +1 @@ +export * from './calculate-adaptive-curve-data'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/bezier-curve/calculate-bezier-curve-data.spec.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/bezier-curve/calculate-bezier-curve-data.spec.ts new file mode 100644 index 00000000..3b1ca11f --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/bezier-curve/calculate-bezier-curve-data.spec.ts @@ -0,0 +1,111 @@ +import { + CalculateBezierCurveData, + EFConnectableSide, + IFConnectionBuilderRequest, + IFConnectionBuilderResponse, +} from '@foblex/flow'; + +describe('CalculateBezierCurveData', () => { + let builder: CalculateBezierCurveData; + + beforeEach(() => { + builder = new CalculateBezierCurveData(); + }); + + it('builds cubic path for a simple horizontal connection (no pivots)', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 0 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.LEFT, + radius: 0, + offset: 20, + waypoints: [], + }; + + const res: IFConnectionBuilderResponse = builder.handle(request); + + expect(res.path).toBe('M 0 0 C 100 0, 0 0, 100.0002 0.0002'); + + expect(res.secondPoint).toEqual({ x: 100, y: 0 }); + expect(res.penultimatePoint).toEqual({ x: 0, y: 0 }); + + expect(res.points).toBeDefined(); + expect(res.points.length).toBeGreaterThan(0); + + expect(res.candidates).toBeDefined(); + expect(res.candidates.length).toBe(1); + }); + + it('builds cubic path for a simple vertical connection (no pivots)', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 0, y: 100 }, + sourceSide: EFConnectableSide.BOTTOM, + targetSide: EFConnectableSide.TOP, + radius: 0, + offset: 20, + waypoints: [], + }; + + const res = builder.handle(request); + + expect(res.path).toBe('M 0 0 C 0 100, 0 0, 0.0002 100.0002'); + + expect(res.secondPoint).toEqual({ x: 0, y: 100 }); + expect(res.penultimatePoint).toEqual({ x: 0, y: 0 }); + + expect(res.candidates).toBeDefined(); + expect(res.candidates.length).toBe(1); + }); + + it('builds multi-segment cubic path when pivots are present', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 0 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.LEFT, + radius: 0, + offset: 20, + waypoints: [ + { x: 50, y: 50 }, // pivot 1 + ], + }; + + const res = builder.handle(request); + + expect(res.path).toContain('M 0 0'); + + expect((res.path.match(/\sC\s/g) ?? []).length).toBe(2); + + expect(res.points).toBeDefined(); + expect(res.points.length).toBeGreaterThan(0); + + // candidates: typically 1 per segment + expect(res.candidates).toBeDefined(); + expect(res.candidates.length).toBe(2); + }); + + it('handles diagonal connection and returns stable endpoints/control points', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 100 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.BOTTOM, + radius: 0, + offset: 20, + waypoints: [], + }; + + const res = builder.handle(request); + + expect(res.path).toContain('M 0 0'); + expect(res.path).toContain(' C '); + + expect(res.secondPoint).toBeDefined(); + expect(res.penultimatePoint).toBeDefined(); + + expect(res.candidates).toBeDefined(); + expect(res.candidates.length).toBe(1); + }); +}); diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/bezier-curve/calculate-bezier-curve-data.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/bezier-curve/calculate-bezier-curve-data.ts new file mode 100644 index 00000000..9797ccc6 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/bezier-curve/calculate-bezier-curve-data.ts @@ -0,0 +1,83 @@ +import { IPoint } from '@foblex/2d'; +import { + buildConnectionAnchors, + calculateCurveCandidates, + createMultiCubicPath, + ICubicSegment, + sampleMultiCubicUniform, +} from '../utils'; +import { + IFConnectionBuilder, + IFConnectionBuilderRequest, + IFConnectionBuilderResponse, +} from '../../models'; +import { EFConnectableSide } from '../../../../enums'; + +export class CalculateBezierCurveData implements IFConnectionBuilder { + public handle({ + source, + sourceSide, + target, + targetSide, + offset, + waypoints, + }: IFConnectionBuilderRequest): IFConnectionBuilderResponse { + const anchors = buildConnectionAnchors(source, target, waypoints); + + const segments: ICubicSegment[] = []; + + for (let i = 0; i < anchors.length - 1; i++) { + const a = anchors[i]; + const b = anchors[i + 1]; + + const c1 = getAnglePoint(sourceSide, a, b, offset ?? 0); + const c2 = getAnglePoint(targetSide, b, a, offset ?? 0); + + segments.push({ p0: a, c1, c2, p3: b, chainIndex: i }); + } + + const points = sampleMultiCubicUniform(segments, 12); + + return { + path: createMultiCubicPath(segments), + secondPoint: segments[0]?.c1 ?? source, + penultimatePoint: segments[segments.length - 1]?.c2 ?? target, + points, + candidates: calculateCurveCandidates(segments), + }; + } +} + +function getAnglePoint( + side: EFConnectableSide, + source: IPoint, + target: IPoint, + offset: number, +): IPoint { + const result: IPoint = { x: source.x, y: source.y }; + + switch (side) { + case EFConnectableSide.LEFT: + result.x -= getConnectorOffset(source.x - target.x, offset); + break; + case EFConnectableSide.RIGHT: + result.x += getConnectorOffset(target.x - source.x, offset); + break; + case EFConnectableSide.TOP: + result.y -= getConnectorOffset(source.y - target.y, offset); + break; + case EFConnectableSide.BOTTOM: + result.y += getConnectorOffset(target.y - source.y, offset); + break; + case EFConnectableSide.AUTO: + break; + } + + return result; +} + +function getConnectorOffset(distance: number, offset: number): number { + if (distance >= offset) return distance; + + return offset * Math.sqrt(offset - distance); +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/bezier-curve/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/bezier-curve/index.ts new file mode 100644 index 00000000..eaaa0de4 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/bezier-curve/index.ts @@ -0,0 +1 @@ +export * from './calculate-bezier-curve-data'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/index.ts new file mode 100644 index 00000000..e247ccc3 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/index.ts @@ -0,0 +1,5 @@ +export * from './adaptive-curve'; +export * from './bezier-curve'; +export * from './segment-line'; +export * from './straight-line'; +export * from './utils'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/build-corner-mid-points-and-apply-offsets.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/build-corner-mid-points-and-apply-offsets.ts new file mode 100644 index 00000000..c0f9325f --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/build-corner-mid-points-and-apply-offsets.ts @@ -0,0 +1,142 @@ +import { IPoint } from '@foblex/2d'; +import { EFConnectableSide } from '../../../../enums'; + +export function buildCornerMidPointsAndApplyOffsets(params: { + axis: 'x' | 'y'; + source: IPoint; + target: IPoint; + + sourceSide: EFConnectableSide; + targetSide: EFConnectableSide; + + sourceGap: IPoint; + targetGap: IPoint; + + sourceDir: IPoint; + targetDir: IPoint; + + currentDir: number; + offset: number; + + sourceGapOffset: IPoint; + targetGapOffset: IPoint; +}): IPoint[] { + const { + axis, + source, + target, + sourceSide, + targetSide, + sourceGap, + targetGap, + sourceDir, + targetDir, + currentDir, + offset, + sourceGapOffset, + targetGapOffset, + } = params; + + const corners = buildCornerPoints(sourceGap, targetGap); + + let midPoints = pickCornerByDirection(axis, sourceDir, currentDir, corners); + + if (sourceSide === targetSide) { + applySameSideGapOffsetFix( + axis, + source, + target, + offset, + sourceDir, + currentDir, + sourceGap, + targetGap, + sourceGapOffset, + targetGapOffset, + ); + } else { + midPoints = maybeFlipCornerForDifferentSides( + axis, + sourceDir, + targetDir, + sourceGap, + targetGap, + corners, + midPoints, + ); + } + + return midPoints; +} + +function buildCornerPoints( + sourceGap: IPoint, + targetGap: IPoint, +): { sourceTarget: IPoint[]; targetSource: IPoint[] } { + return { + sourceTarget: [{ x: sourceGap.x, y: targetGap.y }], + targetSource: [{ x: targetGap.x, y: sourceGap.y }], + }; +} + +function pickCornerByDirection( + axis: 'x' | 'y', + sourceDir: IPoint, + currentDir: number, + corners: { sourceTarget: IPoint[]; targetSource: IPoint[] }, +): IPoint[] { + if (axis === 'x') { + return sourceDir.x === currentDir ? corners.targetSource : corners.sourceTarget; + } + + return sourceDir.y === currentDir ? corners.sourceTarget : corners.targetSource; +} + +function applySameSideGapOffsetFix( + axis: 'x' | 'y', + source: IPoint, + target: IPoint, + offset: number, + sourceDir: IPoint, + currentDir: number, + sourceGap: IPoint, + targetGap: IPoint, + sourceGapOffset: IPoint, + targetGapOffset: IPoint, +): void { + const diff = Math.abs(source[axis] - target[axis]); + if (diff > offset) return; + + const gapOffset = Math.min(offset - 1, offset - diff); + if (gapOffset <= 0) return; + + if (sourceDir[axis] === currentDir) { + sourceGapOffset[axis] = (sourceGap[axis] > source[axis] ? -1 : 1) * gapOffset; + } else { + targetGapOffset[axis] = (targetGap[axis] > target[axis] ? -1 : 1) * gapOffset; + } +} + +function maybeFlipCornerForDifferentSides( + axis: 'x' | 'y', + sourceDir: IPoint, + targetDir: IPoint, + sourceGap: IPoint, + targetGap: IPoint, + corners: { sourceTarget: IPoint[]; targetSource: IPoint[] }, + currentPicked: IPoint[], +): IPoint[] { + const opp = axis === 'x' ? 'y' : 'x'; + + const isSameDir = sourceDir[axis] === targetDir[opp]; + const sourceGt = sourceGap[opp] > targetGap[opp]; + const sourceLt = sourceGap[opp] < targetGap[opp]; + + const flip = + (sourceDir[axis] === 1 && ((!isSameDir && sourceGt) || (isSameDir && sourceLt))) || + (sourceDir[axis] !== 1 && ((!isSameDir && sourceLt) || (isSameDir && sourceGt))); + + if (!flip) return currentPicked; + + return axis === 'x' ? corners.sourceTarget : corners.targetSource; +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/calculate-segment-line-data.spec.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/calculate-segment-line-data.spec.ts new file mode 100644 index 00000000..2c4d9b4f --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/calculate-segment-line-data.spec.ts @@ -0,0 +1,145 @@ +import { IFConnectionBuilderRequest, IFConnectionBuilderResponse } from '../../models'; +import { CalculateSegmentLineData, EFConnectableSide } from '@foblex/flow'; + +describe('CalculateSegmentLineData', () => { + let builder: CalculateSegmentLineData; + + beforeEach(() => { + builder = new CalculateSegmentLineData(); + }); + + function expectCommon(response: IFConnectionBuilderResponse) { + expect(response.path).toBeTruthy(); + expect(response.points).toBeTruthy(); + expect(Array.isArray(response.points)).toBe(true); + expect(response.points.length).toBeGreaterThanOrEqual(2); + + const pts = response.points; + expect(response.secondPoint).toEqual(pts[1]); + expect(response.penultimatePoint).toEqual(pts[pts.length - 2]); + } + + it('builds a path for a horizontal connection (RIGHT -> LEFT)', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 0 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.LEFT, + radius: 10, + offset: 20, + waypoints: [], + }; + + const response = builder.handle(request); + + expectCommon(response); + + expect(response.points[0]).toEqual(request.source); + expect(response.points[response.points.length - 1]).toEqual(request.target); + + expect(response.candidates?.length ?? 0).toBeGreaterThan(0); + }); + + it('builds a path for a vertical connection (BOTTOM -> TOP)', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 0, y: 100 }, + sourceSide: EFConnectableSide.BOTTOM, + targetSide: EFConnectableSide.TOP, + radius: 10, + offset: 20, + waypoints: [], + }; + + const response = builder.handle(request); + + expectCommon(response); + + expect(response.points[0]).toEqual(request.source); + expect(response.points[response.points.length - 1]).toEqual(request.target); + + expect(response.candidates?.length ?? 0).toBeGreaterThan(0); + }); + + it('builds a path for a diagonal connection (RIGHT -> BOTTOM)', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 100 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.BOTTOM, + radius: 10, + offset: 20, + waypoints: [], + }; + + const response = builder.handle(request); + + expectCommon(response); + + expect(response.points.length).toBeGreaterThanOrEqual(2); + expect(response.candidates?.length ?? 0).toBeGreaterThan(0); + }); + + it('respects offset by introducing gaps near ports', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 50, y: 50 }, + sourceSide: EFConnectableSide.BOTTOM, + targetSide: EFConnectableSide.LEFT, + radius: 10, + offset: 30, + waypoints: [], + }; + + const response = builder.handle(request); + + expectCommon(response); + + const p1 = response.points[1]; + expect(p1.x !== request.source.x || p1.y !== request.source.y).toBe(true); + }); + + it('produces quadratic bends (Q) when radius > 0 and there is at least one corner', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 100 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.LEFT, + radius: 10, + offset: 20, + waypoints: [], + }; + + const response = builder.handle(request); + + expectCommon(response); + + const hasQ = response.path.includes(' Q ') || response.path.includes('Q '); + if (!hasQ) { + const pts = response.points; + const allSameX = pts.every((p) => p.x === pts[0].x); + const allSameY = pts.every((p) => p.y === pts[0].y); + expect(allSameX || allSameY).toBe(true); + } + }); + + it('supports pivots (anchors chain): path starts at source and ends at target', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 200, y: 0 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.LEFT, + radius: 10, + offset: 20, + waypoints: [{ x: 100, y: 100 }], // один pivot + }; + + const response = builder.handle(request); + + expectCommon(response); + + expect(response.points[0]).toEqual(request.source); + expect(response.points[response.points.length - 1]).toEqual(request.target); + expect(response.candidates?.length ?? 0).toBeGreaterThan(0); + }); +}); diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/calculate-segment-line-data.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/calculate-segment-line-data.ts new file mode 100644 index 00000000..174537a8 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/calculate-segment-line-data.ts @@ -0,0 +1,151 @@ +import { IPoint, PointExtensions } from '@foblex/2d'; +import { + buildConnectionAnchors, + calculateCenterBetweenPoints, + calculatePolylineCandidates, + mergePointChains, + normalizePolyline, +} from '../utils'; +import { createSegmentLinePath } from './create-segment-line-path'; +import { + IFConnectionBuilder, + IFConnectionBuilderRequest, + IFConnectionBuilderResponse, +} from '../../models'; +import { EFConnectableSide } from '../../../../enums'; +import { buildCornerMidPointsAndApplyOffsets } from './build-corner-mid-points-and-apply-offsets'; + +const CONNECTOR_SIDE_POINT: Record = { + [EFConnectableSide.LEFT]: PointExtensions.initialize(-1, 0), + + [EFConnectableSide.RIGHT]: PointExtensions.initialize(1, 0), + + [EFConnectableSide.TOP]: PointExtensions.initialize(0, -1), + + [EFConnectableSide.BOTTOM]: PointExtensions.initialize(0, 1), + + [EFConnectableSide.AUTO]: PointExtensions.initialize(0, 0), +}; + +export class CalculateSegmentLineData implements IFConnectionBuilder { + public handle({ + source, + sourceSide, + target, + targetSide, + waypoints, + offset, + radius, + }: IFConnectionBuilderRequest): IFConnectionBuilderResponse { + const anchors = buildConnectionAnchors(source, target, waypoints); + + const chains: IPoint[][] = []; + const candidates: IPoint[] = []; + + for (let i = 0; i < anchors.length - 1; i++) { + const a = anchors[i]; + const b = anchors[i + 1]; + + const points = this._getPathPoints(a, sourceSide, b, targetSide, offset ?? 0); + + chains.push(points); + candidates.push(...calculatePolylineCandidates(points)); + } + + const polyline = normalizePolyline(mergePointChains(chains)); + + const penultimatePoint = polyline.length > 1 ? polyline[polyline.length - 2] : source; + const secondPoint = polyline.length > 1 ? polyline[1] : target; + + return { + path: createSegmentLinePath(polyline, radius ?? 0), + penultimatePoint, + secondPoint, + points: polyline, + candidates, + }; + } + + private _getPathPoints( + source: IPoint, + sourceSide: EFConnectableSide, + target: IPoint, + targetSide: EFConnectableSide, + offset: number, + ): IPoint[] { + const sourceDirection = CONNECTOR_SIDE_POINT[sourceSide]; + const targetDirection = CONNECTOR_SIDE_POINT[targetSide]; + + const sourceGap: IPoint = { + x: source.x + sourceDirection.x * offset, + y: source.y + sourceDirection.y * offset, + }; + const targetGap: IPoint = { + x: target.x + targetDirection.x * offset, + y: target.y + targetDirection.y * offset, + }; + + const direction = this._getDirection(sourceGap, sourceSide, targetGap); + const directionAccessor: 'x' | 'y' = direction.x !== 0 ? 'x' : 'y'; + const currentDirection = direction[directionAccessor]; + + let points: IPoint[] = []; + const sourceGapOffset = PointExtensions.initialize(); + const targetGapOffset = PointExtensions.initialize(); + + const centerBetweenPoints = calculateCenterBetweenPoints(source, target); + + if (sourceDirection[directionAccessor] * targetDirection[directionAccessor] === -1) { + const verticalSplit: IPoint[] = [ + { x: centerBetweenPoints.x, y: sourceGap.y }, + { x: centerBetweenPoints.x, y: targetGap.y }, + ]; + const horizontalSplit: IPoint[] = [ + { x: sourceGap.x, y: centerBetweenPoints.y }, + { x: targetGap.x, y: centerBetweenPoints.y }, + ]; + + if (sourceDirection[directionAccessor] === currentDirection) { + points = directionAccessor === 'x' ? verticalSplit : horizontalSplit; + } else { + points = directionAccessor === 'x' ? horizontalSplit : verticalSplit; + } + } else { + points = buildCornerMidPointsAndApplyOffsets({ + axis: directionAccessor, + source, + target, + sourceSide, + targetSide, + sourceGap, + targetGap, + sourceDir: sourceDirection, + targetDir: targetDirection, + currentDir: currentDirection, + offset, + sourceGapOffset, + targetGapOffset, + }); + } + + return [ + source, + { x: sourceGap.x + sourceGapOffset.x, y: sourceGap.y + sourceGapOffset.y }, + ...points, + { x: targetGap.x + targetGapOffset.x, y: targetGap.y + targetGapOffset.y }, + target, + ]; + } + + private _getDirection(source: IPoint, sourceSide: EFConnectableSide, target: IPoint): IPoint { + if (sourceSide === EFConnectableSide.LEFT || sourceSide === EFConnectableSide.RIGHT) { + return source.x < target.x + ? PointExtensions.initialize(1, 0) + : PointExtensions.initialize(-1, 0); + } + + return source.y < target.y + ? PointExtensions.initialize(0, 1) + : PointExtensions.initialize(0, -1); + } +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/create-segment-line-path.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/create-segment-line-path.ts new file mode 100644 index 00000000..5801350f --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/create-segment-line-path.ts @@ -0,0 +1,64 @@ +import { IPoint } from '@foblex/2d'; + +const EPS = 1e-6; +const END_EPS = 0.0002; +const MIN_VISIBLE = 0.75; + +export function createSegmentLinePath(points: IPoint[], borderRadius: number): string { + const n = points.length; + const parts: string[] = []; + parts.push(`M ${points[0].x} ${points[0].y}`); + + for (let i = 1; i < n - 1; i++) { + parts.push(getBend(points[i - 1], points[i], points[i + 1], borderRadius)); + } + + const last = points[n - 1]; + parts.push(`L ${last.x + END_EPS} ${last.y + END_EPS}`); + + return parts.join(' '); +} + +function getBend(a: IPoint, b: IPoint, c: IPoint, size: number): string { + const x = b.x; + const y = b.y; + + if (size <= 0) { + return `L ${x} ${y}`; + } + + const collinearX = Math.abs(a.x - x) <= EPS && Math.abs(x - c.x) <= EPS; + const collinearY = Math.abs(a.y - y) <= EPS && Math.abs(y - c.y) <= EPS; + if (collinearX || collinearY) { + return `L ${x} ${y}`; + } + + const ab = Math.hypot(x - a.x, y - a.y); + const bc = Math.hypot(c.x - x, c.y - y); + + const bendSize = Math.min(ab * 0.5, bc * 0.5, size); + + if (bendSize < MIN_VISIBLE) { + return `L ${x} ${y}`; + } + + const incomingHorizontal = Math.abs(a.y - y) <= EPS; + + if (incomingHorizontal) { + const xDir = a.x < c.x ? -1 : 1; + const yDir = a.y < c.y ? 1 : -1; + + const lx = x + bendSize * xDir; + const qy = y + bendSize * yDir; + + return `L ${lx} ${y} Q ${x} ${y} ${x} ${qy}`; + } + + const xDir = a.x < c.x ? 1 : -1; + const yDir = a.y < c.y ? -1 : 1; + + const ly = y + bendSize * yDir; + const qx = x + bendSize * xDir; + + return `L ${x} ${ly} Q ${x} ${y} ${qx} ${y}`; +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/index.ts new file mode 100644 index 00000000..9a5061bf --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/segment-line/index.ts @@ -0,0 +1,3 @@ +export * from './build-corner-mid-points-and-apply-offsets'; +export * from './calculate-segment-line-data'; +export * from './create-segment-line-path'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/straight-line/calculate-straight-line-data.spec.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/straight-line/calculate-straight-line-data.spec.ts new file mode 100644 index 00000000..59fba456 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/straight-line/calculate-straight-line-data.spec.ts @@ -0,0 +1,129 @@ +import { + CalculateStraightLineData, + EFConnectableSide, + IFConnectionBuilderRequest, + IFConnectionBuilderResponse, +} from '@foblex/flow'; + +describe('CalculateStraightLineData', () => { + let builder: CalculateStraightLineData; + + beforeEach(() => { + builder = new CalculateStraightLineData(); + }); + + it('should build a straight path for a horizontal connection', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 0 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.LEFT, + radius: 0, + offset: 0, + waypoints: [], + }; + + const response: IFConnectionBuilderResponse = builder.handle(request); + + expect(response.path).toBe('M 0 0 L 100.0002 0.0002'); + + expect(response.points).toEqual([ + { x: 0, y: 0 }, + { x: 100, y: 0 }, + ]); + + expect(response.secondPoint).toEqual({ x: 100, y: 0 }); + expect(response.penultimatePoint).toEqual({ x: 0, y: 0 }); + + expect(response.candidates.length).toBe(1); + expect(response.candidates[0]).toEqual({ x: 50, y: 0 }); + }); + + it('should build a straight path for a vertical connection', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 0, y: 100 }, + sourceSide: EFConnectableSide.BOTTOM, + targetSide: EFConnectableSide.TOP, + radius: 0, + offset: 0, + waypoints: [], + }; + + const response: IFConnectionBuilderResponse = builder.handle(request); + + expect(response.path).toBe('M 0 0 L 0.0002 100.0002'); + + expect(response.points).toEqual([ + { x: 0, y: 0 }, + { x: 0, y: 100 }, + ]); + + expect(response.secondPoint).toEqual({ x: 0, y: 100 }); + expect(response.penultimatePoint).toEqual({ x: 0, y: 0 }); + + expect(response.candidates.length).toBe(1); + expect(response.candidates[0]).toEqual({ x: 0, y: 50 }); + }); + + it('should build a straight path for a diagonal connection', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 0, y: 0 }, + target: { x: 100, y: 100 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.BOTTOM, + radius: 0, + offset: 0, + waypoints: [], + }; + + const response: IFConnectionBuilderResponse = builder.handle(request); + + expect(response.path).toBe('M 0 0 L 100.0002 100.0002'); + + expect(response.points).toEqual([ + { x: 0, y: 0 }, + { x: 100, y: 100 }, + ]); + + expect(response.secondPoint).toEqual({ x: 100, y: 100 }); + expect(response.penultimatePoint).toEqual({ x: 0, y: 0 }); + + expect(response.candidates.length).toBe(1); + expect(response.candidates[0]).toEqual({ x: 50, y: 50 }); + }); + + it('should support pivots and build multi-segment straight path', () => { + const request: IFConnectionBuilderRequest = { + source: { x: 10, y: 20 }, + target: { x: 110, y: 120 }, + sourceSide: EFConnectableSide.RIGHT, + targetSide: EFConnectableSide.BOTTOM, + radius: 0, + offset: 0, + waypoints: [ + { x: 60, y: 20 }, + { x: 60, y: 90 }, + ], + }; + + const response: IFConnectionBuilderResponse = builder.handle(request); + + expect(response.points).toEqual([ + { x: 10, y: 20 }, + { x: 60, y: 20 }, + { x: 60, y: 90 }, + { x: 110, y: 120 }, + ]); + + expect(response.path).toBe('M 10 20' + ' L 60 20' + ' L 60 90' + ' L 110.0002 120.0002'); + + expect(response.secondPoint).toEqual({ x: 60, y: 20 }); + expect(response.penultimatePoint).toEqual({ x: 60, y: 90 }); + + expect(response.candidates.length).toBe(3); + expect(response.candidates[0]).toEqual({ x: 35, y: 20 }); + expect(response.candidates[1]).toEqual({ x: 60, y: 55 }); + expect(response.candidates[2]).toEqual({ x: 85, y: 105 }); + }); +}); diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/straight-line/calculate-straight-line-data.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/straight-line/calculate-straight-line-data.ts new file mode 100644 index 00000000..8dc80398 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/straight-line/calculate-straight-line-data.ts @@ -0,0 +1,43 @@ +import { + IFConnectionBuilder, + IFConnectionBuilderRequest, + IFConnectionBuilderResponse, +} from '../../models'; +import { buildConnectionAnchors } from '../utils'; +import { IPoint } from '@foblex/2d'; + +const EPS = 0.0002; + +export class CalculateStraightLineData implements IFConnectionBuilder { + public handle(request: IFConnectionBuilderRequest): IFConnectionBuilderResponse { + const anchors = buildConnectionAnchors(request.source, request.target, request.waypoints); + + const n = anchors.length; + + const p0 = anchors[0]; + let d = `M ${p0.x} ${p0.y}`; + + const candidates: IPoint[] = new Array(n - 1); + + for (let i = 0; i < n - 1; i++) { + const a = anchors[i]; + const b = anchors[i + 1]; + + const isLast = i === n - 2; + const bx = isLast ? b.x + EPS : b.x; + const by = isLast ? b.y + EPS : b.y; + + d += ` L ${bx} ${by}`; + + candidates[i] = { x: (a.x + b.x) * 0.5, y: (a.y + b.y) * 0.5 }; + } + + return { + path: d, + candidates, + points: anchors, + secondPoint: anchors[1] ?? request.target, + penultimatePoint: anchors[n - 2] ?? request.source, + }; + } +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/straight-line/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/straight-line/index.ts new file mode 100644 index 00000000..5d212bfe --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/straight-line/index.ts @@ -0,0 +1 @@ +export * from './calculate-straight-line-data'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/build-connection-anchors.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/build-connection-anchors.ts new file mode 100644 index 00000000..93b5874e --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/build-connection-anchors.ts @@ -0,0 +1,9 @@ +import { IPoint } from '@foblex/2d'; + +export function buildConnectionAnchors( + source: IPoint, + target: IPoint, + waypoints?: IPoint[], +): IPoint[] { + return [source, ...(waypoints ?? []), target]; +} diff --git a/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/calculate-center-between-points.handler.spec.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-center-between-points.spec.ts similarity index 58% rename from projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/calculate-center-between-points.handler.spec.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-center-between-points.spec.ts index 87f7660d..a0cdc325 100644 --- a/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/calculate-center-between-points.handler.spec.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-center-between-points.spec.ts @@ -1,20 +1,17 @@ -import { CalculateCenterBetweenPointsHandler } from './calculate-center-between-points.handler'; import { PointExtensions, IPoint } from '@foblex/2d'; -import { CalculateCenterBetweenPointsRequest } from './calculate-center-between-points-request'; - -describe('CalculateCenterBetweenPointsHandler', () => { - let handler: CalculateCenterBetweenPointsHandler; +import { TestBed } from '@angular/core/testing'; +import { calculateCenterBetweenPoints } from '@foblex/flow'; +describe('calculateCenterBetweenPoints', () => { beforeEach(() => { - handler = new CalculateCenterBetweenPointsHandler(); + TestBed.configureTestingModule({}); }); it('should calculate center between two points when target.x > source.x and target.y > source.y', () => { const source: IPoint = { x: 0, y: 0 }; const target: IPoint = { x: 4, y: 4 }; - const request = new CalculateCenterBetweenPointsRequest(source, target); - const result = handler.handle(request); + const result = calculateCenterBetweenPoints(source, target); expect(result).toEqual(PointExtensions.initialize(2, 2)); }); @@ -22,9 +19,7 @@ describe('CalculateCenterBetweenPointsHandler', () => { it('should calculate center between two points when target.x < source.x and target.y < source.y', () => { const source: IPoint = { x: 4, y: 4 }; const target: IPoint = { x: 0, y: 0 }; - const request = new CalculateCenterBetweenPointsRequest(source, target); - - const result = handler.handle(request); + const result = calculateCenterBetweenPoints(source, target); expect(result).toEqual(PointExtensions.initialize(2, 2)); }); @@ -32,9 +27,7 @@ describe('CalculateCenterBetweenPointsHandler', () => { it('should calculate center between two points when target.x > source.x and target.y < source.y', () => { const source: IPoint = { x: 0, y: 4 }; const target: IPoint = { x: 4, y: 0 }; - const request = new CalculateCenterBetweenPointsRequest(source, target); - - const result = handler.handle(request); + const result = calculateCenterBetweenPoints(source, target); expect(result).toEqual(PointExtensions.initialize(2, 2)); }); @@ -42,9 +35,7 @@ describe('CalculateCenterBetweenPointsHandler', () => { it('should calculate center between two points when target.x < source.x and target.y > source.y', () => { const source: IPoint = { x: 4, y: 0 }; const target: IPoint = { x: 0, y: 4 }; - const request = new CalculateCenterBetweenPointsRequest(source, target); - - const result = handler.handle(request); + const result = calculateCenterBetweenPoints(source, target); expect(result).toEqual(PointExtensions.initialize(2, 2)); }); @@ -52,9 +43,7 @@ describe('CalculateCenterBetweenPointsHandler', () => { it('should calculate center when source and target are the same point', () => { const source: IPoint = { x: 4, y: 4 }; const target: IPoint = { x: 4, y: 4 }; - const request = new CalculateCenterBetweenPointsRequest(source, target); - - const result = handler.handle(request); + const result = calculateCenterBetweenPoints(source, target); expect(result).toEqual(PointExtensions.initialize(4, 4)); }); diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-center-between-points.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-center-between-points.ts new file mode 100644 index 00000000..8a5f11d3 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-center-between-points.ts @@ -0,0 +1,11 @@ +import { IPoint } from '@foblex/2d'; + +export function calculateCenterBetweenPoints(source: IPoint, target: IPoint): IPoint { + const offsetX = Math.abs(target.x - source.x) / 2; + const centerX = target.x < source.x ? target.x + offsetX : target.x - offsetX; + + const offsetY = Math.abs(target.y - source.y) / 2; + const centerY = target.y < source.y ? target.y + offsetY : target.y - offsetY; + + return { x: centerX, y: centerY }; +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-curve-candidates.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-curve-candidates.ts new file mode 100644 index 00000000..44f767a8 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-curve-candidates.ts @@ -0,0 +1,9 @@ +import { ICubicSegment } from './multi-cubic'; +import { cubicBezierAtT } from './sample-cubic-bezier-uniform'; +import { IPoint } from '@foblex/2d'; + +export function calculateCurveCandidates(segments: ICubicSegment[]): IPoint[] { + return segments.map((s) => { + return cubicBezierAtT(s.p0, s.c1, s.c2, s.p3, 0.5); + }); +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-polyline-candidates.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-polyline-candidates.ts new file mode 100644 index 00000000..e2b164cb --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/calculate-polyline-candidates.ts @@ -0,0 +1,55 @@ +import { IPoint } from '@foblex/2d'; + +export function calculatePolylineCandidates( + polyline: IPoint[], +): IPoint[] { + if (polyline.length < 2) { + throw new Error('Polylines must be at least two points'); + } + + const total = polylineTotalLength(polyline); + if (total <= 0) { + return [{ ...polyline[0] }]; + } + + const mid = pointAtPolylineLength(polyline, total / 2); + + return [mid]; +} + +function polylineTotalLength(points: IPoint[]): number { + let len = 0; + for (let i = 0; i < points.length - 1; i++) { + len += dist(points[i], points[i + 1]); + } + + return len; +} + +function pointAtPolylineLength(points: IPoint[], s: number): IPoint { + let acc = 0; + + for (let i = 0; i < points.length - 1; i++) { + const a = points[i]; + const b = points[i + 1]; + const segLen = dist(a, b); + + if (segLen <= 0) continue; + + if (acc + segLen >= s) { + const t = (s - acc) / segLen; + + return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t }; + } + + acc += segLen; + } + + const last = points[points.length - 1]; + + return { x: last.x, y: last.y }; +} + +function dist(a: IPoint, b: IPoint): number { + return Math.hypot(b.x - a.x, b.y - a.y); +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/index.ts new file mode 100644 index 00000000..3e71c16e --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/index.ts @@ -0,0 +1,8 @@ +export * from './build-connection-anchors'; +export * from './calculate-center-between-points'; +export * from './calculate-curve-candidates'; +export * from './calculate-polyline-candidates'; +export * from './merge-point-chains'; +export * from './multi-cubic'; +export * from './normalize-polyline'; +export * from './sample-cubic-bezier-uniform'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/merge-point-chains.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/merge-point-chains.ts new file mode 100644 index 00000000..cd667154 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/merge-point-chains.ts @@ -0,0 +1,13 @@ +import { IPoint } from '@foblex/2d'; + +export function mergePointChains(chains: IPoint[][]): IPoint[] { + const out: IPoint[] = []; + for (const chain of chains) { + for (const p of chain) { + const last = out[out.length - 1]; + if (!last || last.x !== p.x || last.y !== p.y) out.push(p); + } + } + + return out; +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/multi-cubic.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/multi-cubic.ts new file mode 100644 index 00000000..2b091b35 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/multi-cubic.ts @@ -0,0 +1,45 @@ +import { IPoint } from '@foblex/2d'; +import { sampleCubicBezierUniform } from './sample-cubic-bezier-uniform'; + +export interface ICubicSegment { + p0: IPoint; + c1: IPoint; + c2: IPoint; + p3: IPoint; + chainIndex: number; +} + +export function createMultiCubicPath(segments: ICubicSegment[]): string { + if (!segments.length) return ''; + let d = `M ${segments[0].p0.x} ${segments[0].p0.y}`; + + for (let i = 0; i < segments.length; i++) { + const s = segments[i]; + const isLast = i === segments.length - 1; + + const x = isLast ? s.p3.x + 0.0002 : s.p3.x; + const y = isLast ? s.p3.y + 0.0002 : s.p3.y; + + d += ` C ${s.c1.x} ${s.c1.y}, ${s.c2.x} ${s.c2.y}, ${x} ${y}`; + } + + return d; +} + +export function sampleMultiCubicUniform( + segments: ICubicSegment[], + samplesPerSegment = 16, +): IPoint[] { + if (!segments.length) return []; + const out: IPoint[] = []; + + for (let i = 0; i < segments.length; i++) { + const s = segments[i]; + const pts = sampleCubicBezierUniform([s.p0, s.c1, s.c2, s.p3], samplesPerSegment); + + if (i > 0) pts.shift(); + out.push(...pts); + } + + return out; +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/normalize-polyline.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/normalize-polyline.ts new file mode 100644 index 00000000..9e4f1ef2 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/normalize-polyline.ts @@ -0,0 +1,38 @@ +import { IPoint } from '@foblex/2d'; + +export function normalizePolyline(points: IPoint[], eps = 1e-6): IPoint[] { + const n = points.length; + if (n <= 2) return points; + + const tmp: IPoint[] = []; + tmp.push(points[0]); + + for (let i = 1; i < n; i++) { + const p = points[i]; + const last = tmp[tmp.length - 1]; + + if (Math.abs(p.x - last.x) > eps || Math.abs(p.y - last.y) > eps) { + tmp.push(p); + } + } + + if (tmp.length <= 2) return tmp; + + const out: IPoint[] = []; + out.push(tmp[0]); + + for (let i = 1; i < tmp.length - 1; i++) { + const a = out[out.length - 1]; + const b = tmp[i]; + const c = tmp[i + 1]; + + const collinearX = Math.abs(a.x - b.x) <= eps && Math.abs(b.x - c.x) <= eps; + const collinearY = Math.abs(a.y - b.y) <= eps && Math.abs(b.y - c.y) <= eps; + + if (!collinearX && !collinearY) out.push(b); + } + + out.push(tmp[tmp.length - 1]); + + return out; +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/sample-cubic-bezier-uniform.spec.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/sample-cubic-bezier-uniform.spec.ts new file mode 100644 index 00000000..c090c96f --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/sample-cubic-bezier-uniform.spec.ts @@ -0,0 +1,95 @@ +import { TestBed } from '@angular/core/testing'; +import { IPoint } from '@foblex/2d'; +import { sampleCubicBezierUniform } from '@foblex/flow'; + +describe('sampleCubicBezierUniform', () => { + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should return samples + 1 points', () => { + const points: IPoint[] = [ + { x: 0, y: 0 }, + { x: 1, y: 2 }, + { x: 3, y: 2 }, + { x: 4, y: 0 }, + ]; + + const result = sampleCubicBezierUniform(points, 10); + + expect(result.length).toBe(11); + }); + + it('should include both endpoints', () => { + const points: IPoint[] = [ + { x: 10, y: 20 }, + { x: 15, y: 25 }, + { x: 30, y: 5 }, + { x: 40, y: 50 }, + ]; + + const result = sampleCubicBezierUniform(points, 8); + + expect(result[0].x).toBeCloseTo(10); + expect(result[0].y).toBeCloseTo(20); + + expect(result[result.length - 1].x).toBeCloseTo(40); + expect(result[result.length - 1].y).toBeCloseTo(50); + }); + + it('should return correct point for a straight line (all control points collinear)', () => { + const points: IPoint[] = [ + { x: 0, y: 0 }, // p0 + { x: 1, y: 0 }, // p1 + { x: 3, y: 0 }, // p2 + { x: 4, y: 0 }, // p3 + ]; + + const result = sampleCubicBezierUniform(points, 4); + // t = 0.5 for i=2 when samples=4 + expect(result[2].x).toBeCloseTo(2); + expect(result[2].y).toBeCloseTo(0); + }); + + it('should return correct midpoint for a symmetric arch', () => { + const points: IPoint[] = [ + { x: 0, y: 0 }, // p0 + { x: 0, y: 2 }, // p1 + { x: 4, y: 2 }, // p2 + { x: 4, y: 0 }, // p3 + ]; + + const result = sampleCubicBezierUniform(points, 2); + // i=1 => t=0.5 + expect(result[1].x).toBeCloseTo(2); + expect(result[1].y).toBeCloseTo(1.5); + }); + + it('should work with default samples value', () => { + const points: IPoint[] = [ + { x: 0, y: 0 }, + { x: 2, y: 3 }, + { x: 3, y: 2 }, + { x: 5, y: 0 }, + ]; + + const result = sampleCubicBezierUniform(points); + + expect(result.length).toBe(33); + expect(result[0].x).toBeCloseTo(0); + expect(result[0].y).toBeCloseTo(0); + expect(result[result.length - 1].x).toBeCloseTo(5); + expect(result[result.length - 1].y).toBeCloseTo(0); + }); + + it('should clone the first point (do not keep reference)', () => { + const p0: IPoint = { x: 1, y: 1 }; + const points: IPoint[] = [p0, { x: 2, y: 2 }, { x: 3, y: 2 }, { x: 4, y: 1 }]; + + const result = sampleCubicBezierUniform(points, 3); + + expect(result[0]).not.toBe(p0); + expect(result[0].x).toBeCloseTo(1); + expect(result[0].y).toBeCloseTo(1); + }); +}); diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/sample-cubic-bezier-uniform.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/sample-cubic-bezier-uniform.ts new file mode 100644 index 00000000..e4655471 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/builders/utils/sample-cubic-bezier-uniform.ts @@ -0,0 +1,45 @@ +import { IPoint } from '@foblex/2d'; + +/** + * Uniformly samples a cubic Bézier segment. + * + * @param points - Start point, First control point, Second control point, End point. + * @param samples - Number of sub-segments (default: 32). The function returns `samples + 1` points. + * @returns Array of sampled points including both endpoints. + * @remarks + * Sampling is uniform in parameter `t`, not in arc length. This is typically + * sufficient for hit-testing and bounding boxes; if you need error-bounded + * flattening, consider an adaptive subdivision strategy instead. + */ +export function sampleCubicBezierUniform(points: IPoint[], samples = 32): IPoint[] { + const out: IPoint[] = new Array(samples + 1); + out[0] = { ...points[0] }; + for (let i = 1; i <= samples; i++) { + out[i] = cubicBezierAtT(points[0], points[1], points[2], points[3], i / samples); + } + + return out; +} + +/** + * Evaluates a cubic Bézier at parameter `t` in [0, 1]. + * + * @param p0 - Start point. + * @param p1 - First control point. + * @param p2 - Second control point. + * @param p3 - End point. + * @param t - Parameter in [0, 1]. + * @returns Point on the curve at `t`. + */ +export function cubicBezierAtT(p0: IPoint, p1: IPoint, p2: IPoint, p3: IPoint, t: number): IPoint { + const u = 1 - t, + tt = t * t, + uu = u * u, + uuu = uu * u, + ttt = tt * t; + + return { + x: uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x, + y: uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y, + }; +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/connection-line-builder-request.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/connection-line-builder-request.ts new file mode 100644 index 00000000..91bf27c3 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/connection-line-builder-request.ts @@ -0,0 +1,9 @@ +import { IFConnectionBuilderRequest } from './models'; +import { EFConnectionType } from '../../enums'; + +export class ConnectionLineBuilderRequest { + constructor( + public readonly type: string | EFConnectionType, + public readonly payload: IFConnectionBuilderRequest, + ) {} +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/connection-line-builder.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/connection-line-builder.ts new file mode 100644 index 00000000..2c94a7cb --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/connection-line-builder.ts @@ -0,0 +1,60 @@ +import { IFConnectionBuilderResponse, IFConnectionBuilder } from './models'; +import { + CalculateAdaptiveCurveData, + CalculateBezierCurveData, + CalculateSegmentLineData, + CalculateStraightLineData, +} from './builders'; +import { ConnectionLineBuilderRequest } from './connection-line-builder-request'; +import { inject, Injectable } from '@angular/core'; +import { F_CONNECTION_BUILDERS } from './providers'; +import { EFConnectionType } from '../../enums'; + +@Injectable() +export class ConnectionLineBuilder { + private readonly _builtinBuilders: Record = { + [EFConnectionType.STRAIGHT]: new CalculateStraightLineData(), + [EFConnectionType.BEZIER]: new CalculateBezierCurveData(), + [EFConnectionType.ADAPTIVE_CURVE]: new CalculateAdaptiveCurveData(), + [EFConnectionType.SEGMENT]: new CalculateSegmentLineData(), + }; + + private readonly _providedBuilders: Record; + private readonly _builders: Record; + + constructor() { + this._providedBuilders = inject(F_CONNECTION_BUILDERS, { optional: true }) || {}; + this._builders = { + ...this._builtinBuilders, + ...this._providedBuilders, + }; + } + + public handle({ type, payload }: ConnectionLineBuilderRequest): IFConnectionBuilderResponse { + const builder = this._builders[type]; + if (!builder) { + throw this._createBuilderNotFoundError(type); + } + + return builder.handle(payload); + } + + private _createBuilderNotFoundError(requestedType: string): Error { + const builtinTypes = Object.keys(this._builtinBuilders).sort(); + const providedTypes = Object.keys(this._providedBuilders).sort(); + const registeredTypes = Object.keys(this._builders).sort(); + + const overriddenBuiltins = builtinTypes.filter((t) => t in this._providedBuilders).sort(); + + const lines = [ + `Connection Builder Error: builder type "${requestedType}" not found.`, + `Registered types: ${registeredTypes.length ? registeredTypes.join(', ') : '(none)'}`, + `Built-in types: ${builtinTypes.length ? builtinTypes.join(', ') : '(none)'}`, + `Provided (F_CONNECTION_BUILDERS) types: ${providedTypes.length ? providedTypes.join(', ') : '(none)'}`, + overriddenBuiltins.length ? `Overridden built-ins: ${overriddenBuiltins.join(', ')}` : null, + `Tip: ensure you pass a valid Connection Type or provide a builder via F_CONNECTION_BUILDERS.`, + ].filter(Boolean); + + return new Error(lines.join('\n')); + } +} diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/index.ts new file mode 100644 index 00000000..7773b34a --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/index.ts @@ -0,0 +1,5 @@ +export * from './builders'; +export * from './models'; +export * from './providers'; +export * from './connection-line-builder'; +export * from './connection-line-builder-request'; diff --git a/projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-builder-request.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/i-f-connection-builder-request.ts similarity index 75% rename from projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-builder-request.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/i-f-connection-builder-request.ts index 3c132561..bb1dc989 100644 --- a/projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-builder-request.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/i-f-connection-builder-request.ts @@ -1,8 +1,7 @@ import { IPoint } from '@foblex/2d'; -import { EFConnectableSide } from '../../f-connectors'; +import { EFConnectableSide } from '../../../enums'; export interface IFConnectionBuilderRequest { - source: IPoint; sourceSide: EFConnectableSide; @@ -14,4 +13,6 @@ export interface IFConnectionBuilderRequest { radius: number; offset: number; + + waypoints: IPoint[]; } diff --git a/projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-builder-response.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/i-f-connection-builder-response.ts similarity index 76% rename from projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-builder-response.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/i-f-connection-builder-response.ts index 641a439f..bc41f7b6 100644 --- a/projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-builder-response.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/i-f-connection-builder-response.ts @@ -3,11 +3,11 @@ import { IPoint } from '@foblex/2d'; export interface IFConnectionBuilderResponse { path: string; - connectionCenter: IPoint; - penultimatePoint: IPoint; secondPoint: IPoint; - points?: IPoint[]; + points: IPoint[]; + + candidates: IPoint[]; } diff --git a/projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-builder.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/i-f-connection-builder.ts similarity index 59% rename from projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-builder.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/i-f-connection-builder.ts index 513ccc8e..fffdcf14 100644 --- a/projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-builder.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/i-f-connection-builder.ts @@ -1,9 +1,6 @@ -import { IHandler } from '@foblex/mediator'; import { IFConnectionBuilderRequest } from './i-f-connection-builder-request'; import { IFConnectionBuilderResponse } from './i-f-connection-builder-response'; -export interface IFConnectionBuilder - extends IHandler { - +export interface IFConnectionBuilder { handle(request: IFConnectionBuilderRequest): IFConnectionBuilderResponse; } diff --git a/projects/f-flow/src/f-connection/f-connection-builder/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/index.ts similarity index 51% rename from projects/f-flow/src/f-connection/f-connection-builder/index.ts rename to projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/index.ts index 64bf8dd4..7396620f 100644 --- a/projects/f-flow/src/f-connection/f-connection-builder/index.ts +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/models/index.ts @@ -1,11 +1,3 @@ -export * from './f-connection-builders'; - -export * from './f-connection-factory'; - -export * from './i-f-connection-builder'; - export * from './i-f-connection-builder-request'; - export * from './i-f-connection-builder-response'; - -export * from './i-f-connection-factory-request'; +export * from './i-f-connection-builder'; diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/providers/f-connection-builders.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/providers/f-connection-builders.ts new file mode 100644 index 00000000..a060ee84 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/providers/f-connection-builders.ts @@ -0,0 +1,8 @@ +import { InjectionToken } from '@angular/core'; +import { IFConnectionBuilder } from '../models'; + +export type IConnectionBuilders = Record; + +export const F_CONNECTION_BUILDERS = new InjectionToken( + 'F_CONNECTION_BUILDERS', +); diff --git a/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/providers/index.ts b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/providers/index.ts new file mode 100644 index 00000000..3ef1b45f --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/connection-line-builder/providers/index.ts @@ -0,0 +1 @@ +export * from './f-connection-builders'; diff --git a/projects/f-flow/src/f-connection-v2/utils/create-connection-dom-identifier.ts b/projects/f-flow/src/f-connection-v2/utils/create-connection-dom-identifier.ts new file mode 100644 index 00000000..0339b20a --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/create-connection-dom-identifier.ts @@ -0,0 +1,9 @@ +import { normalizeDomElementId } from '@foblex/utils'; + +export function createConnectionDomIdentifier( + componentId: string, + sourceId: string, + targetId: string, +): string { + return normalizeDomElementId('connection_' + componentId + sourceId + targetId); +} diff --git a/projects/f-flow/src/f-connection-v2/utils/create-connection-selection-dom-identifier.ts b/projects/f-flow/src/f-connection-v2/utils/create-connection-selection-dom-identifier.ts new file mode 100644 index 00000000..e98d9144 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/create-connection-selection-dom-identifier.ts @@ -0,0 +1,9 @@ +import { normalizeDomElementId } from '@foblex/utils'; + +export function createConnectionSelectionDomIdentifier( + componentId: string, + sourceId: string, + targetId: string, +): string { + return normalizeDomElementId('connection_for_selection_' + componentId + sourceId + targetId); +} diff --git a/projects/f-flow/src/f-connection-v2/utils/create-gradient-dom-identifier.ts b/projects/f-flow/src/f-connection-v2/utils/create-gradient-dom-identifier.ts new file mode 100644 index 00000000..111adbd5 --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/create-gradient-dom-identifier.ts @@ -0,0 +1,9 @@ +import { normalizeDomElementId } from '@foblex/utils'; + +export function createGradientDomIdentifier( + componentId: string, + sourceId: string, + targetId: string, +): string { + return normalizeDomElementId('connection_gradient_' + componentId + sourceId + targetId); +} diff --git a/projects/f-flow/src/f-connection-v2/utils/create-gradient-dom-url.ts b/projects/f-flow/src/f-connection-v2/utils/create-gradient-dom-url.ts new file mode 100644 index 00000000..7fbabc1d --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/create-gradient-dom-url.ts @@ -0,0 +1,9 @@ +import { createGradientDomIdentifier } from './create-gradient-dom-identifier'; + +export function createGradientDomUrl( + componentId: string, + sourceId: string, + targetId: string, +): string { + return `url(#${createGradientDomIdentifier(componentId, sourceId, targetId)})`; +} diff --git a/projects/f-flow/src/f-connection-v2/utils/index.ts b/projects/f-flow/src/f-connection-v2/utils/index.ts new file mode 100644 index 00000000..56403c2f --- /dev/null +++ b/projects/f-flow/src/f-connection-v2/utils/index.ts @@ -0,0 +1,6 @@ +export * from './connection-behaviour'; +export * from './connection-line-builder'; +export * from './create-connection-dom-identifier'; +export * from './create-connection-selection-dom-identifier'; +export * from './create-gradient-dom-identifier'; +export * from './create-gradient-dom-url'; diff --git a/projects/f-flow/src/f-connection/common/domain/adaptive-curve-builder.spec.ts b/projects/f-flow/src/f-connection/common/domain/adaptive-curve-builder.spec.ts deleted file mode 100644 index 7d9cbe2b..00000000 --- a/projects/f-flow/src/f-connection/common/domain/adaptive-curve-builder.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { IFConnectionBuilderRequest, IFConnectionBuilderResponse } from '@foblex/flow'; -import { EFConnectableSide } from '@foblex/flow'; -import { AdaptiveCurveBuilder } from '@foblex/flow'; - -describe('AdaptiveCurveBuilder', () => { - let builder: AdaptiveCurveBuilder; - - beforeEach(() => { - builder = new AdaptiveCurveBuilder(); - }); - - function expectCenterWithinPointsBBox(resp: IFConnectionBuilderResponse) { - const xs = resp.points?.map((p) => p.x) || []; - const ys = resp.points?.map((p) => p.y) || []; - const minX = Math.min(...xs); - const maxX = Math.max(...xs); - const minY = Math.min(...ys); - const maxY = Math.max(...ys); - - expect(resp.connectionCenter).toBeDefined(); - expect(Number.isFinite(resp.connectionCenter.x)).toBe(true); - expect(Number.isFinite(resp.connectionCenter.y)).toBe(true); - - const EPS = 1e-2; - expect(resp.connectionCenter.x).toBeGreaterThanOrEqual(minX - EPS); - expect(resp.connectionCenter.x).toBeLessThanOrEqual(maxX + EPS); - expect(resp.connectionCenter.y).toBeGreaterThanOrEqual(minY - EPS); - expect(resp.connectionCenter.y).toBeLessThanOrEqual(maxY + EPS); - } - - it('builds a path and center for a horizontal connection (left → right)', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 100, y: 0 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.LEFT, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.path.startsWith('M 0 0 C')).toBe(true); // cubic path - expect(response.points?.length).toBe(33); // 32 samples + start - expect(response.secondPoint).toBeDefined(); - expect(response.penultimatePoint).toBeDefined(); - - expect(response.connectionCenter.x).toBeCloseTo(50, 5); - expect(response.connectionCenter.y).toBeCloseTo(0, 5); - }); - - it('builds a path and center for a vertical connection (top → bottom)', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 0, y: 100 }, - sourceSide: EFConnectableSide.BOTTOM, - targetSide: EFConnectableSide.TOP, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.path.startsWith('M 0 0 C')).toBe(true); - expect(response.points?.length).toBe(33); - - expect(response.connectionCenter.x).toBeCloseTo(0, 5); - expect(response.connectionCenter.y).toBeCloseTo(50, 5); - }); - - it('builds a path and center for a diagonal connection', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 100, y: 100 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.BOTTOM, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.path).toContain('C'); - expect(response.points?.length).toBe(33); - - expectCenterWithinPointsBBox(response); - }); - - it('builds a path and center for a connection with offset', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 50, y: 50 }, - sourceSide: EFConnectableSide.BOTTOM, - targetSide: EFConnectableSide.LEFT, - radius: 10, - offset: 30, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.path).toContain('C'); - expect(response.points?.length).toBe(33); - expectCenterWithinPointsBBox(response); - }); - - it('ensures control points are not equal to anchors (non-degenerate handles)', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 10, y: 20 }, - target: { x: 110, y: 120 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.TOP, - radius: 0, - offset: 16, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - const { secondPoint: c1, penultimatePoint: c2 } = response; - expect(c1).toBeDefined(); - expect(c2).toBeDefined(); - - expect(!(c1.x === request.source.x && c1.y === request.source.y)).toBe(true); - expect(!(c2.x === request.target.x && c2.y === request.target.y)).toBe(true); - - expectCenterWithinPointsBBox(response); - }); -}); diff --git a/projects/f-flow/src/f-connection/common/domain/adaptive-curve-builder.ts b/projects/f-flow/src/f-connection/common/domain/adaptive-curve-builder.ts deleted file mode 100644 index d6cd0a78..00000000 --- a/projects/f-flow/src/f-connection/common/domain/adaptive-curve-builder.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { IPoint } from '@foblex/2d'; -import { EFConnectableSide } from '../../../f-connectors'; -import { - CalculateConnectionCenterHandler, - CalculateConnectionCenterRequest, -} from './calculate-connection-center'; -import { - IFConnectionBuilder, - IFConnectionBuilderRequest, - IFConnectionBuilderResponse, -} from '../../f-connection-builder'; - -/** - * AdaptiveCurveBuilder - * - * Builds a smooth **adaptive connector curve** between two points using a cubic Bézier path, - * but with control points computed differently from a classic “Bezier” connector. - * - * Key differences from a classic Bézier builder: - * - Control handles are **aligned with the port side** (LEFT/RIGHT/TOP/BOTTOM), - * yet are **slightly blended toward the target direction** to reduce harsh S-shapes. - * - Handle lengths are **adaptive**: based on the `offset` (your “padding” from ports) - * and the projected distance along the side’s axis. This produces a subtler curvature - * and a distinct look & feel compared to a standard Bézier connector. - * - * Compatibility: - * - Implements {@link IFConnectionBuilder}; output shape is the same: - * SVG path, sampled points for hit-testing, and connection center. - * - Only the four base sides are expected to be passed here: - * {@link EFConnectableSide.LEFT}, {@link EFConnectableSide.RIGHT}, - * {@link EFConnectableSide.TOP}, {@link EFConnectableSide.BOTTOM}. - * - * Usage example: - * ```ts - * const builder = new AdaptiveCurveBuilder(); - * const res = builder.handle({ - * source: { x: 100, y: 120 }, - * sourceSide: EFConnectableSide.RIGHT, - * target: { x: 340, y: 220 }, - * targetSide: EFConnectableSide.LEFT, - * offset: 16, // acts as padding + minimal handle baseline - * }); - * // res.path -> SVG "M … C …" string - * // res.points -> sampled points along the curve - * ``` - */ -export class AdaptiveCurveBuilder implements IFConnectionBuilder { - /** - * Returns a unit direction vector for a given side. - * - * @param side - One of LEFT/RIGHT/TOP/BOTTOM. - * @returns Unit vector pointing outwards from the side. - * @remarks - * Only base sides should reach this builder. A fallback `{0,0}` is returned - * defensively, but should not occur in normal usage. - */ - private static _dir(side: EFConnectableSide): IPoint { - switch (side) { - case EFConnectableSide.LEFT: - return { x: -1, y: 0 }; - case EFConnectableSide.RIGHT: - return { x: 1, y: 0 }; - case EFConnectableSide.TOP: - return { x: 0, y: -1 }; - case EFConnectableSide.BOTTOM: - return { x: 0, y: 1 }; - } - - return { x: 0, y: 0 }; - } - - /** - * Checks whether a side is horizontal (LEFT/RIGHT). - * - * @param side - Side to check. - * @returns `true` for LEFT/RIGHT, otherwise `false`. - */ - private static _isH(side: EFConnectableSide): boolean { - return side === EFConnectableSide.LEFT || side === EFConnectableSide.RIGHT; - } - - /** - * Computes a **soft** handle length for a control point. - * - * The handle length is derived from: - * - `offset` (acts as padding & the baseline handle), - * - distance projected along the side’s axis (dx for horizontal, dy for vertical), - * - a conservative upper bound to avoid bloated curves. - * - * @param p0 - Start (or end) point of the segment. - * @param p3 - Opposite end (or start) point. - * @param side - Side at which the handle originates. - * @param offset - Visual padding/minimal handle baseline (must be ≥ 0). - * @returns A bounded positive handle length. - */ - private static _handleLen( - p0: IPoint, - p3: IPoint, - side: EFConnectableSide, - offset: number, - ): number { - const dx = Math.abs(p3.x - p0.x); - const dy = Math.abs(p3.y - p0.y); - const d = Math.hypot(dx, dy); - const along = this._isH(side) ? dx : dy; - - const MIN = Math.max(8, offset); - const MAX = offset + 0.5 * d; // upper cap to keep the shape compact - const len = offset * 1.05 + along * 0.3; // moderate growth along the axis - - return Math.min(MAX, Math.max(MIN, len)); - } - - /** - * Computes a control point by taking the side direction and **lightly blending** - * it toward the target direction. This reduces harsh inflections while keeping - * the perceptual attachment to the port side. - * - * @param side - Base side defining the primary direction (LEFT/RIGHT/TOP/BOTTOM). - * @param source - Anchor point where the control handle is attached. - * @param target - Opposite end; used to orient the handle slightly toward the goal. - * @param handle - Handle length (see {@link _handleLen}). - * @returns Control point coordinates for the cubic Bézier segment. - */ - private static _softControl( - side: EFConnectableSide, - source: IPoint, - target: IPoint, - handle: number, - ): IPoint { - const v = this._dir(side); - - const dx = target.x - source.x; - const dy = target.y - source.y; - const dist = Math.hypot(dx, dy) || 1; - const tx = dx / dist; - const ty = dy / dist; - - // Orientation of side vs. target direction (in [-1, 1]) - const dot = v.x * tx + v.y * ty; - - // Small base blend toward the target; slightly increase if the side points away. - const baseBlend = 0.12; - const extra = Math.max(0, -dot) * 0.08; // add a bit when pointing “backwards” - const blend = Math.min(0.2, baseBlend + extra); - - const dirX = v.x * (1 - blend) + tx * blend; - const dirY = v.y * (1 - blend) + ty * blend; - const len = Math.hypot(dirX, dirY) || 1; - - return { x: source.x + (dirX / len) * handle, y: source.y + (dirY / len) * handle }; - } - - /** - * Builds an adaptive connector between `source` and `target`. - * - * @param request - Connection build request. - * @param request.source - Start point of the connector. - * @param request.sourceSide - Side at the source port (LEFT/RIGHT/TOP/BOTTOM). - * @param request.target - End point of the connector. - * @param request.targetSide - Side at the target port (LEFT/RIGHT/TOP/BOTTOM). - * @param request.offset - Visual padding & minimal handle baseline (≥ 0). - * - * @returns Standard {@link IFConnectionBuilderResponse}: - * - `path`: SVG cubic path (“M … C …”), - * - `points`: discretized samples along the curve, - * - `connectionCenter`: approximate visual center for labeling/interaction, - * - `secondPoint`/`penultimatePoint`: first/second control points (for tooling). - * - * @remarks - * The returned path slightly offsets the end point by +0.0002 on both axes - * to avoid edge cases in some renderers and hit-testing implementations. - */ - public handle(request: IFConnectionBuilderRequest): IFConnectionBuilderResponse { - const { source, sourceSide, target, targetSide } = request; - const offset = Math.max(0, request.offset ?? 0); - - const p0 = { x: source.x, y: source.y }; - const p3 = { x: target.x, y: target.y }; - - const h0 = AdaptiveCurveBuilder._handleLen(p0, p3, sourceSide, offset); - const h3 = AdaptiveCurveBuilder._handleLen(p3, p0, targetSide, offset); - - const c1 = AdaptiveCurveBuilder._softControl(sourceSide, p0, p3, h0); - const c2 = AdaptiveCurveBuilder._softControl(targetSide, p3, p0, h3); - - const path = `M ${p0.x} ${p0.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${p3.x + 0.0002} ${p3.y + 0.0002}`; - - const connectionCenter = new CalculateConnectionCenterHandler().handle( - new CalculateConnectionCenterRequest([p0, c1, c2, p3]), - ); - - return { - path, - connectionCenter, - secondPoint: c1, - penultimatePoint: c2, - points: _sampleCubic(p0, c1, c2, p3, 32), - }; - } -} - -/** - * Evaluates a cubic Bézier at parameter `t` in [0, 1]. - * - * @param p0 - Start point. - * @param p1 - First control point. - * @param p2 - Second control point. - * @param p3 - End point. - * @param t - Parameter in [0, 1]. - * @returns Point on the curve at `t`. - */ -function bez3(p0: IPoint, p1: IPoint, p2: IPoint, p3: IPoint, t: number): IPoint { - const u = 1 - t, - tt = t * t, - uu = u * u, - uuu = uu * u, - ttt = tt * t; - - return { - x: uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x, - y: uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y, - }; -} - -/** - * Uniformly samples a cubic Bézier segment. - * - * @param p0 - Start point. - * @param p1 - First control point. - * @param p2 - Second control point. - * @param p3 - End point. - * @param samples - Number of sub-segments (default: 32). The function returns `samples + 1` points. - * @returns Array of sampled points including both endpoints. - * @remarks - * Sampling is uniform in parameter `t`, not in arc length. This is typically - * sufficient for hit-testing and bounding boxes; if you need error-bounded - * flattening, consider an adaptive subdivision strategy instead. - */ -function _sampleCubic(p0: IPoint, p1: IPoint, p2: IPoint, p3: IPoint, samples = 32): IPoint[] { - const out: IPoint[] = new Array(samples + 1); - out[0] = { ...p0 }; - for (let i = 1; i <= samples; i++) { - out[i] = bez3(p0, p1, p2, p3, i / samples); - } - - return out; -} diff --git a/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/calculate-center-between-points-request.ts b/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/calculate-center-between-points-request.ts deleted file mode 100644 index 380e1387..00000000 --- a/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/calculate-center-between-points-request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IPoint } from '@foblex/2d'; - -export class CalculateCenterBetweenPointsRequest { - static readonly fToken = Symbol('CalculateCenterBetweenPointsRequest'); - - constructor( - public source: IPoint, - public target: IPoint, - ) { - } -} diff --git a/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/calculate-center-between-points.handler.ts b/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/calculate-center-between-points.handler.ts deleted file mode 100644 index 4f016d53..00000000 --- a/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/calculate-center-between-points.handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IHandler } from '@foblex/mediator'; -import { IPoint, PointExtensions } from '@foblex/2d'; -import { CalculateCenterBetweenPointsRequest } from './calculate-center-between-points-request'; - -export class CalculateCenterBetweenPointsHandler - implements IHandler { - - public handle(request: CalculateCenterBetweenPointsRequest): IPoint { - const { source, target } = request; - - const offsetX = Math.abs(target.x - source.x) / 2; - const centerX = (target.x < source.x) ? target.x + offsetX : target.x - offsetX; - - const offsetY = Math.abs(target.y - source.y) / 2; - const centerY = (target.y < source.y) ? target.y + offsetY : target.y - offsetY; - - return PointExtensions.initialize(centerX, centerY); - } -} diff --git a/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/index.ts b/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/index.ts deleted file mode 100644 index 2420187d..00000000 --- a/projects/f-flow/src/f-connection/common/domain/calculate-center-between-points/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './calculate-center-between-points.handler'; - -export * from './calculate-center-between-points-request'; diff --git a/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/calculate-connection-center-request.ts b/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/calculate-connection-center-request.ts deleted file mode 100644 index 3cb77342..00000000 --- a/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/calculate-connection-center-request.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IPoint } from '@foblex/2d'; - -export class CalculateConnectionCenterRequest { - static readonly fToken = Symbol('CalculateConnectionCenterRequest'); - - constructor( - public points: IPoint[], - ) { - } -} diff --git a/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/calculate-connection-center.handler.spec.ts b/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/calculate-connection-center.handler.spec.ts deleted file mode 100644 index 7e49a83d..00000000 --- a/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/calculate-connection-center.handler.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { CalculateConnectionCenterHandler } from './calculate-connection-center.handler'; -import { CalculateConnectionCenterRequest } from './calculate-connection-center-request'; -import { IPoint } from '@foblex/2d'; - -describe('CalculateConnectionCenterHandler', () => { - let handler: CalculateConnectionCenterHandler; - - beforeEach(() => { - handler = new CalculateConnectionCenterHandler(); - }); - - it('should calculate the center point of a connection with multiple points', () => { - const points: IPoint[] = [ - { x: 0, y: 0 }, - { x: 4, y: 0 }, - { x: 4, y: 4 }, - ]; - const request = new CalculateConnectionCenterRequest(points); - - const result = handler.handle(request); - - expect(result.x).toBeCloseTo(4); - expect(result.y).toBeCloseTo(0); - }); - - it('should calculate the center point for a straight line', () => { - const points: IPoint[] = [ - { x: 0, y: 0 }, - { x: 4, y: 0 }, - ]; - const request = new CalculateConnectionCenterRequest(points); - - const result = handler.handle(request); - - expect(result.x).toBeCloseTo(2); - expect(result.y).toBeCloseTo(0); - }); - - it('should calculate the center point for a single segment', () => { - const points: IPoint[] = [ - { x: 0, y: 0 }, - { x: 0, y: 4 }, - ]; - const request = new CalculateConnectionCenterRequest(points); - - const result = handler.handle(request); - - expect(result.x).toBeCloseTo(0); - expect(result.y).toBeCloseTo(2); - }); - - it('should calculate the center for points forming a non-straight path', () => { - const points: IPoint[] = [ - { x: 0, y: 0 }, - { x: 3, y: 4 }, - { x: 6, y: 0 }, - ]; - const request = new CalculateConnectionCenterRequest(points); - - const result = handler.handle(request); - - expect(result.x).toBeCloseTo(3); - expect(result.y).toBeCloseTo(4); - }); -}); diff --git a/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/calculate-connection-center.handler.ts b/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/calculate-connection-center.handler.ts deleted file mode 100644 index 71602c76..00000000 --- a/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/calculate-connection-center.handler.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { IPoint } from '@foblex/2d'; -import { IHandler } from '@foblex/mediator'; -import { CalculateConnectionCenterRequest } from './calculate-connection-center-request'; - -export class CalculateConnectionCenterHandler - implements IHandler { - - public handle(request: CalculateConnectionCenterRequest): IPoint { - - const { points } = request; - let totalDistance = 0; - const distances: number[] = []; - - for (let i = 0; i < points.length - 1; i++) { - const distance = this.calculateDistance(points[ i ], points[ i + 1 ]); - distances.push(distance); - totalDistance += distance; - } - - let accumulatedDistance = 0; - let centerIndex = 0; - const halfTotalDistance = totalDistance / 2; - for (let i = 0; i < distances.length; i++) { - accumulatedDistance += distances[ i ]; - if (accumulatedDistance >= halfTotalDistance) { - centerIndex = i; - break; - } - } - - const remainingDistanceToCenter = halfTotalDistance - (accumulatedDistance - distances[ centerIndex ]); - - return this.findPointAtDistance(points[ centerIndex ], points[ centerIndex + 1 ], remainingDistanceToCenter); - } - - private calculateDistance(pointA: IPoint, pointB: IPoint): number { - return Math.sqrt(Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2)); - } - - private findPointAtDistance(startPoint: IPoint, endPoint: IPoint, distance: number): IPoint { - const totalDistance = this.calculateDistance(startPoint, endPoint); - const ratio = distance / totalDistance; - const x = (1 - ratio) * startPoint.x + ratio * endPoint.x; - const y = (1 - ratio) * startPoint.y + ratio * endPoint.y; - - return { x, y }; - } -} diff --git a/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/index.ts b/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/index.ts deleted file mode 100644 index 2f5af25c..00000000 --- a/projects/f-flow/src/f-connection/common/domain/calculate-connection-center/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './calculate-connection-center.handler'; - -export * from './calculate-connection-center-request'; diff --git a/projects/f-flow/src/f-connection/common/domain/f-bezier.path-builder.spec.ts b/projects/f-flow/src/f-connection/common/domain/f-bezier.path-builder.spec.ts deleted file mode 100644 index 78e0caf7..00000000 --- a/projects/f-flow/src/f-connection/common/domain/f-bezier.path-builder.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { FBezierPathBuilder } from './f-bezier.path-builder'; -import { IFConnectionBuilderRequest, IFConnectionBuilderResponse } from '../../f-connection-builder'; -import { EFConnectableSide } from '../../../f-connectors'; - -describe('FBezierPathBuilder', () => { - let builder: FBezierPathBuilder; - - beforeEach(() => { - builder = new FBezierPathBuilder(); - }); - - it('should build a path and calculate the center for a horizontal connection from left to right', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 100, y: 0 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.LEFT, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.connectionCenter).toBeDefined(); - expect(response.connectionCenter.x).toBeCloseTo(50); - expect(response.connectionCenter.y).toBeCloseTo(0); - }); - - it('should build a path and calculate the center for a vertical connection from top to bottom', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 0, y: 100 }, - sourceSide: EFConnectableSide.BOTTOM, - targetSide: EFConnectableSide.TOP, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.connectionCenter).toBeDefined(); - expect(response.connectionCenter.x).toBeCloseTo(0); - expect(response.connectionCenter.y).toBeCloseTo(50); - }); - - it('should build a path and calculate the center for a diagonal connection', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 100, y: 100 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.BOTTOM, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.connectionCenter).toBeDefined(); - expect(response.connectionCenter.x).toBeCloseTo(100); - expect(response.connectionCenter.y).toBeCloseTo(219.08902300206648); - }); - - it('should build a path and calculate the center for a connection with an offset', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 50, y: 50 }, - sourceSide: EFConnectableSide.BOTTOM, - targetSide: EFConnectableSide.LEFT, - radius: 10, - offset: 30, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.connectionCenter).toBeDefined(); - expect(response.connectionCenter.x).toBeCloseTo(0); - expect(response.connectionCenter.y).toBeCloseTo(50); - }); -}); diff --git a/projects/f-flow/src/f-connection/common/domain/f-bezier.path-builder.ts b/projects/f-flow/src/f-connection/common/domain/f-bezier.path-builder.ts deleted file mode 100644 index 58a9cc3a..00000000 --- a/projects/f-flow/src/f-connection/common/domain/f-bezier.path-builder.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { IPoint } from '@foblex/2d'; -import { EFConnectableSide } from '../../../f-connectors'; -import { - CalculateConnectionCenterHandler, - CalculateConnectionCenterRequest, -} from './calculate-connection-center'; -import { - IFConnectionBuilder, - IFConnectionBuilderRequest, - IFConnectionBuilderResponse, -} from '../../f-connection-builder'; - -export class FBezierPathBuilder implements IFConnectionBuilder { - private static _getConnectorOffset(distance: number, offset: number): number { - if (distance >= offset) { - return distance; - } - - return offset * Math.sqrt(offset - distance); - } - - private static _getAnglePoint( - side: EFConnectableSide, - source: IPoint, - target: IPoint, - offset: number, - ): IPoint { - const result: IPoint = { x: source.x, y: source.y }; - - switch (side) { - case EFConnectableSide.LEFT: - result.x -= FBezierPathBuilder._getConnectorOffset(source.x - target.x, offset); - break; - case EFConnectableSide.RIGHT: - result.x += FBezierPathBuilder._getConnectorOffset(target.x - source.x, offset); - break; - case EFConnectableSide.TOP: - result.y -= FBezierPathBuilder._getConnectorOffset(source.y - target.y, offset); - break; - case EFConnectableSide.BOTTOM: - result.y += FBezierPathBuilder._getConnectorOffset(target.y - source.y, offset); - break; - } - - return result; - } - - public handle(request: IFConnectionBuilderRequest): IFConnectionBuilderResponse { - const { source, sourceSide, target, targetSide, offset } = request; - - const sourceAnglePoint = FBezierPathBuilder._getAnglePoint(sourceSide, source, target, offset); - - const targetAnglePoint = FBezierPathBuilder._getAnglePoint(targetSide, target, source, offset); - - const path = `M ${source.x} ${source.y} C ${sourceAnglePoint.x} ${sourceAnglePoint.y}, ${targetAnglePoint.x} ${targetAnglePoint.y}, ${target.x + 0.0002} ${target.y + 0.0002}`; - - const connectionCenter = new CalculateConnectionCenterHandler().handle( - new CalculateConnectionCenterRequest([source, sourceAnglePoint, targetAnglePoint, target]), - ); - - return { - path, - connectionCenter, - penultimatePoint: targetAnglePoint, - secondPoint: sourceAnglePoint, - points: _sampleCubic(source, sourceAnglePoint, targetAnglePoint, target, 32), - }; - } -} - -function bez3(p0: IPoint, p1: IPoint, p2: IPoint, p3: IPoint, t: number): IPoint { - const u = 1 - t, - tt = t * t, - uu = u * u, - uuu = uu * u, - ttt = tt * t; - - return { - x: uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x, - y: uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y, - }; -} - -function _sampleCubic(p0: IPoint, p1: IPoint, p2: IPoint, p3: IPoint, samples = 32): IPoint[] { - const out: IPoint[] = new Array(samples + 1); - out[0] = { ...p0 }; - for (let i = 1; i <= samples; i++) { - out[i] = bez3(p0, p1, p2, p3, i / samples); - } - - return out; -} diff --git a/projects/f-flow/src/f-connection/common/domain/f-segment.path-builder.spec.ts b/projects/f-flow/src/f-connection/common/domain/f-segment.path-builder.spec.ts deleted file mode 100644 index fb753be0..00000000 --- a/projects/f-flow/src/f-connection/common/domain/f-segment.path-builder.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { FSegmentPathBuilder } from './f-segment.path-builder'; -import { IFConnectionBuilderRequest, IFConnectionBuilderResponse } from '../../f-connection-builder'; -import { EFConnectableSide } from '../../../f-connectors'; - -describe('FSegmentPathBuilder', () => { - let builder: FSegmentPathBuilder; - - beforeEach(() => { - builder = new FSegmentPathBuilder(); - }); - - it('should build a path and calculate the center for a horizontal connection from left to right', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 100, y: 0 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.LEFT, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.connectionCenter).toBeDefined(); - expect(response.connectionCenter.x).toBeCloseTo(50); - expect(response.connectionCenter.y).toBeCloseTo(0); - }); - - it('should build a path and calculate the center for a vertical connection from top to bottom', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 0, y: 100 }, - sourceSide: EFConnectableSide.BOTTOM, - targetSide: EFConnectableSide.TOP, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.connectionCenter).toBeDefined(); - expect(response.connectionCenter.x).toBeCloseTo(0); - expect(response.connectionCenter.y).toBeCloseTo(50); - }); - - it('should build a path and calculate the center for a diagonal connection', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 100, y: 100 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.BOTTOM, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.connectionCenter).toBeDefined(); - expect(response.connectionCenter.x).toBeCloseTo(20); - expect(response.connectionCenter.y).toBeCloseTo(100); - }); - - it('should build a path and calculate the center for a connection with an offset', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 50, y: 50 }, - sourceSide: EFConnectableSide.BOTTOM, - targetSide: EFConnectableSide.LEFT, - radius: 10, - offset: 30, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBeDefined(); - expect(response.connectionCenter).toBeDefined(); - expect(response.connectionCenter.x).toBeCloseTo(0); - expect(response.connectionCenter.y).toBeCloseTo(50); - }); - - it('should build a path with bends for a complex connection', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 100, y: 100 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.LEFT, - radius: 10, - offset: 20, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toContain('Q'); // Ensure it has a quadratic bend - expect(response.connectionCenter).toBeDefined(); - }); -}); diff --git a/projects/f-flow/src/f-connection/common/domain/f-segment.path-builder.ts b/projects/f-flow/src/f-connection/common/domain/f-segment.path-builder.ts deleted file mode 100644 index 62363dfd..00000000 --- a/projects/f-flow/src/f-connection/common/domain/f-segment.path-builder.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { IPoint, PointExtensions } from '@foblex/2d'; -import { EFConnectableSide } from '../../../f-connectors'; -import { - CalculateCenterBetweenPointsHandler, - CalculateCenterBetweenPointsRequest, -} from './calculate-center-between-points'; -import { - CalculateConnectionCenterHandler, - CalculateConnectionCenterRequest, -} from './calculate-connection-center'; -import { - IFConnectionBuilder, - IFConnectionBuilderRequest, - IFConnectionBuilderResponse, -} from '../../f-connection-builder'; -import { IMap } from '../../../domain'; - -const CONNECTOR_SIDE_POINT: IMap = { - [EFConnectableSide.LEFT]: PointExtensions.initialize(-1, 0), - - [EFConnectableSide.RIGHT]: PointExtensions.initialize(1, 0), - - [EFConnectableSide.TOP]: PointExtensions.initialize(0, -1), - - [EFConnectableSide.BOTTOM]: PointExtensions.initialize(0, 1), - - [EFConnectableSide.AUTO]: PointExtensions.initialize(0, 0), -}; - -export class FSegmentPathBuilder implements IFConnectionBuilder { - public handle(request: IFConnectionBuilderRequest): IFConnectionBuilderResponse { - const { source, sourceSide, target, targetSide } = request; - - const { points, center } = this._getPathPoints( - source, - sourceSide, - target, - targetSide, - request.offset, - ); - - const path = this._buildPath(points, request.radius); - - const penultimatePoint = points.length > 1 ? points[points.length - 2] : source; - const secondPoint = points.length > 1 ? points[1] : target; - - return { - path, - connectionCenter: center, - penultimatePoint, - secondPoint, - points, - }; - } - - private _getPathPoints( - source: IPoint, - sourceSide: EFConnectableSide, - target: IPoint, - targetSide: EFConnectableSide, - offset: number, - ): { points: IPoint[]; center: IPoint } { - const sourceDirection = CONNECTOR_SIDE_POINT[sourceSide]; - const targetDirection = CONNECTOR_SIDE_POINT[targetSide]; - - const sourceGap: IPoint = { - x: source.x + sourceDirection.x * offset, - y: source.y + sourceDirection.y * offset, - }; - const targetGap: IPoint = { - x: target.x + targetDirection.x * offset, - y: target.y + targetDirection.y * offset, - }; - - const direction = this._getDirection(sourceGap, sourceSide, targetGap); - const directionAccessor = direction.x !== 0 ? 'x' : 'y'; - const currentDirection = direction[directionAccessor]; - - let points: IPoint[] = []; - const sourceGapOffset = PointExtensions.initialize(); - const targetGapOffset = PointExtensions.initialize(); - - const centerBetweenPoints = new CalculateCenterBetweenPointsHandler().handle( - new CalculateCenterBetweenPointsRequest(source, target), - ); - - if (sourceDirection[directionAccessor] * targetDirection[directionAccessor] === -1) { - const verticalSplit: IPoint[] = [ - { x: centerBetweenPoints.x, y: sourceGap.y }, - { x: centerBetweenPoints.x, y: targetGap.y }, - ]; - const horizontalSplit: IPoint[] = [ - { x: sourceGap.x, y: centerBetweenPoints.y }, - { x: targetGap.x, y: centerBetweenPoints.y }, - ]; - - if (sourceDirection[directionAccessor] === currentDirection) { - points = directionAccessor === 'x' ? verticalSplit : horizontalSplit; - } else { - points = directionAccessor === 'x' ? horizontalSplit : verticalSplit; - } - } else { - const sourceTarget: IPoint[] = [{ x: sourceGap.x, y: targetGap.y }]; - const targetSource: IPoint[] = [{ x: targetGap.x, y: sourceGap.y }]; - - if (directionAccessor === 'x') { - points = sourceDirection.x === currentDirection ? targetSource : sourceTarget; - } else { - points = sourceDirection.y === currentDirection ? sourceTarget : targetSource; - } - - if (sourceSide === targetSide) { - const diff = Math.abs(source[directionAccessor] - target[directionAccessor]); - - if (diff <= offset) { - const gapOffset = Math.min(offset - 1, offset - diff); - if (sourceDirection[directionAccessor] === currentDirection) { - sourceGapOffset[directionAccessor] = - (sourceGap[directionAccessor] > source[directionAccessor] ? -1 : 1) * gapOffset; - } else { - targetGapOffset[directionAccessor] = - (targetGap[directionAccessor] > target[directionAccessor] ? -1 : 1) * gapOffset; - } - } - } - - if (sourceSide !== targetSide) { - const dirAccessorOpposite = directionAccessor === 'x' ? 'y' : 'x'; - const isSameDir = - sourceDirection[directionAccessor] === targetDirection[dirAccessorOpposite]; - const sourceGtTargetOppo = sourceGap[dirAccessorOpposite] > targetGap[dirAccessorOpposite]; - const sourceLtTargetOppo = sourceGap[dirAccessorOpposite] < targetGap[dirAccessorOpposite]; - const flipSourceTarget = - (sourceDirection[directionAccessor] === 1 && - ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) || - (sourceDirection[directionAccessor] !== 1 && - ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo))); - - if (flipSourceTarget) { - points = directionAccessor === 'x' ? sourceTarget : targetSource; - } - } - } - - const pathPoints = [ - source, - { x: sourceGap.x + sourceGapOffset.x, y: sourceGap.y + sourceGapOffset.y }, - ...points, - { x: targetGap.x + targetGapOffset.x, y: targetGap.y + targetGapOffset.y }, - target, - ]; - - const center = new CalculateConnectionCenterHandler().handle( - new CalculateConnectionCenterRequest(pathPoints), - ); - - return { points: pathPoints, center }; - } - - private _getDirection(source: IPoint, sourceSide: EFConnectableSide, target: IPoint): IPoint { - if (sourceSide === EFConnectableSide.LEFT || sourceSide === EFConnectableSide.RIGHT) { - return source.x < target.x - ? PointExtensions.initialize(1, 0) - : PointExtensions.initialize(-1, 0); - } - - return source.y < target.y - ? PointExtensions.initialize(0, 1) - : PointExtensions.initialize(0, -1); - } - - private _distance(a: IPoint, b: IPoint): number { - return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); - } - - private _buildPath(points: IPoint[], borderRadius: number): string { - let path = ''; - for (let i = 0; i < points.length; i++) { - const p = points[i]; - let segment = ''; - - if (i > 0 && i < points.length - 1) { - segment = this._getBend(points[i - 1], p, points[i + 1], borderRadius); - } else if (i === points.length - 1) { - segment = this._buildLastLineSegment(i, p); - } else { - segment = this._buildMoveOrLineSegment(i, p); - } - path += segment; - } - - return path; - } - - private _getBend(a: IPoint, b: IPoint, c: IPoint, size: number): string { - const bendSize = Math.min(this._distance(a, b) / 2, this._distance(b, c) / 2, size); - const { x, y } = b; - - if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) { - return `L${x} ${y}`; - } - - if (a.y === y) { - const xDir = a.x < c.x ? -1 : 1; - const yDir = a.y < c.y ? 1 : -1; - - return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`; - } - - const xDir = a.x < c.x ? 1 : -1; - const yDir = a.y < c.y ? -1 : 1; - - return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`; - } - - private _buildMoveOrLineSegment(index: number, point: IPoint): string { - return `${index === 0 ? 'M' : 'L'}${point.x} ${point.y}`; - } - - private _buildLastLineSegment(index: number, point: IPoint): string { - return `L${point.x + 0.0002} ${point.y + 0.0002}`; - } -} diff --git a/projects/f-flow/src/f-connection/common/domain/f-straight.path-builder.spec.ts b/projects/f-flow/src/f-connection/common/domain/f-straight.path-builder.spec.ts deleted file mode 100644 index 6acbad02..00000000 --- a/projects/f-flow/src/f-connection/common/domain/f-straight.path-builder.spec.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { FStraightPathBuilder } from './f-straight.path-builder'; -import { IFConnectionBuilderRequest, IFConnectionBuilderResponse } from '../../f-connection-builder'; -import { EFConnectableSide } from '../../../f-connectors'; - - -describe('FStraightPathBuilder', () => { - let builder: FStraightPathBuilder; - - beforeEach(() => { - builder = new FStraightPathBuilder(); - }); - - it('should build a straight path and calculate the center for a horizontal connection', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 100, y: 0 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.LEFT, - radius: 0, - offset: 0, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBe('M 0 0 L 100.0002 0.0002'); - expect(response.connectionCenter).toEqual({ x: 50, y: 0 }); - }); - - it('should build a straight path and calculate the center for a vertical connection', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 0, y: 100 }, - sourceSide: EFConnectableSide.BOTTOM, - targetSide: EFConnectableSide.TOP, - radius: 0, - offset: 0, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBe('M 0 0 L 0.0002 100.0002'); - expect(response.connectionCenter).toEqual({ x: 0, y: 50 }); - }); - - it('should build a straight path and calculate the center for a diagonal connection', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 0, y: 0 }, - target: { x: 100, y: 100 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.BOTTOM, - radius: 0, - offset: 0, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBe('M 0 0 L 100.0002 100.0002'); - expect(response.connectionCenter).toEqual({ x: 50, y: 50 }); - }); - - it('should build a straight path and calculate the center for a connection with offset', () => { - const request: IFConnectionBuilderRequest = { - source: { x: 10, y: 20 }, - target: { x: 110, y: 120 }, - sourceSide: EFConnectableSide.RIGHT, - targetSide: EFConnectableSide.BOTTOM, - radius: 0, - offset: 0, - }; - - const response: IFConnectionBuilderResponse = builder.handle(request); - - expect(response.path).toBe('M 10 20 L 110.0002 120.0002'); - expect(response.connectionCenter).toEqual({ x: 60, y: 70 }); - }); -}); diff --git a/projects/f-flow/src/f-connection/common/domain/f-straight.path-builder.ts b/projects/f-flow/src/f-connection/common/domain/f-straight.path-builder.ts deleted file mode 100644 index e726e4c4..00000000 --- a/projects/f-flow/src/f-connection/common/domain/f-straight.path-builder.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - CalculateConnectionCenterHandler, - CalculateConnectionCenterRequest, -} from './calculate-connection-center'; -import { - IFConnectionBuilder, - IFConnectionBuilderRequest, - IFConnectionBuilderResponse, -} from '../../f-connection-builder'; - -export class FStraightPathBuilder implements IFConnectionBuilder { - public handle(request: IFConnectionBuilderRequest): IFConnectionBuilderResponse { - const { source, target } = request; - const path = `M ${source.x} ${source.y} L ${target.x + 0.0002} ${target.y + 0.0002}`; - - const connectionCenter = new CalculateConnectionCenterHandler().handle( - new CalculateConnectionCenterRequest([source, target]), - ); - - return { - path, - connectionCenter, - penultimatePoint: source, - secondPoint: target, - points: [source, connectionCenter, target], - }; - } -} diff --git a/projects/f-flow/src/f-connection/common/domain/index.ts b/projects/f-flow/src/f-connection/common/domain/index.ts deleted file mode 100644 index cfeb7794..00000000 --- a/projects/f-flow/src/f-connection/common/domain/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export * from './calculate-center-between-points'; - -export * from './calculate-connection-center'; - -export * from './adaptive-curve-builder'; - -export * from './f-bezier.path-builder'; - -export * from './f-segment.path-builder'; - -export * from './f-straight.path-builder'; diff --git a/projects/f-flow/src/f-connection/common/f-connection-identifiers.ts b/projects/f-flow/src/f-connection/common/f-connection-identifiers.ts deleted file mode 100644 index d170d4ad..00000000 --- a/projects/f-flow/src/f-connection/common/f-connection-identifiers.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { normalizeDomElementId } from '@foblex/utils'; - -export const F_CONNECTION_IDENTIFIERS = { - - textId(connectionId: string): string { - return normalizeDomElementId('connection_text_' + connectionId); - }, - connectionForSelectionId(connectionId: string): string { - return normalizeDomElementId('connection_for_selection_' + connectionId); - }, - connectionId(connectionId: string): string { - return normalizeDomElementId('connection_' + connectionId); - }, - gradientId(connectionId: string): string { - return normalizeDomElementId('connection_gradient_' + connectionId); - }, - linkToGradient(connectionId: string): string { - return `url(#${ F_CONNECTION_IDENTIFIERS.gradientId(connectionId) })`; - }, - linkToConnection(connectionId: string): string { - return `#${ F_CONNECTION_IDENTIFIERS.connectionId(connectionId) }`; - }, -} diff --git a/projects/f-flow/src/f-connection/common/f-connection-text/f-connection-text-path.directive.ts b/projects/f-flow/src/f-connection/common/f-connection-text/f-connection-text-path.directive.ts deleted file mode 100644 index b29c0cca..00000000 --- a/projects/f-flow/src/f-connection/common/f-connection-text/f-connection-text-path.directive.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Directive, ElementRef, inject, OnInit } from '@angular/core'; -import { F_CONNECTION_IDENTIFIERS } from '../f-connection-identifiers'; -import { F_CONNECTION } from '../f-connection.injection-token'; -import { IHasConnectionText } from '../i-has-connection-text'; -import { IHasConnectionFromTo } from '../i-has-connection-from-to'; -import { BrowserService } from '@foblex/platform'; -import { IHasHostElement } from '../../../i-has-host-element'; - -@Directive({ - selector: 'textPath[f-connection-text-path]', - host: { - '[attr.href]': 'linkToConnection', - }, -}) -export class FConnectionTextPathDirective implements IHasHostElement, OnInit { - public readonly hostElement = inject(ElementRef).nativeElement; - private readonly _base = inject(F_CONNECTION) as IHasConnectionText & IHasConnectionFromTo; - private readonly _browser = inject(BrowserService); - - public get linkToConnection(): string { - return F_CONNECTION_IDENTIFIERS.linkToConnection( - this._base.fId() + this._base.fOutputId() + this._base.fInputId(), - ); - } - - public symbolWidth: number = 8; - public fontSize: string = '12px'; - - public ngOnInit(): void { - this.hostElement.setAttribute('text-anchor', `middle`); - this.symbolWidth = this._getSymbolWidth(this._base.fText || ''); - } - - public getBBox(): DOMRect { - return this.hostElement.getBBox(); - } - - public redraw(): void { - this.hostElement.setAttribute('startOffset', this._base.fTextStartOffset || '50%'); - } - - private _getFontStyles(element: SVGTextPathElement): { fontSize: string; fontFamily: string } { - const computedStyles = this._browser.window.getComputedStyle(element); - - return { - fontSize: computedStyles.fontSize, - fontFamily: computedStyles.fontFamily, - }; - } - - private _getSymbolWidth(name: string): number { - const text = name || 'connection'; - const { fontFamily, fontSize } = this._getFontStyles(this.hostElement); - this.fontSize = fontSize || '12px'; - const canvas = this._browser.document.createElement('canvas'); - let context; - - try { - context = canvas.getContext('2d'); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - context = null; - } - if (!context) { - return 0; - } - - context.font = `${fontSize} ${fontFamily}`; - - const metrics = context.measureText(text); - const symbolWidth = metrics.width / text.length; - - return symbolWidth; - } -} diff --git a/projects/f-flow/src/f-connection/common/f-connection-text/f-connection-text.component.html b/projects/f-flow/src/f-connection/common/f-connection-text/f-connection-text.component.html deleted file mode 100644 index d75bc3ae..00000000 --- a/projects/f-flow/src/f-connection/common/f-connection-text/f-connection-text.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - - {{ text }} - - diff --git a/projects/f-flow/src/f-connection/common/f-connection-text/f-connection-text.component.ts b/projects/f-flow/src/f-connection/common/f-connection-text/f-connection-text.component.ts deleted file mode 100644 index a84564da..00000000 --- a/projects/f-flow/src/f-connection/common/f-connection-text/f-connection-text.component.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef, inject, viewChild } from '@angular/core'; -import { ILine, PointExtensions } from '@foblex/2d'; -import { FConnectionTextPathDirective } from './f-connection-text-path.directive'; -import { F_CONNECTION_IDENTIFIERS } from '../f-connection-identifiers'; -import { IHasConnectionText } from '../i-has-connection-text'; -import { IHasConnectionFromTo } from '../i-has-connection-from-to'; -import { F_CONNECTION } from '../f-connection.injection-token'; -import { CONNECTION_TEXT, IConnectionText } from './i-connection-text'; - -@Component({ - selector: 'text[f-connection-text]', - templateUrl: './f-connection-text.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - host: { - class: 'f-component f-connection-text', - '[attr.id]': 'textId', - }, - providers: [{ provide: CONNECTION_TEXT, useExisting: FConnectionTextComponent }], -}) -export class FConnectionTextComponent implements IConnectionText { - public readonly hostElement = inject(ElementRef).nativeElement; - private readonly _base = inject(F_CONNECTION) as IHasConnectionText & IHasConnectionFromTo; - - private readonly _textPathDirective = viewChild.required(FConnectionTextPathDirective); - - public get textId(): string { - return F_CONNECTION_IDENTIFIERS.textId( - this._base.fId() + this._base.fOutputId() + this._base.fInputId(), - ); - } - - public get text(): string { - return this._base.fText || ''; - } - - public redraw(line: ILine): void { - this._textPathDirective().redraw(); - const isTextReverse: boolean = FConnectionTextComponent._isTextReverse(line); - const dyValue = this._calculateDy(this._textPathDirective().fontSize, isTextReverse); - - this.hostElement.setAttribute('dy', dyValue); - - const textRect = this._textPathDirective().getBBox(); - const textRectCenter = [textRect.x + textRect.width / 2, textRect.y + textRect.height / 2]; - this.hostElement.setAttribute( - 'transform', - isTextReverse ? `rotate(180, ${textRectCenter})` : '', - ); - const startOffset = FConnectionTextComponent._getTextStartOffset( - line, - this._base.fText || '', - this._textPathDirective().symbolWidth, - ); - if (startOffset < 0) { - this.hostElement.style.display = 'none'; - } else { - this.hostElement.style.display = 'unset'; - } - } - - private _calculateDy(fontSize: string, isTextReverse: boolean): string { - const fontSizeNumber = parseFloat(fontSize); - - const dyValue = isTextReverse ? fontSizeNumber * 1.5 : fontSizeNumber * -0.8; - - return dyValue.toString(); - } - - private static _isTextReverse(line: ILine): boolean { - return line.point1.x > line.point2.x; - } - - private static _getTextStartOffset(line: ILine, name: string, symbolWidth: number): number { - const vectorLength: number = PointExtensions.hypotenuse(line.point1, line.point2); - - return vectorLength / 2 - ((name || '').length * symbolWidth) / 2; - } -} diff --git a/projects/f-flow/src/f-connection/common/f-connection-text/i-connection-text.ts b/projects/f-flow/src/f-connection/common/f-connection-text/i-connection-text.ts deleted file mode 100644 index 4057d32e..00000000 --- a/projects/f-flow/src/f-connection/common/f-connection-text/i-connection-text.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ILine } from '@foblex/2d'; -import { InjectionToken } from '@angular/core'; -import { IHasHostElement } from '../../../i-has-host-element'; - -export const CONNECTION_TEXT = new InjectionToken('CONNECTION_TEXT'); - -export interface IConnectionText extends IHasHostElement { - - redraw(line: ILine): void; -} diff --git a/projects/f-flow/src/f-connection/common/f-connection-text/index.ts b/projects/f-flow/src/f-connection/common/f-connection-text/index.ts deleted file mode 100644 index cd48d5b5..00000000 --- a/projects/f-flow/src/f-connection/common/f-connection-text/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './f-connection-text.component'; - -export * from './f-connection-text-path.directive'; - -export * from './i-connection-text'; diff --git a/projects/f-flow/src/f-connection/common/f-connection.injection-token.ts b/projects/f-flow/src/f-connection/common/f-connection.injection-token.ts deleted file mode 100644 index a973ea22..00000000 --- a/projects/f-flow/src/f-connection/common/f-connection.injection-token.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -export const F_CONNECTION = new InjectionToken('F_CONNECTION'); diff --git a/projects/f-flow/src/f-connection/common/f-drag-handle/f-connection-drag-handle-end.component.ts b/projects/f-flow/src/f-connection/common/f-drag-handle/f-connection-drag-handle-end.component.ts deleted file mode 100644 index 18027bc9..00000000 --- a/projects/f-flow/src/f-connection/common/f-drag-handle/f-connection-drag-handle-end.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, ElementRef, inject, -} from "@angular/core"; -import { IPoint } from '@foblex/2d'; -import { IHasHostElement } from '../../../i-has-host-element'; -import { F_CSS_CLASS } from '../../../domain/css-cls'; - -@Component({ - selector: "circle[f-connection-drag-handle-end]", - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, - host: { - '[class]': 'class', - }, -}) -export class FConnectionDragHandleEndComponent implements IHasHostElement { - private readonly _elementReference = inject(ElementRef); - - protected readonly class: string = F_CSS_CLASS.CONNECTION.DRAG_HANDLE; - - public point!: IPoint; - - public get hostElement(): SVGCircleElement { - return this._elementReference.nativeElement; - } - - public redraw(penultimatePoint: IPoint, endPoint: IPoint): void { - this.point = this._calculateCircleCenter(penultimatePoint, endPoint, 8); - this.hostElement.setAttribute('cx', this.point.x.toString()); - this.hostElement.setAttribute('cy', this.point.y.toString()); - } - - private _calculateCircleCenter(start: IPoint, end: IPoint, radius: number): IPoint { - const direction = { x: end.x - start.x, y: end.y - start.y }; - const length = Math.sqrt(direction.x * direction.x + direction.y * direction.y) || 1; - const unitDirection = { x: direction.x / length, y: direction.y / length }; - const scaledDirection = { x: unitDirection.x * radius, y: unitDirection.y * radius }; - - return { x: end.x - scaledDirection.x, y: end.y - scaledDirection.y }; - } -} - diff --git a/projects/f-flow/src/f-connection/common/f-drag-handle/f-connection-drag-handle-start.component.ts b/projects/f-flow/src/f-connection/common/f-drag-handle/f-connection-drag-handle-start.component.ts deleted file mode 100644 index df140dbf..00000000 --- a/projects/f-flow/src/f-connection/common/f-drag-handle/f-connection-drag-handle-start.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, ElementRef, inject, -} from "@angular/core"; -import { IPoint } from '@foblex/2d'; -import { IHasHostElement } from '../../../i-has-host-element'; -import { F_CSS_CLASS } from '../../../domain/css-cls'; - -@Component({ - selector: "circle[f-connection-drag-handle-start]", - template: '', - changeDetection: ChangeDetectionStrategy.OnPush, - host: { - '[class]': 'class', - }, -}) -export class FConnectionDragHandleStartComponent implements IHasHostElement { - private readonly _elementReference = inject(ElementRef); - - protected readonly class: string = F_CSS_CLASS.CONNECTION.DRAG_HANDLE; - - public point!: IPoint; - - public get hostElement(): SVGCircleElement { - return this._elementReference.nativeElement; - } - - public redraw(penultimatePoint: IPoint, startPoint: IPoint): void { - this.point = this._calculateCircleCenter(penultimatePoint, startPoint, 8); - this.hostElement.setAttribute('cx', this.point.x.toString()); - this.hostElement.setAttribute('cy', this.point.y.toString()); - } - - private _calculateCircleCenter(start: IPoint, end: IPoint, radius: number): IPoint { - const direction = { x: end.x - start.x, y: end.y - start.y }; - const length = Math.sqrt(direction.x * direction.x + direction.y * direction.y) || 1; - const unitDirection = { x: direction.x / length, y: direction.y / length }; - const scaledDirection = { x: unitDirection.x * radius, y: unitDirection.y * radius }; - - return { x: end.x - scaledDirection.x, y: end.y - scaledDirection.y }; - } -} - diff --git a/projects/f-flow/src/f-connection/common/f-drag-handle/index.ts b/projects/f-flow/src/f-connection/common/f-drag-handle/index.ts deleted file mode 100644 index 7ac93ea5..00000000 --- a/projects/f-flow/src/f-connection/common/f-drag-handle/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './f-connection-drag-handle-start.component'; - -export * from './f-connection-drag-handle-end.component'; diff --git a/projects/f-flow/src/f-connection/common/f-gradient/i-connection-gradient.ts b/projects/f-flow/src/f-connection/common/f-gradient/i-connection-gradient.ts deleted file mode 100644 index e675df6e..00000000 --- a/projects/f-flow/src/f-connection/common/f-gradient/i-connection-gradient.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ILine } from '@foblex/2d'; -import { InjectionToken } from '@angular/core'; -import { IHasHostElement } from '../../../i-has-host-element'; - -export const CONNECTION_GRADIENT = new InjectionToken('CONNECTION_GRADIENT'); - -export interface IConnectionGradient extends IHasHostElement { - - initialize(): void; - - redraw(line: ILine): void; -} diff --git a/projects/f-flow/src/f-connection/common/f-gradient/index.ts b/projects/f-flow/src/f-connection/common/f-gradient/index.ts deleted file mode 100644 index 959de1c6..00000000 --- a/projects/f-flow/src/f-connection/common/f-gradient/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './f-connection-gradient.component'; -export * from './i-connection-gradient'; diff --git a/projects/f-flow/src/f-connection/common/f-path/f-connection-path.component.ts b/projects/f-flow/src/f-connection/common/f-path/f-connection-path.component.ts deleted file mode 100644 index 5662991b..00000000 --- a/projects/f-flow/src/f-connection/common/f-path/f-connection-path.component.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef, inject } from '@angular/core'; -import { F_CONNECTION_IDENTIFIERS } from '../f-connection-identifiers'; -import { IHasConnectionColor } from '../i-has-connection-color'; -import { IHasConnectionFromTo } from '../i-has-connection-from-to'; -import { F_CONNECTION } from '../f-connection.injection-token'; -import { CONNECTION_PATH, IConnectionPath } from './i-connection-path'; -import { - getMarkerEndId, - getMarkerSelectedEndId, - getMarkerSelectedStartId, - getMarkerStartId, -} from './get-path-marker-id'; - -@Component({ - selector: 'path[f-connection-path]', - template: '', - styleUrls: ['./f-connection-path.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { - class: 'f-component f-connection-path', - '[attr.id]': 'attrConnectionId', - '[attr.data-f-path-id]': 'fPathId', - '[attr.stroke]': 'linkToGradient', - }, - providers: [{ provide: CONNECTION_PATH, useExisting: FConnectionPathComponent }], -}) -export class FConnectionPathComponent implements IConnectionPath { - public readonly hostElement = inject(ElementRef).nativeElement; - private readonly _base = inject(F_CONNECTION) as IHasConnectionColor & IHasConnectionFromTo; - - public get fPathId(): string { - return this._base.fId(); - } - - public get linkToGradient(): string { - return F_CONNECTION_IDENTIFIERS.linkToGradient( - this._base.fId() + this._base.fOutputId() + this._base.fInputId(), - ); - } - - public get attrConnectionId(): string { - return F_CONNECTION_IDENTIFIERS.connectionId( - this._base.fId() + this._base.fOutputId() + this._base.fInputId(), - ); - } - - public initialize(): void { - this.deselect(); - } - - public setPath(path: string): void { - this.hostElement.setAttribute('d', `${path}`); - } - - public select(): void { - this.hostElement.setAttribute( - 'marker-start', - `url(#${getMarkerSelectedStartId(this._base.fId())})`, - ); - this.hostElement.setAttribute( - 'marker-end', - `url(#${getMarkerSelectedEndId(this._base.fId())})`, - ); - } - - public deselect(): void { - this.hostElement.setAttribute('marker-start', `url(#${getMarkerStartId(this._base.fId())})`); - this.hostElement.setAttribute('marker-end', `url(#${getMarkerEndId(this._base.fId())})`); - } -} diff --git a/projects/f-flow/src/f-connection/common/f-path/get-path-marker-id.ts b/projects/f-flow/src/f-connection/common/f-path/get-path-marker-id.ts deleted file mode 100644 index 46c35928..00000000 --- a/projects/f-flow/src/f-connection/common/f-path/get-path-marker-id.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { normalizeDomElementId } from '@foblex/utils'; - -export function getMarkerStartId(fConnectionId: string): string { - return normalizeDomElementId(`f-connection-marker-start-${ fConnectionId }`); -} - -export function getMarkerEndId(fConnectionId: string): string { - return normalizeDomElementId(`f-connection-marker-end-${ fConnectionId }`); -} - -export function getMarkerSelectedStartId(fConnectionId: string): string { - return normalizeDomElementId(`f-connection-selected-marker-start-${ fConnectionId }`); -} - -export function getMarkerSelectedEndId(fConnectionId: string): string { - return normalizeDomElementId(`f-connection-selected-marker-end-${ fConnectionId }`); -} diff --git a/projects/f-flow/src/f-connection/common/f-path/i-connection-path.ts b/projects/f-flow/src/f-connection/common/f-path/i-connection-path.ts deleted file mode 100644 index 53eb0c9c..00000000 --- a/projects/f-flow/src/f-connection/common/f-path/i-connection-path.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { InjectionToken } from '@angular/core'; -import { IHasHostElement } from '../../../i-has-host-element'; - -export const CONNECTION_PATH = new InjectionToken('CONNECTION_PATH'); - -export interface IConnectionPath extends IHasHostElement { - - initialize(): void; - - setPath(path: string): void; - - select(): void; - - deselect(): void; -} diff --git a/projects/f-flow/src/f-connection/common/f-path/index.ts b/projects/f-flow/src/f-connection/common/f-path/index.ts deleted file mode 100644 index 2767790a..00000000 --- a/projects/f-flow/src/f-connection/common/f-path/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './f-connection-path.component'; - -export * from './get-path-marker-id'; - -export * from './i-connection-path'; diff --git a/projects/f-flow/src/f-connection/common/f-selection/f-connection-selection.component.ts b/projects/f-flow/src/f-connection/common/f-selection/f-connection-selection.component.ts deleted file mode 100644 index 65e1af41..00000000 --- a/projects/f-flow/src/f-connection/common/f-selection/f-connection-selection.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ChangeDetectionStrategy, Component, ElementRef, inject } from '@angular/core'; -import { F_CONNECTION_IDENTIFIERS } from '../f-connection-identifiers'; -import { IHasConnectionFromTo } from '../i-has-connection-from-to'; -import { F_CONNECTION } from '../f-connection.injection-token'; -import { IHasHostElement } from '../../../i-has-host-element'; - -@Component({ - selector: 'path[fConnectionSelection]', - template: '', - styleUrls: ['./f-connection-selection.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, - host: { - class: 'f-component f-connection-selection', - '[attr.id]': 'connectionForSelectionId', - }, -}) -export class FConnectionSelectionComponent implements IHasHostElement { - public readonly hostElement = inject(ElementRef).nativeElement; - private readonly _base = inject(F_CONNECTION) as IHasConnectionFromTo; - - public get connectionForSelectionId(): string { - return F_CONNECTION_IDENTIFIERS.connectionForSelectionId( - this._base.fId() + this._base.fOutputId() + this._base.fInputId(), - ); - } - - public setPath(path: string) { - this.hostElement.setAttribute('d', `${path}`); - } -} diff --git a/projects/f-flow/src/f-connection/common/f-selection/index.ts b/projects/f-flow/src/f-connection/common/f-selection/index.ts deleted file mode 100644 index b0a3d85c..00000000 --- a/projects/f-flow/src/f-connection/common/f-selection/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './f-connection-selection.component'; diff --git a/projects/f-flow/src/f-connection/common/i-has-connection-color.ts b/projects/f-flow/src/f-connection/common/i-has-connection-color.ts deleted file mode 100644 index 6d03eec8..00000000 --- a/projects/f-flow/src/f-connection/common/i-has-connection-color.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Signal } from "@angular/core"; - -export interface IHasConnectionColor { - - fStartColor: Signal; - - fEndColor: Signal; -} diff --git a/projects/f-flow/src/f-connection/common/i-has-connection-from-to.ts b/projects/f-flow/src/f-connection/common/i-has-connection-from-to.ts deleted file mode 100644 index 0cec1660..00000000 --- a/projects/f-flow/src/f-connection/common/i-has-connection-from-to.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Signal } from '@angular/core'; - -export interface IHasConnectionFromTo { - fId: Signal; - - fOutputId: Signal; - - fInputId: Signal; -} diff --git a/projects/f-flow/src/f-connection/common/i-has-connection-text.ts b/projects/f-flow/src/f-connection/common/i-has-connection-text.ts deleted file mode 100644 index 0628db70..00000000 --- a/projects/f-flow/src/f-connection/common/i-has-connection-text.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface IHasConnectionText { - - fText: string; - - fTextStartOffset: string; -} diff --git a/projects/f-flow/src/f-connection/common/index.ts b/projects/f-flow/src/f-connection/common/index.ts deleted file mode 100644 index 0082dece..00000000 --- a/projects/f-flow/src/f-connection/common/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export * from './domain'; - -export * from './f-connection-text'; - -export * from './f-drag-handle'; - -export * from './f-gradient'; - -export * from './f-path'; - -export * from './f-selection'; - -export * from './f-connection-base'; - -export * from './e-f-connection-behavior'; - -export * from './e-f-connection-connectable-side'; - -export * from './e-f-connection-type'; - -export * from './f-connection-identifiers'; - -export * from './i-has-connection-color'; - -export * from './i-has-connection-from-to'; - -export * from './i-has-connection-text'; diff --git a/projects/f-flow/src/f-connection/f-connection-builder/f-connection-builders.ts b/projects/f-flow/src/f-connection/f-connection-builder/f-connection-builders.ts deleted file mode 100644 index 6bf05b7c..00000000 --- a/projects/f-flow/src/f-connection/f-connection-builder/f-connection-builders.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { InjectionToken } from '@angular/core'; -import { IFConnectionBuilder } from './i-f-connection-builder'; - -export const F_CONNECTION_BUILDERS = new InjectionToken('F_CONNECTION_BUILDERS'); - -export type IFConnectionBuilders = Record; diff --git a/projects/f-flow/src/f-connection/f-connection-builder/f-connection-factory.ts b/projects/f-flow/src/f-connection/f-connection-builder/f-connection-factory.ts deleted file mode 100644 index b24a1e97..00000000 --- a/projects/f-flow/src/f-connection/f-connection-builder/f-connection-factory.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { inject, Injectable } from '@angular/core'; -import { IFConnectionBuilder } from './i-f-connection-builder'; -import { - AdaptiveCurveBuilder, - EFConnectionType, - FBezierPathBuilder, - FSegmentPathBuilder, - FStraightPathBuilder, -} from '../common'; -import { F_CONNECTION_BUILDERS } from './f-connection-builders'; -import { IFConnectionFactoryRequest } from './i-f-connection-factory-request'; -import { IFConnectionBuilderResponse } from './i-f-connection-builder-response'; -import { IMap } from '../../domain'; - -@Injectable() -export class FConnectionFactory { - private readonly _builders: IMap = { - [EFConnectionType.STRAIGHT]: new FStraightPathBuilder(), - - [EFConnectionType.BEZIER]: new FBezierPathBuilder(), - - [EFConnectionType.ADAPTIVE_CURVE]: new AdaptiveCurveBuilder(), - - [EFConnectionType.SEGMENT]: new FSegmentPathBuilder(), - - ...(inject(F_CONNECTION_BUILDERS, { optional: true }) || {}), - }; - - public handle(request: IFConnectionFactoryRequest): IFConnectionBuilderResponse { - const builder = this._builders[request.type]; - if (!builder) { - throw new Error(`FConnectionBuilder not found for type ${request.type}`); - } - - return builder.handle(request.payload); - } -} diff --git a/projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-factory-request.ts b/projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-factory-request.ts deleted file mode 100644 index 4a5df705..00000000 --- a/projects/f-flow/src/f-connection/f-connection-builder/i-f-connection-factory-request.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EFConnectionType } from '../common'; -import { IFConnectionBuilderRequest } from './i-f-connection-builder-request'; - -export interface IFConnectionFactoryRequest { - - type: EFConnectionType | string; - - payload: IFConnectionBuilderRequest; -} diff --git a/projects/f-flow/src/f-connection/f-connection-center/f-connection-center.directive.ts b/projects/f-flow/src/f-connection/f-connection-center/f-connection-center.directive.ts deleted file mode 100644 index eac39453..00000000 --- a/projects/f-flow/src/f-connection/f-connection-center/f-connection-center.directive.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { booleanAttribute, Directive, input, isDevMode } from '@angular/core'; - -let warnedFConnectionCenter = false; -/** @deprecated '[fConnectionCenter] is deprecated and will be removed in v18.0.0. Use FConnectionContent directive instead.' */ -@Directive({ - selector: '[fConnectionCenter]', -}) -export class FConnectionCenterDirective { - /** - * @deprecated '[fConnectionCenter] is deprecated and will be removed in v18.0.0. Use FConnectionContent directive instead.' - */ - readonly fConnectionCenter = input(true, { - alias: 'fConnectionCenter', - transform: (v: unknown) => { - // Превращаем presence-атрибут в boolean - const val = booleanAttribute(v); - if (!warnedFConnectionCenter && isDevMode()) { - warnedFConnectionCenter = true; - console.warn( - '[fConnectionCenter] is deprecated and will be removed in v18.0.0. Use [FConnectionContent] instead.', - ); - } - - return val; - }, - }); -} diff --git a/projects/f-flow/src/f-connection/f-connection-center/index.ts b/projects/f-flow/src/f-connection/f-connection-center/index.ts deleted file mode 100644 index d196be78..00000000 --- a/projects/f-flow/src/f-connection/f-connection-center/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './f-connection-center.directive'; diff --git a/projects/f-flow/src/f-connection/f-connection-content/index.ts b/projects/f-flow/src/f-connection/f-connection-content/index.ts deleted file mode 100644 index 1ff13569..00000000 --- a/projects/f-flow/src/f-connection/f-connection-content/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './polyline-content-engine'; -export * from './f-connection-content'; diff --git a/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.html b/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.html index 591040df..d0e5ef67 100644 --- a/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.html +++ b/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.html @@ -1,5 +1,6 @@ + @@ -8,16 +9,9 @@ - - -@if (fConnectionCenters().length) { -
- -
-} -@if (fConnectionContents().length) { +@if (fContents().length) { } diff --git a/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.scss b/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.scss index fc025e06..366904a9 100644 --- a/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.scss +++ b/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.scss @@ -10,7 +10,6 @@ } } - .f-connection-center, .f-connection-content { pointer-events: all; } diff --git a/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.ts b/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.ts index de82ac1c..af0e5806 100644 --- a/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.ts +++ b/projects/f-flow/src/f-connection/f-connection-for-create/f-connection-for-create.component.ts @@ -11,20 +11,20 @@ import { OnInit, signal, } from '@angular/core'; -import { EFConnectionBehavior, EFConnectionConnectableSide } from '../common'; -import { EFConnectionType } from '../common'; import { NotifyDataChangedRequest } from '../../f-storage'; -import { F_CONNECTION } from '../common/f-connection.injection-token'; -//TODO: Need to deal with cyclic dependencies, since in some cases an error occurs when importing them ../common -// TypeError: Class extends value undefined is not a constructor or null -// at f-connection-for-create.component.ts:34:11 -import { FConnectionBase } from '../common/f-connection-base'; import { castToEnum } from '@foblex/utils'; import { FMediator } from '@foblex/mediator'; import { AddConnectionForCreateToStoreRequest, RemoveConnectionForCreateFromStoreRequest, } from '../../domain'; +import { + EFConnectionBehavior, + EFConnectionConnectableSide, + EFConnectionType, + F_CONNECTION_COMPONENTS_PARENT, + FConnectionBase, +} from '../../f-connection-v2'; let uniqueId = 0; @@ -36,7 +36,9 @@ let uniqueId = 0; host: { class: 'f-component f-connection f-connection-for-create', }, - providers: [{ provide: F_CONNECTION, useExisting: FConnectionForCreateComponent }], + providers: [ + { provide: F_CONNECTION_COMPONENTS_PARENT, useExisting: FConnectionForCreateComponent }, + ], }) export class FConnectionForCreateComponent extends FConnectionBase @@ -44,10 +46,6 @@ export class FConnectionForCreateComponent { public override fId = signal(`f-connection-for-create-${uniqueId++}`); - public override fText: string = ''; - - public override fTextStartOffset: string = ''; - public override fOutputId = signal(''); public override fInputId = signal(''); diff --git a/projects/f-flow/src/f-connection/f-connection/f-connection.component.html b/projects/f-flow/src/f-connection/f-connection/f-connection.component.html index 8c6edd12..ea879063 100644 --- a/projects/f-flow/src/f-connection/f-connection/f-connection.component.html +++ b/projects/f-flow/src/f-connection/f-connection/f-connection.component.html @@ -1,4 +1,4 @@ - + @@ -11,17 +11,10 @@ } - @if (fText) { - - } -@if (fConnectionCenters().length) { -
- -
-} + -@if (fConnectionContents().length) { +@if (fContents().length) { } diff --git a/projects/f-flow/src/f-connection/f-connection/f-connection.component.scss b/projects/f-flow/src/f-connection/f-connection/f-connection.component.scss index 18d9a240..b020f2a2 100644 --- a/projects/f-flow/src/f-connection/f-connection/f-connection.component.scss +++ b/projects/f-flow/src/f-connection/f-connection/f-connection.component.scss @@ -2,6 +2,8 @@ pointer-events: none; svg { + display: block; + vertical-align: middle; overflow: visible !important; position: absolute; @@ -10,7 +12,6 @@ } } - .f-connection-center, .f-connection-content { pointer-events: all; } diff --git a/projects/f-flow/src/f-connection/f-connection/f-connection.component.ts b/projects/f-flow/src/f-connection/f-connection/f-connection.component.ts index 236e19e6..2d003704 100644 --- a/projects/f-flow/src/f-connection/f-connection/f-connection.component.ts +++ b/projects/f-flow/src/f-connection/f-connection/f-connection.component.ts @@ -10,17 +10,18 @@ import { OnDestroy, OnInit, } from '@angular/core'; -import { EFConnectionBehavior, EFConnectionConnectableSide, EFConnectionType } from '../common'; import { NotifyDataChangedRequest } from '../../f-storage'; -import { F_CONNECTION } from '../common/f-connection.injection-token'; -//TODO: Need to deal with cyclic dependencies, since in some cases an error occurs when importing them ../common -// TypeError: Class extends value undefined is not a constructor or null -// at f-connection-for-create.component.ts:34:11 -import { FConnectionBase } from '../common/f-connection-base'; import { castToEnum } from '@foblex/utils'; import { FMediator } from '@foblex/mediator'; import { AddConnectionToStoreRequest, RemoveConnectionFromStoreRequest } from '../../domain'; import { stringAttribute } from '../../utils'; +import { + EFConnectionBehavior, + EFConnectionConnectableSide, + EFConnectionType, + F_CONNECTION_COMPONENTS_PARENT, + FConnectionBase, +} from '../../f-connection-v2'; let uniqueId = 0; @@ -36,19 +37,11 @@ let uniqueId = 0; '[class.f-connection-selection-disabled]': 'fSelectionDisabled()', '[class.f-connection-reassign-disabled]': 'fDraggingDisabled()', }, - providers: [{ provide: F_CONNECTION, useExisting: FConnectionComponent }], + providers: [{ provide: F_CONNECTION_COMPONENTS_PARENT, useExisting: FConnectionComponent }], }) export class FConnectionComponent extends FConnectionBase implements OnInit, OnChanges, OnDestroy { public override fId = input(`f-connection-${uniqueId++}`, { alias: 'fConnectionId' }); - /** @deprecated [fText] is deprecated and will be removed in v18.0.0. Use FConnectionContent directive instead. */ - @Input() - public override fText: string = ''; - - /** @deprecated [fTextStartOffset] is deprecated and will be removed in v18.0.0. Use FConnectionContent directive instead. */ - @Input() - public override fTextStartOffset: string = ''; - public override fOutputId = input('', { transform: (value) => stringAttribute(value) || '', }); diff --git a/projects/f-flow/src/f-connection/f-marker/index.ts b/projects/f-flow/src/f-connection/f-marker/index.ts deleted file mode 100644 index 639cbf68..00000000 --- a/projects/f-flow/src/f-connection/f-marker/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './e-f-marker-type'; - -export * from './f-marker.directive'; - -export * from './f-marker-base'; diff --git a/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.html b/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.html index 591040df..d0e5ef67 100644 --- a/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.html +++ b/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.html @@ -1,5 +1,6 @@ + @@ -8,16 +9,9 @@ - - -@if (fConnectionCenters().length) { -
- -
-} -@if (fConnectionContents().length) { +@if (fContents().length) { } diff --git a/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.scss b/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.scss index fc025e06..366904a9 100644 --- a/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.scss +++ b/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.scss @@ -10,7 +10,6 @@ } } - .f-connection-center, .f-connection-content { pointer-events: all; } diff --git a/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.ts b/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.ts index 0ffe656c..797c4e30 100644 --- a/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.ts +++ b/projects/f-flow/src/f-connection/f-snap-connection/f-snap-connection.component.ts @@ -11,17 +11,20 @@ import { OnInit, signal, } from '@angular/core'; -import { EFConnectionBehavior, EFConnectionConnectableSide } from '../common'; -import { EFConnectionType } from '../common'; import { NotifyDataChangedRequest } from '../../f-storage'; -import { F_CONNECTION } from '../common/f-connection.injection-token'; -import { FConnectionBase } from '../common/f-connection-base'; import { castToEnum } from '@foblex/utils'; import { FMediator } from '@foblex/mediator'; import { AddSnapConnectionToStoreRequest, RemoveSnapConnectionFromStoreRequest, } from '../../domain'; +import { + EFConnectionBehavior, + EFConnectionConnectableSide, + EFConnectionType, + F_CONNECTION_COMPONENTS_PARENT, + FConnectionBase, +} from '../../f-connection-v2'; let uniqueId = 0; @@ -33,7 +36,7 @@ let uniqueId = 0; host: { class: 'f-component f-connection f-snap-connection', }, - providers: [{ provide: F_CONNECTION, useExisting: FSnapConnectionComponent }], + providers: [{ provide: F_CONNECTION_COMPONENTS_PARENT, useExisting: FSnapConnectionComponent }], }) export class FSnapConnectionComponent extends FConnectionBase @@ -41,10 +44,6 @@ export class FSnapConnectionComponent { public override fId = signal(`f-snap-connection-${uniqueId++}`); - public override fText: string = ''; - - public override fTextStartOffset: string = ''; - @Input({ transform: numberAttribute }) public fSnapThreshold: number = 20; diff --git a/projects/f-flow/src/f-connection/index.ts b/projects/f-flow/src/f-connection/index.ts index 8cf73d6a..cbc3991a 100644 --- a/projects/f-flow/src/f-connection/index.ts +++ b/projects/f-flow/src/f-connection/index.ts @@ -1,15 +1,6 @@ -export * from './common'; - -export * from './f-connection-for-create'; - export * from './f-connection'; -export * from './f-connection-builder'; - -export * from './f-connection-center'; -export * from './f-connection-content'; - -export * from './f-marker'; +export * from './f-connection-for-create'; export * from './f-snap-connection'; diff --git a/projects/f-flow/src/f-connection/providers.ts b/projects/f-flow/src/f-connection/providers.ts index 7a241fd4..9faa9542 100644 --- a/projects/f-flow/src/f-connection/providers.ts +++ b/projects/f-flow/src/f-connection/providers.ts @@ -1,28 +1,28 @@ -import { - FConnectionDragHandleEndComponent, FConnectionDragHandleStartComponent, - FConnectionGradientComponent, FConnectionPathComponent, FConnectionSelectionComponent, - FConnectionTextComponent, - FConnectionTextPathDirective, -} from './common'; import { FConnectionComponent } from './f-connection'; -import { FConnectionCenterDirective } from './f-connection-center'; import { FConnectionForCreateComponent } from './f-connection-for-create'; -import { FMarkerDirective } from './f-marker'; import { FSnapConnectionComponent } from './f-snap-connection'; +import { + FConnectionContent, + FConnectionWaypoints, + FConnectionDragHandleEnd, + FConnectionDragHandleStart, + FConnectionGradient, + FConnectionMarker, + FConnectionPath, + FConnectionSelection, +} from '../f-connection-v2'; export const F_CONNECTION_PROVIDERS = [ - - FConnectionTextComponent, - FConnectionTextPathDirective, - FConnectionDragHandleStartComponent, - FConnectionDragHandleEndComponent, - FConnectionGradientComponent, - FConnectionPathComponent, - FConnectionSelectionComponent, + FConnectionDragHandleStart, + FConnectionDragHandleEnd, + FConnectionGradient, + FConnectionPath, + FConnectionSelection, + FConnectionMarker, FConnectionComponent, - FConnectionCenterDirective, FConnectionForCreateComponent, - FMarkerDirective, FSnapConnectionComponent, ]; + +export const F_CONNECTION_IMPORTS_EXPORTS = [FConnectionContent, FConnectionWaypoints]; diff --git a/projects/f-flow/src/f-connectors/f-connector-base.ts b/projects/f-flow/src/f-connectors/f-connector-base.ts index ca4ef240..443d359c 100644 --- a/projects/f-flow/src/f-connectors/f-connector-base.ts +++ b/projects/f-flow/src/f-connectors/f-connector-base.ts @@ -1,6 +1,6 @@ -import { EFConnectableSide } from './e-f-connectable-side'; import { IHasHostElement } from '../i-has-host-element'; import { Signal } from '@angular/core'; +import { EFConnectableSide } from '../f-connection-v2/enums/e-f-connectable-side'; export abstract class FConnectorBase implements IHasHostElement { public abstract fId: Signal; diff --git a/projects/f-flow/src/f-connectors/f-node-input/f-node-input.directive.ts b/projects/f-flow/src/f-connectors/f-node-input/f-node-input.directive.ts index 71af6570..b74db220 100644 --- a/projects/f-flow/src/f-connectors/f-node-input/f-node-input.directive.ts +++ b/projects/f-flow/src/f-connectors/f-node-input/f-node-input.directive.ts @@ -11,13 +11,13 @@ import { SimpleChanges, } from '@angular/core'; import { F_NODE_INPUT, FNodeInputBase } from './f-node-input-base'; -import { EFConnectableSide } from '../e-f-connectable-side'; import { F_NODE } from '../../f-node'; import { castToEnum } from '@foblex/utils'; import { FMediator } from '@foblex/mediator'; import { AddInputToStoreRequest, F_CSS_CLASS, RemoveInputFromStoreRequest } from '../../domain'; import { FConnectorBase } from '../f-connector-base'; import { stringAttribute } from '../../utils'; +import { EFConnectableSide } from '../../f-connection-v2'; let uniqueId = 0; diff --git a/projects/f-flow/src/f-connectors/f-node-outlet/f-node-outlet.directive.ts b/projects/f-flow/src/f-connectors/f-node-outlet/f-node-outlet.directive.ts index 37c060c9..4c3ba4b6 100644 --- a/projects/f-flow/src/f-connectors/f-node-outlet/f-node-outlet.directive.ts +++ b/projects/f-flow/src/f-connectors/f-node-outlet/f-node-outlet.directive.ts @@ -10,10 +10,10 @@ import { } from '@angular/core'; import { F_NODE_OUTLET, FNodeOutletBase } from './f-node-outlet-base'; import { F_NODE } from '../../f-node'; -import { EFConnectableSide } from '../e-f-connectable-side'; import { FMediator } from '@foblex/mediator'; import { AddOutletToStoreRequest, RemoveOutletFromStoreRequest } from '../../domain'; import { stringAttribute } from '../../utils'; +import { EFConnectableSide } from '../../f-connection-v2'; let uniqueId = 0; diff --git a/projects/f-flow/src/f-connectors/f-node-output/f-node-output.directive.ts b/projects/f-flow/src/f-connectors/f-node-output/f-node-output.directive.ts index 0c0b84d5..0cf55491 100644 --- a/projects/f-flow/src/f-connectors/f-node-output/f-node-output.directive.ts +++ b/projects/f-flow/src/f-connectors/f-node-output/f-node-output.directive.ts @@ -11,13 +11,13 @@ import { SimpleChanges, } from '@angular/core'; import { FNodeOutputBase, F_NODE_OUTPUT } from './f-node-output-base'; -import { EFConnectableSide } from '../e-f-connectable-side'; import { F_NODE } from '../../f-node'; import { castToEnum } from '@foblex/utils'; import { FMediator } from '@foblex/mediator'; import { AddOutputToStoreRequest, F_CSS_CLASS, RemoveOutputFromStoreRequest } from '../../domain'; import { FConnectorBase } from '../f-connector-base'; import { stringAttribute } from '../../utils'; +import { EFConnectableSide } from '../../f-connection-v2'; let uniqueId = 0; diff --git a/projects/f-flow/src/f-connectors/f-source-connector-base.ts b/projects/f-flow/src/f-connectors/f-source-connector-base.ts index f624431c..4a50004d 100644 --- a/projects/f-flow/src/f-connectors/f-source-connector-base.ts +++ b/projects/f-flow/src/f-connectors/f-source-connector-base.ts @@ -1,6 +1,8 @@ import { FConnectorBase } from './f-connector-base'; import { FNodeInputBase } from './f-node-input'; +import { Directive } from '@angular/core'; +@Directive() export abstract class FSourceConnectorBase extends FConnectorBase { public abstract canBeConnectedInputs: string[]; diff --git a/projects/f-flow/src/f-connectors/index.ts b/projects/f-flow/src/f-connectors/index.ts index 6b9dada9..0c564da8 100644 --- a/projects/f-flow/src/f-connectors/index.ts +++ b/projects/f-flow/src/f-connectors/index.ts @@ -8,6 +8,4 @@ export * from './f-connector-base'; export * from './f-source-connector-base'; -export * from './e-f-connectable-side'; - export * from './providers'; diff --git a/projects/f-flow/src/f-draggable/domain/is-connection-under-node/is-connection-under-node.execution.ts b/projects/f-flow/src/f-draggable/domain/is-connection-under-node/is-connection-under-node.execution.ts index 0cb20e5b..cc02c46c 100644 --- a/projects/f-flow/src/f-draggable/domain/is-connection-under-node/is-connection-under-node.execution.ts +++ b/projects/f-flow/src/f-draggable/domain/is-connection-under-node/is-connection-under-node.execution.ts @@ -6,8 +6,8 @@ import { FComponentsStore } from '../../../f-storage'; import { FNodeIntersectedWithConnections } from '../../index'; import { FNodeBase } from '../../../f-node'; import { FConnectorBase } from '../../../f-connectors'; -import { FConnectionBase } from '../../../f-connection'; import { GetNormalizedConnectorRectRequest } from '../../../domain'; +import { FConnectionBase } from '../../../f-connection-v2'; @Injectable() @FExecutionRegister(IsConnectionUnderNodeRequest) diff --git a/projects/f-flow/src/f-draggable/f-connection/f-create-connection/create-preparation/create-connection-preparation.ts b/projects/f-flow/src/f-draggable/f-connection/f-create-connection/create-preparation/create-connection-preparation.ts index c899b86a..06e6ae74 100644 --- a/projects/f-flow/src/f-draggable/f-connection/f-create-connection/create-preparation/create-connection-preparation.ts +++ b/projects/f-flow/src/f-draggable/f-connection/f-create-connection/create-preparation/create-connection-preparation.ts @@ -11,8 +11,6 @@ import { FDraggableDataContext } from '../../../f-draggable-data-context'; import { FReassignConnectionPreparationRequest } from '../../f-reassign-connection'; import { isValidEventTrigger } from '../../../../domain'; import { IPointerEvent } from '../../../../drag-toolkit'; -// import { IPoint } from '@foblex/2d'; -// import { BrowserService } from '@foblex/platform'; @Injectable() @FExecutionRegister(CreateConnectionPreparationRequest) @@ -22,7 +20,6 @@ export class CreateConnectionPreparation private readonly _mediator = inject(FMediator); private readonly _store = inject(FComponentsStore); private readonly _dragContext = inject(FDraggableDataContext); - // private readonly _browser = inject(BrowserService); private _node: FNodeBase | undefined; @@ -30,10 +27,6 @@ export class CreateConnectionPreparation if (!this._isValid(request) || !this._isValidTrigger(request)) { return; } - // const elements = this._elementsFromPoint(request.event.getPosition()); - // - // const outlet = this._findOutlet(elements); - // const output = this._findOutput(elements); if (isNodeOutlet(request.event.targetElement)) { this._mediator.execute( @@ -63,16 +56,4 @@ export class CreateConnectionPreparation private _isValidTrigger(request: FReassignConnectionPreparationRequest): boolean { return isValidEventTrigger(request.event.originalEvent, request.fTrigger); } - - // private _elementsFromPoint(position: IPoint): HTMLElement[] { - // return this._browser.document.elementsFromPoint(position.x, position.y) as HTMLElement[]; - // } - // - // private _findOutlet(elements: HTMLElement[]): HTMLElement | undefined { - // return elements.find((el) => isNodeOutlet(el)); - // } - // - // private _findOutput(elements: HTMLElement[]): HTMLElement | undefined { - // return elements.find((el) => isNodeOutput(el)); - // } } diff --git a/projects/f-flow/src/f-draggable/f-connection/f-create-connection/f-create-connection.drag-handler.ts b/projects/f-flow/src/f-draggable/f-connection/f-create-connection/f-create-connection.drag-handler.ts index d4ef5a52..2603d289 100644 --- a/projects/f-flow/src/f-draggable/f-connection/f-create-connection/f-create-connection.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-connection/f-create-connection/f-create-connection.drag-handler.ts @@ -2,7 +2,6 @@ import { FDragHandlerResult, IFDragHandler } from '../../f-drag-handler'; import { CalculateClosestConnectorRequest, CalculateTargetConnectorsToConnectRequest, - CalculateConnectionLineByBehaviorRequest, GetConnectorAndRectRequest, IConnectorAndRect, IClosestConnector, @@ -10,19 +9,17 @@ import { UnmarkConnectableConnectorsRequest, } from '../../../domain'; import { FConnectionForCreateComponent, FSnapConnectionComponent } from '../../../f-connection'; -import { EFConnectableSide, FNodeOutletBase, FNodeOutputBase } from '../../../f-connectors'; +import { FNodeOutletBase, FNodeOutputBase } from '../../../f-connectors'; import { FMediator } from '@foblex/mediator'; -import { - RoundedRect, - ILine, - IPoint, - PointExtensions, - RectExtensions, - IRoundedRect, -} from '@foblex/2d'; +import { RoundedRect, IPoint, PointExtensions, RectExtensions, IRoundedRect } from '@foblex/2d'; import { FComponentsStore } from '../../../f-storage'; import { IFCreateConnectionDragResult } from './i-f-create-connection-drag-result'; import { Injector } from '@angular/core'; +import { + ConnectionBehaviourBuilder, + ConnectionBehaviourBuilderRequest, + EFConnectableSide, +} from '../../../f-connection-v2'; export class FCreateConnectionDragHandler implements IFDragHandler { public fEventType = 'create-connection'; @@ -30,6 +27,7 @@ export class FCreateConnectionDragHandler implements IFDragHandler { private readonly _result: FDragHandlerResult; private readonly _mediator: FMediator; + private readonly _connectionBehaviour: ConnectionBehaviourBuilder; private readonly _store: FComponentsStore; private readonly _toConnectorRect = new RoundedRect(); @@ -53,6 +51,7 @@ export class FCreateConnectionDragHandler implements IFDragHandler { ) { this._result = _injector.get(FDragHandlerResult); this._mediator = _injector.get(FMediator); + this._connectionBehaviour = _injector.get(ConnectionBehaviourBuilder); this._store = _injector.get(FComponentsStore); this._toConnectorRect = RoundedRect.fromRect( @@ -121,8 +120,8 @@ export class FCreateConnectionDragHandler implements IFDragHandler { } private _drawConnectionForCreate(toConnectorRect: IRoundedRect, fSide: EFConnectableSide): void { - const line = this._mediator.execute( - new CalculateConnectionLineByBehaviorRequest( + const line = this._connectionBehaviour.handle( + new ConnectionBehaviourBuilderRequest( this._fOutputWithRect.fRect, toConnectorRect, this._connection, @@ -137,8 +136,8 @@ export class FCreateConnectionDragHandler implements IFDragHandler { private _drawSnapConnection(fClosestInput: IClosestConnector | undefined): void { if (fClosestInput) { - const line = this._mediator.execute( - new CalculateConnectionLineByBehaviorRequest( + const line = this._connectionBehaviour.handle( + new ConnectionBehaviourBuilderRequest( this._fOutputWithRect.fRect, fClosestInput.fRect, this._snapConnection!, diff --git a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-connection.drag-handler.ts b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-connection.drag-handler.ts index 760f8a4f..4a47c3fa 100644 --- a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-connection.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-connection.drag-handler.ts @@ -1,6 +1,6 @@ import { FDragHandlerResult, IFDragHandler } from '../../f-drag-handler'; import { GetConnectorAndRectRequest, IConnectorAndRect } from '../../../domain'; -import { FConnectionBase, FSnapConnectionComponent } from '../../../f-connection'; +import { FSnapConnectionComponent } from '../../../f-connection'; import { FNodeInputDirective, FNodeOutputDirective } from '../../../f-connectors'; import { IPoint } from '@foblex/2d'; import { FMediator } from '@foblex/mediator'; @@ -10,6 +10,7 @@ import { Injector } from '@angular/core'; import { IFReassignHandler, roundedRectFromPoint } from './i-f-reassign-handler'; import { FReassignTargetDragHandler } from './f-reassign-target.drag-handler'; import { FReassignSourceDragHandler } from './f-reassign-source.drag-handler'; +import { ConnectionBehaviourBuilder, FConnectionBase } from '../../../f-connection-v2'; export class FReassignConnectionDragHandler implements IFDragHandler { public fEventType = 'reassign-connection'; @@ -17,6 +18,7 @@ export class FReassignConnectionDragHandler implements IFDragHandler { private readonly _result: FDragHandlerResult; private readonly _mediator: FMediator; + private readonly _connectionBehaviour: ConnectionBehaviourBuilder; private readonly _store: FComponentsStore; private get _snapConnection(): FSnapConnectionComponent | undefined { @@ -62,6 +64,7 @@ export class FReassignConnectionDragHandler implements IFDragHandler { ) { this._result = _injector.get(FDragHandlerResult); this._mediator = _injector.get(FMediator); + this._connectionBehaviour = _injector.get(ConnectionBehaviourBuilder); this._store = _injector.get(FComponentsStore); this.fData = { @@ -76,6 +79,7 @@ export class FReassignConnectionDragHandler implements IFDragHandler { private _sourceDragHandler(): FReassignSourceDragHandler { return new FReassignSourceDragHandler( this._mediator, + this._connectionBehaviour, this._connection, this._sourceConnectorAndRect, this._targetConnectorAndRect, @@ -85,6 +89,7 @@ export class FReassignConnectionDragHandler implements IFDragHandler { private _targetDragHandler(): FReassignTargetDragHandler { return new FReassignTargetDragHandler( this._mediator, + this._connectionBehaviour, this._connection, this._sourceConnectorAndRect, this._targetConnectorAndRect, diff --git a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-source.drag-handler.ts b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-source.drag-handler.ts index 2bf7e440..6a74fcfd 100644 --- a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-source.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-source.drag-handler.ts @@ -1,6 +1,5 @@ import { FMediator } from '@foblex/mediator'; import { - CalculateConnectionLineByBehaviorRequest, CalculateClosestConnectorRequest, CalculateSourceConnectorsToConnectRequest, IClosestConnector, @@ -8,18 +7,20 @@ import { MarkConnectableConnectorsRequest, UnmarkConnectableConnectorsRequest, } from '../../../domain'; -import { - EFConnectableSide, - FNodeInputDirective, - FNodeOutputDirective, -} from '../../../f-connectors'; -import { FConnectionBase, FSnapConnectionComponent } from '../../../f-connection'; +import { FNodeInputDirective, FNodeOutputDirective } from '../../../f-connectors'; +import { FSnapConnectionComponent } from '../../../f-connection'; import { ILine, IPoint, RoundedRect } from '@foblex/2d'; import { IFReassignHandler, isClosestConnectorInsideSnapThreshold, roundedRectFromPoint, } from './i-f-reassign-handler'; +import { + ConnectionBehaviourBuilder, + ConnectionBehaviourBuilderRequest, + EFConnectableSide, + FConnectionBase, +} from '../../../f-connection-v2'; export class FReassignSourceDragHandler implements IFReassignHandler { private _connectableConnectors: IConnectorAndRect[] = []; @@ -38,6 +39,7 @@ export class FReassignSourceDragHandler implements IFReassignHandler { constructor( private readonly _mediator: FMediator, + private readonly _connectionBehaviour: ConnectionBehaviourBuilder, private readonly _connection: FConnectionBase, private readonly _sourceConnectorAndRect: IConnectorAndRect, private readonly _targetConnectorAndRect: IConnectorAndRect, @@ -108,8 +110,8 @@ export class FReassignSourceDragHandler implements IFReassignHandler { } private _calculateNewLine(sourcePoint: IPoint, fSide: EFConnectableSide): ILine { - return this._mediator.execute( - new CalculateConnectionLineByBehaviorRequest( + return this._connectionBehaviour.handle( + new ConnectionBehaviourBuilderRequest( roundedRectFromPoint(sourcePoint), this._targetConnectorAndRect.fRect, this._connection, @@ -135,8 +137,8 @@ export class FReassignSourceDragHandler implements IFReassignHandler { fClosestInput: IClosestConnector, snapConnection: FSnapConnectionComponent, ): ILine { - return this._mediator.execute( - new CalculateConnectionLineByBehaviorRequest( + return this._connectionBehaviour.handle( + new ConnectionBehaviourBuilderRequest( fClosestInput.fRect, this._targetConnectorAndRect.fRect, snapConnection, diff --git a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-target.drag-handler.ts b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-target.drag-handler.ts index d4f6a519..e47c692c 100644 --- a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-target.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/f-reassign-target.drag-handler.ts @@ -1,6 +1,5 @@ import { FMediator } from '@foblex/mediator'; import { - CalculateConnectionLineByBehaviorRequest, CalculateClosestConnectorRequest, CalculateTargetConnectorsToConnectRequest, IClosestConnector, @@ -8,18 +7,20 @@ import { MarkConnectableConnectorsRequest, UnmarkConnectableConnectorsRequest, } from '../../../domain'; -import { - EFConnectableSide, - FNodeInputDirective, - FNodeOutputDirective, -} from '../../../f-connectors'; -import { FConnectionBase, FSnapConnectionComponent } from '../../../f-connection'; +import { FNodeInputDirective, FNodeOutputDirective } from '../../../f-connectors'; +import { FSnapConnectionComponent } from '../../../f-connection'; import { ILine, IPoint, IRoundedRect, RoundedRect } from '@foblex/2d'; import { IFReassignHandler, isClosestConnectorInsideSnapThreshold, roundedRectFromPoint, } from './i-f-reassign-handler'; +import { + ConnectionBehaviourBuilder, + ConnectionBehaviourBuilderRequest, + EFConnectableSide, + FConnectionBase, +} from '../../../f-connection-v2'; export class FReassignTargetDragHandler implements IFReassignHandler { private _connectableConnectors: IConnectorAndRect[] = []; @@ -42,6 +43,7 @@ export class FReassignTargetDragHandler implements IFReassignHandler { constructor( private readonly _mediator: FMediator, + private readonly _connectionBehaviour: ConnectionBehaviourBuilder, private readonly _connection: FConnectionBase, private readonly _sourceConnectorAndRect: IConnectorAndRect, private readonly _targetConnectorAndRect: IConnectorAndRect, @@ -106,8 +108,8 @@ export class FReassignTargetDragHandler implements IFReassignHandler { } private _calculateNewLine(targetPoint: IPoint, fSide: EFConnectableSide): ILine { - return this._mediator.execute( - new CalculateConnectionLineByBehaviorRequest( + return this._connectionBehaviour.handle( + new ConnectionBehaviourBuilderRequest( this._sourceConnectorRect, roundedRectFromPoint(targetPoint), this._connection, @@ -133,8 +135,8 @@ export class FReassignTargetDragHandler implements IFReassignHandler { fClosestInput: IClosestConnector, snapConnection: FSnapConnectionComponent, ): ILine { - return this._mediator.execute( - new CalculateConnectionLineByBehaviorRequest( + return this._connectionBehaviour.handle( + new ConnectionBehaviourBuilderRequest( this._sourceConnectorRect, fClosestInput.fRect, snapConnection, diff --git a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/i-f-reassign-connection-drag-result.ts b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/i-f-reassign-connection-drag-result.ts index cd12f3a2..a7fb1d18 100644 --- a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/i-f-reassign-connection-drag-result.ts +++ b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/i-f-reassign-connection-drag-result.ts @@ -1,9 +1,8 @@ import { RoundedRect } from '@foblex/2d'; -import { FConnectionBase } from '../../../f-connection'; import { IConnectorAndRect } from '../../../domain'; +import { FConnectionBase } from '../../../f-connection-v2'; export interface IFReassignConnectionDragResult { - isTargetDragHandle: boolean; sourceConnectorRect: RoundedRect; diff --git a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/reassign-preparation/f-reassign-connection-preparation.execution.ts b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/reassign-preparation/f-reassign-connection-preparation.execution.ts index cb6a4b6e..f331a060 100644 --- a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/reassign-preparation/f-reassign-connection-preparation.execution.ts +++ b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/reassign-preparation/f-reassign-connection-preparation.execution.ts @@ -5,49 +5,60 @@ import { FComponentsStore } from '../../../../f-storage'; import { FDraggableDataContext } from '../../../f-draggable-data-context'; import { isValidEventTrigger, UpdateItemAndChildrenLayersRequest } from '../../../../domain'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; -import { FConnectionBase } from '../../../../f-connection'; import { FReassignConnectionDragHandler } from '../f-reassign-connection.drag-handler'; import { isDragHandleEnd, isPointerInsideStartOrEndDragHandles, -} from "./is-pointer-inside-start-or-end-drag-handles"; +} from './is-pointer-inside-start-or-end-drag-handles'; +import { calculatePointerInFlow } from '../../../../utils'; +import { FCanvasBase } from '../../../../f-canvas'; +import { FConnectionBase } from '../../../../f-connection-v2'; @Injectable() @FExecutionRegister(FReassignConnectionPreparationRequest) -export class FReassignConnectionPreparationExecution implements IExecution { - - private readonly _fMediator = inject(FMediator); +export class FReassignConnectionPreparationExecution + implements IExecution +{ + private readonly _mediator = inject(FMediator); private readonly _store = inject(FComponentsStore); private readonly _dragContext = inject(FDraggableDataContext); private readonly _injector = inject(Injector); - private _fConnection: FConnectionBase | undefined; + private _connection: FConnectionBase | undefined; + + private get _canvas(): FCanvasBase { + return this._store.fCanvas as FCanvasBase; + } private get _transform(): ITransformModel { - return this._store.fCanvas!.transform; + return this._canvas.transform as ITransformModel; } - private get _fHost(): HTMLElement { - return this._store.fFlow!.hostElement; + private get _flowHost(): HTMLElement { + return this._store.flowHost; } - private get _fConnections(): FConnectionBase[] { + private get _connections(): FConnectionBase[] { return this._store.fConnections; } public handle(request: FReassignConnectionPreparationRequest): void { - const position = this._getPointInFlow(request); + const position = calculatePointerInFlow(request.event, this._flowHost, this._transform); + if (!this._isValid(position) || !this._isValidTrigger(request)) { return; } this._dragContext.onPointerDownScale = this._transform.scale; this._dragContext.onPointerDownPosition = Point.fromPoint(request.event.getPosition()) - .elementTransform(this._fHost).div(this._transform.scale); + .elementTransform(this._flowHost) + .div(this._transform.scale); this._dragContext.draggableItems = [ new FReassignConnectionDragHandler( - this._injector, this._fConnection!, isDragHandleEnd(this._fConnection!, position), + this._injector, + this._connection!, + isDragHandleEnd(this._connection!, position), ), ]; @@ -55,36 +66,26 @@ export class FReassignConnectionPreparationExecution implements IExecution + isPointerInsideStartOrEndDragHandles(x, position), + ); - return connections.length ? connections[0] : undefined; + return this._connection; } - private _getConnectionsFromPoint(position: IPoint): FConnectionBase[] { - return this._fConnections.filter((x) => isPointerInsideStartOrEndDragHandles(x, position)); + private _isValidTrigger(request: FReassignConnectionPreparationRequest): boolean { + return isValidEventTrigger(request.event.originalEvent, request.fTrigger); } private _updateConnectionLayer(): void { - this._fMediator.execute( + this._mediator.execute( new UpdateItemAndChildrenLayersRequest( - this._fConnection!, this._store.fCanvas!.fConnectionsContainer().nativeElement, + this._connection!, + this._canvas.fConnectionsContainer().nativeElement, ), ); } diff --git a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/reassign-preparation/is-pointer-inside-start-or-end-drag-handles.ts b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/reassign-preparation/is-pointer-inside-start-or-end-drag-handles.ts index a0e87f1a..6f0ec167 100644 --- a/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/reassign-preparation/is-pointer-inside-start-or-end-drag-handles.ts +++ b/projects/f-flow/src/f-draggable/f-connection/f-reassign-connection/reassign-preparation/is-pointer-inside-start-or-end-drag-handles.ts @@ -1,20 +1,28 @@ -import { IPoint } from "@foblex/2d"; -import { FConnectionBase } from "../../../../f-connection"; +import { IPoint } from '@foblex/2d'; +import { FConnectionBase } from '../../../../f-connection-v2'; -export function isPointerInsideStartOrEndDragHandles(connection: FConnectionBase, position: IPoint): boolean { +export function isPointerInsideStartOrEndDragHandles( + connection: FConnectionBase, + position: IPoint, +): boolean { return isDragHandleEnd(connection, position) || isDragHandleStart(connection, position); } export function isDragHandleEnd(connection: FConnectionBase, position: IPoint): boolean { - return connection.fDragHandleEnd()?.point - && _isPointInsideCircle(position, connection.fDragHandleEnd().point) - && !connection.fDraggingDisabled(); + return ( + connection.fDragHandleEnd()?.point && + _isPointInsideCircle(position, connection.fDragHandleEnd().point) && + !connection.fDraggingDisabled() + ); } export function isDragHandleStart(connection: FConnectionBase, position: IPoint): boolean { - return !!connection.fDragHandleStart()?.point - && _isPointInsideCircle(position, connection.fDragHandleStart()!.point) - && !connection.fDraggingDisabled() && connection.fReassignableStart(); + return ( + !!connection.fDragHandleStart()?.point && + _isPointInsideCircle(position, connection.fDragHandleStart()!.point) && + !connection.fDraggingDisabled() && + connection.fReassignableStart() + ); } function _isPointInsideCircle(point: IPoint, circleCenter: IPoint): boolean { diff --git a/projects/f-flow/src/f-draggable/f-connection/index.ts b/projects/f-flow/src/f-draggable/f-connection/index.ts index eae4fbac..626d7a4e 100644 --- a/projects/f-flow/src/f-draggable/f-connection/index.ts +++ b/projects/f-flow/src/f-draggable/f-connection/index.ts @@ -2,4 +2,6 @@ export * from './f-create-connection'; export * from './f-reassign-connection'; +export * from './move-connection-waypoint'; + export * from './providers'; diff --git a/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/index.ts b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/index.ts new file mode 100644 index 00000000..23e0d4ec --- /dev/null +++ b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/index.ts @@ -0,0 +1,5 @@ +export * from './move-connection-waypoint-finalize'; + +export * from './move-connection-waypoint-preparation'; + +export * from './move-connection-waypoint-handler'; diff --git a/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-finalize/index.ts b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-finalize/index.ts new file mode 100644 index 00000000..716d4d7f --- /dev/null +++ b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-finalize/index.ts @@ -0,0 +1,3 @@ +export * from './move-connection-waypoint-finalize'; + +export * from './move-connection-waypoint-finalize-request'; diff --git a/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-finalize/move-connection-waypoint-finalize-request.ts b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-finalize/move-connection-waypoint-finalize-request.ts new file mode 100644 index 00000000..5f9fda98 --- /dev/null +++ b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-finalize/move-connection-waypoint-finalize-request.ts @@ -0,0 +1,6 @@ +import { IPointerEvent } from '../../../../drag-toolkit'; + +export class MoveConnectionWaypointFinalizeRequest { + static readonly fToken = Symbol('MoveConnectionWaypointFinalizeRequest'); + constructor(public readonly event: IPointerEvent) {} +} diff --git a/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-finalize/move-connection-waypoint-finalize.ts b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-finalize/move-connection-waypoint-finalize.ts new file mode 100644 index 00000000..4336e29a --- /dev/null +++ b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-finalize/move-connection-waypoint-finalize.ts @@ -0,0 +1,26 @@ +import { inject, Injectable } from '@angular/core'; +import { MoveConnectionWaypointFinalizeRequest } from './move-connection-waypoint-finalize-request'; +import { FExecutionRegister, IExecution } from '@foblex/mediator'; +import { MoveConnectionWaypointHandler } from '../move-connection-waypoint-handler'; +import { FDraggableDataContext } from '../../../../f-draggable'; + +@Injectable() +@FExecutionRegister(MoveConnectionWaypointFinalizeRequest) +export class MoveConnectionWaypointFinalize implements IExecution { + private readonly _dragContext = inject(FDraggableDataContext); + + private get _fDragHandler(): MoveConnectionWaypointHandler { + return this._dragContext.draggableItems[0] as MoveConnectionWaypointHandler; + } + + public handle(_request: MoveConnectionWaypointFinalizeRequest): void { + if (!this._isDroppedConnectionReassignEvent()) { + return; + } + this._fDragHandler.onPointerUp(); + } + + private _isDroppedConnectionReassignEvent(): boolean { + return this._dragContext.draggableItems.some((x) => x instanceof MoveConnectionWaypointHandler); + } +} diff --git a/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-handler.ts b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-handler.ts new file mode 100644 index 00000000..f8aecffe --- /dev/null +++ b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-handler.ts @@ -0,0 +1,67 @@ +import { IPoint, PointExtensions } from '@foblex/2d'; +import { Injector } from '@angular/core'; +import { IFDragHandler } from '../../../f-draggable'; +import { + FConnectionBase, + FConnectionWaypointsBase, + FConnectionWaypointsChangedEvent, + WaypointPick, +} from '../../../f-connection-v2'; +import { FComponentsStore } from '../../../f-storage'; + +export class MoveConnectionWaypointHandler implements IFDragHandler { + public fEventType = 'move-connection-waypoint'; + public fData: unknown; + + private readonly _store: FComponentsStore; + + private _point: IPoint | undefined; + + private get _waypointsComponent(): FConnectionWaypointsBase { + return this._pick.connection.fWaypoints() as FConnectionWaypointsBase; + } + + private get _connection(): FConnectionBase { + return this._pick.connection; + } + + constructor( + readonly _injector: Injector, + private readonly _pick: WaypointPick, + ) { + this._store = this._injector.get(FComponentsStore); + } + + public prepareDragSequence(): void { + if (this._pick.candidate) { + this._point = { ...this._pick.candidate }; + this._waypointsComponent.insert(this._pick.candidate); + } else { + this._point = { ...this._pick.waypoint }; + this._waypointsComponent.select(this._pick.waypoint); + } + this._redrawConnection(); + } + + public onPointerMove(_difference: IPoint): void { + this._waypointsComponent.move(PointExtensions.sum(this._point!, _difference)); + this._redrawConnection(); + } + + public onPointerUp(): void { + this._waypointsComponent.update(); + this._store.fDraggable?.fConnectionWaypointsChanged.emit(this._eventFromPick()); + } + + private _redrawConnection(): void { + this._connection.setLine(this._connection.line); + this._connection.redraw(); + } + + private _eventFromPick(): FConnectionWaypointsChangedEvent { + return new FConnectionWaypointsChangedEvent( + this._pick.connection.fId(), + this._pick.connection.fWaypoints()?.waypoints() || [], + ); + } +} diff --git a/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-preparation/index.ts b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-preparation/index.ts new file mode 100644 index 00000000..5074e118 --- /dev/null +++ b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-preparation/index.ts @@ -0,0 +1,3 @@ +export * from './move-connection-waypoint-preparation'; + +export * from './move-connection-waypoint-preparation-request'; diff --git a/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-preparation/move-connection-waypoint-preparation-request.ts b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-preparation/move-connection-waypoint-preparation-request.ts new file mode 100644 index 00000000..f2b7ebb8 --- /dev/null +++ b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-preparation/move-connection-waypoint-preparation-request.ts @@ -0,0 +1,10 @@ +import { IPointerEvent } from '../../../../drag-toolkit'; +import { FEventTrigger } from '../../../../domain'; + +export class MoveConnectionWaypointPreparationRequest { + static readonly fToken = Symbol('MoveConnectionWaypointPreparationRequest'); + constructor( + public readonly event: IPointerEvent, + public readonly fTrigger: FEventTrigger, + ) {} +} diff --git a/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-preparation/move-connection-waypoint-preparation.ts b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-preparation/move-connection-waypoint-preparation.ts new file mode 100644 index 00000000..eea83c68 --- /dev/null +++ b/projects/f-flow/src/f-draggable/f-connection/move-connection-waypoint/move-connection-waypoint-preparation/move-connection-waypoint-preparation.ts @@ -0,0 +1,76 @@ +import { inject, Injectable, Injector } from '@angular/core'; +import { MoveConnectionWaypointPreparationRequest } from './move-connection-waypoint-preparation-request'; +import { IPoint, ITransformModel, Point } from '@foblex/2d'; +import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; +import { MoveConnectionWaypointHandler } from '../move-connection-waypoint-handler'; +import { FComponentsStore } from '../../../../f-storage'; +import { FDraggableDataContext } from '../../../../f-draggable'; +import { isValidEventTrigger, UpdateItemAndChildrenLayersRequest } from '../../../../domain'; +import { FCanvasBase } from '../../../../f-canvas'; +import { calculatePointerInFlow } from '../../../../utils'; +import { FConnectionBase, pickWaypoint } from '../../../../f-connection-v2'; + +@Injectable() +@FExecutionRegister(MoveConnectionWaypointPreparationRequest) +export class MoveConnectionWaypointPreparation + implements IExecution +{ + private readonly _mediator = inject(FMediator); + private readonly _store = inject(FComponentsStore); + private readonly _dragContext = inject(FDraggableDataContext); + private readonly _injector = inject(Injector); + + private get _canvas(): FCanvasBase { + return this._store.fCanvas as FCanvasBase; + } + + private get _transform(): ITransformModel { + return this._canvas.transform as ITransformModel; + } + + private get _flowHost(): HTMLElement { + return this._store.flowHost; + } + + private get _connections(): FConnectionBase[] { + return this._store.fConnections; + } + + public handle(request: MoveConnectionWaypointPreparationRequest): void { + const position = calculatePointerInFlow(request.event, this._flowHost, this._transform); + + const pick = this._pickControlPoint(position); + if (!pick || !this._isValidTrigger(request)) { + return; + } + + this._dragContext.onPointerDownScale = this._transform.scale; + this._dragContext.onPointerDownPosition = Point.fromPoint(request.event.getPosition()) + .elementTransform(this._flowHost) + .div(this._transform.scale); + this._dragContext.draggableItems = [new MoveConnectionWaypointHandler(this._injector, pick)]; + + queueMicrotask(() => this._updateConnectionLayer(pick.connection)); + } + + private _pickControlPoint(position: IPoint) { + if (!this._dragContext.isEmpty()) { + return undefined; + } + + return pickWaypoint(this._connections, position); + } + + private _isValidTrigger(request: MoveConnectionWaypointPreparationRequest): boolean { + return isValidEventTrigger(request.event.originalEvent, request.fTrigger); + } + + private _updateConnectionLayer(connection: FConnectionBase): void { + this._mediator.execute( + new UpdateItemAndChildrenLayersRequest( + connection, + this._canvas.fConnectionsContainer().nativeElement, + ), + ); + } +} diff --git a/projects/f-flow/src/f-draggable/f-connection/providers.ts b/projects/f-flow/src/f-draggable/f-connection/providers.ts index 00b344c2..db2f593c 100644 --- a/projects/f-flow/src/f-draggable/f-connection/providers.ts +++ b/projects/f-flow/src/f-draggable/f-connection/providers.ts @@ -1,15 +1,19 @@ import { + CreateConnectionFromOutputPreparation, + CreateConnectionPreparation, FCreateConnectionDragHandlerPreparationExecution, FCreateConnectionFinalizeExecution, FCreateConnectionFromOutletPreparationExecution, - CreateConnectionFromOutputPreparation, - CreateConnectionPreparation, GetFirstConnectableOutputExecution, } from './f-create-connection'; import { FReassignConnectionFinalizeExecution, FReassignConnectionPreparationExecution, } from './f-reassign-connection'; +import { + MoveConnectionWaypointFinalize, + MoveConnectionWaypointPreparation, +} from './move-connection-waypoint'; export const CONNECTIONS_PROVIDERS = [ FCreateConnectionFinalizeExecution, @@ -27,4 +31,8 @@ export const CONNECTIONS_PROVIDERS = [ FReassignConnectionFinalizeExecution, FReassignConnectionPreparationExecution, + + MoveConnectionWaypointPreparation, + + MoveConnectionWaypointFinalize, ]; diff --git a/projects/f-flow/src/f-draggable/f-drag-handler/i-f-drag-handler.ts b/projects/f-flow/src/f-draggable/f-drag-handler/i-f-drag-handler.ts index 3103ea55..fa9d8768 100644 --- a/projects/f-flow/src/f-draggable/f-drag-handler/i-f-drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-drag-handler/i-f-drag-handler.ts @@ -1,4 +1,5 @@ import { IPoint } from '@foblex/2d'; +import { IPointerEvent } from '../../drag-toolkit'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface IFDragHandler { @@ -8,7 +9,7 @@ export interface IFDragHandler { prepareDragSequence?(): void; - onPointerMove(difference: IPoint): void; + onPointerMove(difference: IPoint, event?: IPointerEvent): void; onPointerUp?(): void; } diff --git a/projects/f-flow/src/f-draggable/f-draggable-base.ts b/projects/f-flow/src/f-draggable/f-draggable-base.ts index 2b03216a..2e6804dc 100644 --- a/projects/f-flow/src/f-draggable/f-draggable-base.ts +++ b/projects/f-flow/src/f-draggable/f-draggable-base.ts @@ -1,4 +1,10 @@ -import { ElementRef, EventEmitter, inject, InputSignalWithTransform } from '@angular/core'; +import { + ElementRef, + EventEmitter, + inject, + InputSignalWithTransform, + OutputEmitterRef, +} from '@angular/core'; import { FCreateConnectionEvent, FReassignConnectionEvent } from './f-connection'; import { FSelectionChangeEvent } from './f-selection-change-event'; import { FCreateNodeEvent } from '../f-external-item'; @@ -6,6 +12,7 @@ import { FDragStartedEvent, FNodeIntersectedWithConnections } from './domain'; import { FDropToGroupEvent } from './f-drop-to-group'; import { DragAndDropBase } from '../drag-toolkit'; import { FMoveNodesEvent } from './f-node-move'; +import { FConnectionWaypointsChangedEvent } from '../f-connection-v2'; export abstract class FDraggableBase extends DragAndDropBase { public readonly hostElement = inject(ElementRef).nativeElement; @@ -24,6 +31,8 @@ export abstract class FDraggableBase extends DragAndDropBase { public abstract fCreateConnection: EventEmitter; + public abstract fConnectionWaypointsChanged: OutputEmitterRef; + public abstract fDropToGroup: EventEmitter; public abstract vCellSize: InputSignalWithTransform; diff --git a/projects/f-flow/src/f-draggable/f-draggable.directive.ts b/projects/f-flow/src/f-draggable/f-draggable.directive.ts index 9357cef8..7839233d 100644 --- a/projects/f-flow/src/f-draggable/f-draggable.directive.ts +++ b/projects/f-flow/src/f-draggable/f-draggable.directive.ts @@ -10,6 +10,7 @@ import { numberAttribute, OnDestroy, OnInit, + output, Output, QueryList, } from '@angular/core'; @@ -17,22 +18,25 @@ import { FDraggableBase } from './f-draggable-base'; import { FMoveNodesEvent, FNodeMoveFinalizeRequest, - FNodeMovePreparationRequest, + MoveNodePreparationRequest, } from './f-node-move'; import { FCanvasMoveFinalizeRequest, FCanvasMovePreparationRequest } from './f-canvas'; import { + CreateConnectionPreparationRequest, FCreateConnectionEvent, FCreateConnectionFinalizeRequest, - CreateConnectionPreparationRequest, FReassignConnectionEvent, FReassignConnectionFinalizeRequest, FReassignConnectionPreparationRequest, + MoveConnectionWaypointFinalizeRequest, + MoveConnectionWaypointPreparationRequest, } from './f-connection'; import { FSelectionChangeEvent } from './f-selection-change-event'; import { FMediator } from '@foblex/mediator'; import { AddDndToStoreRequest, defaultEventTrigger, + DragRectCache, EmitSelectionChangeEventRequest, EndDragSequenceRequest, FEventTrigger, @@ -40,7 +44,6 @@ import { InitializeDragSequenceRequest, OnPointerMoveRequest, PrepareDragSequenceRequest, - DragRectCache, RemoveDndFromStoreRequest, } from '../domain'; import { @@ -49,7 +52,7 @@ import { FExternalItemPreparationRequest, PreventDefaultIsExternalItemRequest, } from '../f-external-item'; -import { FSingleSelectRequest } from './f-single-select'; +import { SingleSelectRequest } from './single-select'; import { NodeResizeFinalizeRequest, NodeResizePreparationRequest } from './f-node-resize'; import { F_AFTER_MAIN_PLUGIN, @@ -60,13 +63,15 @@ import { EOperationSystem, PlatformService } from '@foblex/platform'; import { FDragStartedEvent, FNodeIntersectedWithConnections } from './domain'; import { FDragHandlerResult } from './f-drag-handler'; import { - FDropToGroupEvent, DropToGroupFinalizeRequest, DropToGroupPreparationRequest, + FDropToGroupEvent, } from './f-drop-to-group'; import { FNodeRotateFinalizeRequest, FNodeRotatePreparationRequest } from './f-node-rotate'; import { IPointerEvent } from '../drag-toolkit'; import { isDragBlocker } from './is-drag-blocker'; +import { PinchToZoomFinalizeRequest, PinchToZoomPreparationRequest } from './pinch-to-zoom'; +import { FConnectionWaypointsChangedEvent } from '../f-connection-v2'; // ┌──────────────────────────────┐ // │ Angular Realm │ @@ -146,6 +151,11 @@ export class FDraggableDirective @Input() public fCreateConnectionTrigger: FEventTrigger = defaultEventTrigger; + public fConnectionWaypointsTrigger = input(defaultEventTrigger); + + @Input() + public fMoveControlPointTrigger: FEventTrigger = defaultEventTrigger; + @Input() public fNodeResizeTrigger: FEventTrigger = defaultEventTrigger; @@ -183,6 +193,8 @@ export class FDraggableDirective @Output() public override fCreateConnection = new EventEmitter(); + public override fConnectionWaypointsChanged = output(); + @Output() public override fDropToGroup = new EventEmitter(); @@ -244,7 +256,9 @@ export class FDraggableDirective this._beforePlugins.forEach((p) => p.onPointerDown?.(event)); - this._mediator.execute(new FSingleSelectRequest(event, this.fMultiSelectTrigger)); + this._mediator.execute(new PinchToZoomPreparationRequest(event)); + + this._mediator.execute(new SingleSelectRequest(event, this.fMultiSelectTrigger)); this._mediator.execute( new FReassignConnectionPreparationRequest(event, this.fReassignConnectionTrigger), @@ -254,6 +268,14 @@ export class FDraggableDirective new CreateConnectionPreparationRequest(event, this.fCreateConnectionTrigger), ); + this._mediator.execute( + new CreateConnectionPreparationRequest(event, this.fCreateConnectionTrigger), + ); + + this._mediator.execute( + new MoveConnectionWaypointPreparationRequest(event, this.fConnectionWaypointsTrigger()), + ); + this._afterPlugins.forEach((p) => p.onPointerDown?.(event)); const isMouseLeftOrTouch = event.isMouseLeftButton(); @@ -271,7 +293,7 @@ export class FDraggableDirective this._mediator.execute(new FNodeRotatePreparationRequest(event, this.fNodeRotateTrigger)); - this._mediator.execute(new FNodeMovePreparationRequest(event, this.fNodeMoveTrigger)); + this._mediator.execute(new MoveNodePreparationRequest(event, this.fNodeMoveTrigger)); this._mediator.execute( new FExternalItemPreparationRequest(event, this.fExternalItemTrigger), @@ -315,6 +337,10 @@ export class FDraggableDirective this._afterPlugins.forEach((x) => x.onPointerUp?.(event)); + this._mediator.execute(new PinchToZoomFinalizeRequest(event)); + + this._mediator.execute(new MoveConnectionWaypointFinalizeRequest(event)); + this._mediator.execute(new EndDragSequenceRequest()); } diff --git a/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/base-connection.drag-handler.ts b/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/base-connection.drag-handler.ts index 401914ff..bcdb34ef 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/base-connection.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/base-connection.drag-handler.ts @@ -1,18 +1,19 @@ import { ILine, IPoint, PointExtensions, RoundedRect } from '@foblex/2d'; import { FMediator } from '@foblex/mediator'; -import { - CalculateConnectionLineByBehaviorRequest, - GetConnectorAndRectRequest, - IConnectorAndRect, -} from '../../../domain'; +import { GetConnectorAndRectRequest, IConnectorAndRect } from '../../../domain'; import { FConnectorBase } from '../../../f-connectors'; import { FComponentsStore } from '../../../f-storage'; -import { FConnectionBase } from '../../../f-connection'; import { Injector } from '@angular/core'; +import { + ConnectionBehaviourBuilder, + ConnectionBehaviourBuilderRequest, + FConnectionBase, +} from '../../../f-connection-v2'; export class BaseConnectionDragHandler { private readonly _mediator: FMediator; private readonly _store: FComponentsStore; + private readonly _connectionBehaviour: ConnectionBehaviourBuilder; private _fOutputWithRect!: IConnectorAndRect; private _fInputWithRect!: IConnectorAndRect; @@ -47,6 +48,7 @@ export class BaseConnectionDragHandler { public fConnection: FConnectionBase, ) { this._mediator = _injector.get(FMediator); + this._connectionBehaviour = _injector.get(ConnectionBehaviourBuilder); this._store = _injector.get(FComponentsStore); this._initialize(); } @@ -73,8 +75,8 @@ export class BaseConnectionDragHandler { } private _recalculateConnection(): ILine { - return this._mediator.execute( - new CalculateConnectionLineByBehaviorRequest( + return this._connectionBehaviour.handle( + new ConnectionBehaviourBuilderRequest( RoundedRect.fromRoundedRect(this._fOutputWithRect.fRect).addPoint(this._sourceDifference), RoundedRect.fromRoundedRect(this._fInputWithRect.fRect).addPoint(this._targetDifference), this.fConnection, diff --git a/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/source-connection.drag-handler.ts b/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/source-connection.drag-handler.ts index e55d3662..c2759e8e 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/source-connection.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/source-connection.drag-handler.ts @@ -1,10 +1,9 @@ import { BaseConnectionDragHandler } from './base-connection.drag-handler'; -import { FConnectionBase } from '../../../f-connection'; import { IPoint } from '@foblex/2d'; import { Injector } from '@angular/core'; +import { FConnectionBase } from '../../../f-connection-v2'; export class SourceConnectionDragHandler extends BaseConnectionDragHandler { - constructor(injector: Injector, fConnection: FConnectionBase) { super(injector, fConnection); } diff --git a/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/source-target-connection.drag-handler.ts b/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/source-target-connection.drag-handler.ts index b98506df..43ee2605 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/source-target-connection.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/source-target-connection.drag-handler.ts @@ -1,10 +1,9 @@ import { BaseConnectionDragHandler } from './base-connection.drag-handler'; -import { FConnectionBase } from '../../../f-connection'; import { IPoint } from '@foblex/2d'; import { Injector } from '@angular/core'; +import { FConnectionBase } from '../../../f-connection-v2'; export class SourceTargetConnectionDragHandler extends BaseConnectionDragHandler { - private readonly _callTracker = new Map(); constructor(injector: Injector, fConnection: FConnectionBase) { @@ -28,4 +27,3 @@ export class SourceTargetConnectionDragHandler extends BaseConnectionDragHandler } } } - diff --git a/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/target-connection.drag-handler.ts b/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/target-connection.drag-handler.ts index e5a44faa..337afadb 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/target-connection.drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/connection-drag-handlers/target-connection.drag-handler.ts @@ -1,10 +1,9 @@ -import { FConnectionBase } from '../../../f-connection'; import { BaseConnectionDragHandler } from './base-connection.drag-handler'; import { IPoint } from '@foblex/2d'; import { Injector } from '@angular/core'; +import { FConnectionBase } from '../../../f-connection-v2'; export class TargetConnectionDragHandler extends BaseConnectionDragHandler { - constructor(injector: Injector, fConnection: FConnectionBase) { super(injector, fConnection); } diff --git a/projects/f-flow/src/f-draggable/f-node-move/create-drag-model-from-selection/create-input-connection-handler-and-set-to-node-handler/create-input-connection-handler-and-set-to-node-handler.ts b/projects/f-flow/src/f-draggable/f-node-move/create-drag-model-from-selection/create-input-connection-handler-and-set-to-node-handler/create-input-connection-handler-and-set-to-node-handler.ts index 6c72057d..2fe9a04d 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/create-drag-model-from-selection/create-input-connection-handler-and-set-to-node-handler/create-input-connection-handler-and-set-to-node-handler.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/create-drag-model-from-selection/create-input-connection-handler-and-set-to-node-handler/create-input-connection-handler-and-set-to-node-handler.ts @@ -3,12 +3,12 @@ import { CreateInputConnectionHandlerAndSetToNodeHandlerRequest } from './create import { FComponentsStore } from '../../../../f-storage'; import { FExecutionRegister, IExecution } from '@foblex/mediator'; import { FNodeBase } from '../../../../f-node'; -import { FConnectionBase } from '../../../../f-connection'; import { BaseConnectionDragHandler, SourceTargetConnectionDragHandler, TargetConnectionDragHandler, } from '../../connection-drag-handlers'; +import { FConnectionBase } from '../../../../f-connection-v2'; @Injectable() @FExecutionRegister(CreateInputConnectionHandlerAndSetToNodeHandlerRequest) diff --git a/projects/f-flow/src/f-draggable/f-node-move/create-drag-model-from-selection/create-output-connection-handler-and-set-to-node-handler/create-output-connection-handler-and-set-to-node-handler.ts b/projects/f-flow/src/f-draggable/f-node-move/create-drag-model-from-selection/create-output-connection-handler-and-set-to-node-handler/create-output-connection-handler-and-set-to-node-handler.ts index 9e6edad8..cdd5f5da 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/create-drag-model-from-selection/create-output-connection-handler-and-set-to-node-handler/create-output-connection-handler-and-set-to-node-handler.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/create-drag-model-from-selection/create-output-connection-handler-and-set-to-node-handler/create-output-connection-handler-and-set-to-node-handler.ts @@ -3,12 +3,12 @@ import { CreateOutputConnectionHandlerAndSetToNodeHandlerRequest } from './creat import { FComponentsStore } from '../../../../f-storage'; import { FExecutionRegister, IExecution } from '@foblex/mediator'; import { FNodeBase } from '../../../../f-node'; -import { FConnectionBase } from '../../../../f-connection'; import { BaseConnectionDragHandler, SourceTargetConnectionDragHandler, SourceConnectionDragHandler, } from '../../connection-drag-handlers'; +import { FConnectionBase } from '../../../../f-connection-v2'; @Injectable() @FExecutionRegister(CreateOutputConnectionHandlerAndSetToNodeHandlerRequest) diff --git a/projects/f-flow/src/f-draggable/f-node-move/create-snap-lines/create-snap-lines-request.ts b/projects/f-flow/src/f-draggable/f-node-move/create-snap-lines/create-snap-lines-request.ts index c51c9f44..5e8f3b6a 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/create-snap-lines/create-snap-lines-request.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/create-snap-lines/create-snap-lines-request.ts @@ -1,10 +1,7 @@ -import { MoveSummaryDragHandler } from "../move-summary-drag-handler"; +import { MoveSummaryDragHandler } from '../move-summary-drag-handler'; export class CreateSnapLinesRequest { static readonly fToken = Symbol('CreateSnapLinesRequest'); - constructor( - public readonly summaryHandler: MoveSummaryDragHandler, - ) { - } + constructor(public readonly summaryHandler: MoveSummaryDragHandler) {} } diff --git a/projects/f-flow/src/f-draggable/f-node-move/move-drag-handler.ts b/projects/f-flow/src/f-draggable/f-node-move/move-drag-handler.ts index 38e3bd3c..e8fb9bd0 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/move-drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/move-drag-handler.ts @@ -2,14 +2,13 @@ import { IPoint, IRect, PointExtensions, RectExtensions } from '@foblex/2d'; import { IFDragHandler } from '../f-drag-handler'; import { FNodeBase } from '../../f-node'; import { BaseConnectionDragHandler } from './connection-drag-handlers'; -import { F_CSS_CLASS, GetNormalizedElementRectRequest } from "../../domain"; -import { Injector } from "@angular/core"; -import { IDragLimits } from "./create-drag-model-from-selection"; -import { DragConstraintPipeline, expandRectFromBaseline, IConstraintResult } from "./constraint"; -import { FMediator } from "@foblex/mediator"; +import { F_CSS_CLASS, GetNormalizedElementRectRequest } from '../../domain'; +import { Injector } from '@angular/core'; +import { IDragLimits } from './create-drag-model-from-selection'; +import { DragConstraintPipeline, expandRectFromBaseline, IConstraintResult } from './constraint'; +import { FMediator } from '@foblex/mediator'; export class MoveDragHandler implements IFDragHandler { - public readonly fEventType = 'move-node'; private readonly _startPosition = PointExtensions.initialize(); @@ -29,7 +28,9 @@ export class MoveDragHandler implements IFDragHandler { public fSourceHandlers: BaseConnectionDragHandler[] = [], public fTargetHandlers: BaseConnectionDragHandler[] = [], ) { - this._startRect = _injector.get(FMediator).execute(new GetNormalizedElementRectRequest(nodeOrGroup.hostElement)) + this._startRect = _injector + .get(FMediator) + .execute(new GetNormalizedElementRectRequest(nodeOrGroup.hostElement)); this._startPosition = { ...nodeOrGroup._position }; } @@ -37,21 +38,23 @@ export class MoveDragHandler implements IFDragHandler { this._limits = limits; this._pipeline = new DragConstraintPipeline(this._injector, this._startPosition, limits); - this._applyConstraints = (difference: IPoint) => { + this._applyConstraints = (difference) => { const summary = this._pipeline.apply(difference); this._applySoftExpansions(summary.soft); return summary.hardDifference; - } + }; } - private _applySoftExpansions( - softResults: IConstraintResult[], - ): void { + private _applySoftExpansions(softResults: IConstraintResult[]): void { this._lastSoftResults = softResults; this._lastSoftResults.forEach((result, index) => { const softLimit = this._limits!.soft[index]; - const expandedRect = expandRectFromBaseline(softLimit.boundingRect, result.overflow, result.edges); + const expandedRect = expandRectFromBaseline( + softLimit.boundingRect, + result.overflow, + result.edges, + ); this._commitParentRect(softLimit.nodeOrGroup, expandedRect); }); } @@ -63,7 +66,12 @@ export class MoveDragHandler implements IFDragHandler { } public getLastRect(): IRect { - return RectExtensions.initialize(this._lastPosition.x, this._lastPosition.y, this._startRect.width, this._startRect.height); + return RectExtensions.initialize( + this._lastPosition.x, + this._lastPosition.y, + this._startRect.width, + this._startRect.height, + ); } public prepareDragSequence(): void { @@ -87,7 +95,7 @@ export class MoveDragHandler implements IFDragHandler { this._applySoftExpansions(summary.soft); return summary.hardDifference; - } + }; } private _nodeOrGroupNewPosition(difference: IPoint): IPoint { @@ -110,7 +118,11 @@ export class MoveDragHandler implements IFDragHandler { private _emitEventIfNodeExpanded(): void { this._lastSoftResults.forEach((result, index) => { const softLimit = this._limits!.soft[index]; - const expandedRect = expandRectFromBaseline(softLimit.boundingRect, result.overflow, result.edges); + const expandedRect = expandRectFromBaseline( + softLimit.boundingRect, + result.overflow, + result.edges, + ); if (result.overflow.x || result.overflow.y) { softLimit.nodeOrGroup.sizeChange.emit(expandedRect); } diff --git a/projects/f-flow/src/f-draggable/f-node-move/move-finalize/f-node-move-finalize.execution.ts b/projects/f-flow/src/f-draggable/f-node-move/move-finalize/f-node-move-finalize.execution.ts index a80c0ac0..63223442 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/move-finalize/f-node-move-finalize.execution.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/move-finalize/f-node-move-finalize.execution.ts @@ -4,26 +4,23 @@ import { IPoint, Point } from '@foblex/2d'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { FComponentsStore } from '../../../f-storage'; import { FDraggableDataContext } from '../../f-draggable-data-context'; -import { - IsConnectionUnderNodeRequest, -} from '../../domain'; +import { IsConnectionUnderNodeRequest } from '../../domain'; import { ISnapResult, ISnapCoordinate } from '../../../f-line-alignment'; import { MoveSummaryDragHandler } from '../move-summary-drag-handler'; import { FNodeBase } from '../../../f-node'; -import { FMoveNodesEvent } from "../f-move-nodes.event"; +import { FMoveNodesEvent } from '../f-move-nodes.event'; @Injectable() @FExecutionRegister(FNodeMoveFinalizeRequest) export class FNodeMoveFinalizeExecution implements IExecution { - private readonly _mediator = inject(FMediator); private readonly _store = inject(FComponentsStore); private readonly _dragContext = inject(FDraggableDataContext); private _summaryHandler: MoveSummaryDragHandler | undefined; - private get _fHost(): HTMLElement { - return this._store.fFlow!.hostElement; + private get _flowHost(): HTMLElement { + return this._store.flowHost; } public handle({ event }: FNodeMoveFinalizeRequest): void { @@ -32,7 +29,10 @@ export class FNodeMoveFinalizeExecution implements IExecution x instanceof MoveSummaryDragHandler); + this._summaryHandler = this._dragContext.draggableItems.find( + (x) => x instanceof MoveSummaryDragHandler, + ); return !!this._summaryHandler; } private _getDifferenceBetweenPreparationAndFinalize(position: IPoint): Point { - return Point.fromPoint(position).elementTransform(this._fHost) + return Point.fromPoint(position) + .elementTransform(this._flowHost) .div(this._dragContext.onPointerDownScale) .sub(this._dragContext.onPointerDownPosition); } @@ -55,10 +58,17 @@ export class FNodeMoveFinalizeExecution implements IExecution x.assignFinalConstraints()); - this._summaryHandler!.onPointerMove({ ...snappedDifference }); - this._summaryHandler!.onPointerUp?.(); + this._summaryHandler?.onPointerMove({ ...snappedDifference }); + this._summaryHandler?.onPointerUp?.(); this._store.fDraggable?.fMoveNodes.emit(this._createMoveNodesEvent()); } private _createMoveNodesEvent(): FMoveNodesEvent { - const eventNodes = this._summaryHandler!.fData.fNodeIds.map((id: string) => { + const eventNodes = this._summaryHandler?.fData.fNodeIds.map((id: string) => { return { id, - position: this._store.fNodes.find(x => x.fId() === id)!._position, - } + position: this._store.fNodes.find((x) => x.fId() === id)?._position, + }; }); return new FMoveNodesEvent(eventNodes); @@ -89,15 +99,17 @@ export class FNodeMoveFinalizeExecution implements IExecution this._mediator.execute(new IsConnectionUnderNodeRequest(this._firstNodeOrGroup()))); + setTimeout(() => + this._mediator.execute(new IsConnectionUnderNodeRequest(this._firstNodeOrGroup())), + ); } } private _isDraggedJustOneNode(): boolean { - return this._summaryHandler!.rootHandlers.length === 1; + return this._summaryHandler?.rootHandlers.length === 1; } private _firstNodeOrGroup(): FNodeBase { - return this._summaryHandler!.rootHandlers[0].nodeOrGroup; + return this._summaryHandler?.rootHandlers[0].nodeOrGroup as FNodeBase; } } diff --git a/projects/f-flow/src/f-draggable/f-node-move/move-preparation/f-node-move-preparation.request.ts b/projects/f-flow/src/f-draggable/f-node-move/move-preparation/f-node-move-preparation.request.ts deleted file mode 100644 index a8bca06c..00000000 --- a/projects/f-flow/src/f-draggable/f-node-move/move-preparation/f-node-move-preparation.request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { FEventTrigger } from '../../../domain'; -import { IPointerEvent } from '../../../drag-toolkit'; - -export class FNodeMovePreparationRequest { - static readonly fToken = Symbol('FNodeMovePreparationRequest'); - - constructor( - public event: IPointerEvent, - public fTrigger: FEventTrigger, - ) {} -} diff --git a/projects/f-flow/src/f-draggable/f-node-move/move-preparation/index.ts b/projects/f-flow/src/f-draggable/f-node-move/move-preparation/index.ts index 461edc18..7f1a057a 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/move-preparation/index.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/move-preparation/index.ts @@ -1,3 +1,3 @@ -export * from './f-node-move-preparation.execution'; +export * from './move-node-preparation'; -export * from './f-node-move-preparation.request'; +export * from './move-node-preparation-request'; diff --git a/projects/f-flow/src/f-draggable/f-node-move/move-preparation/move-node-preparation-request.ts b/projects/f-flow/src/f-draggable/f-node-move/move-preparation/move-node-preparation-request.ts new file mode 100644 index 00000000..115e6f5b --- /dev/null +++ b/projects/f-flow/src/f-draggable/f-node-move/move-preparation/move-node-preparation-request.ts @@ -0,0 +1,11 @@ +import { FEventTrigger } from '../../../domain'; +import { IPointerEvent } from '../../../drag-toolkit'; + +export class MoveNodePreparationRequest { + static readonly fToken = Symbol('MoveNodePreparationRequest'); + + constructor( + public readonly event: IPointerEvent, + public readonly fTrigger: FEventTrigger, + ) {} +} diff --git a/projects/f-flow/src/f-draggable/f-node-move/move-preparation/f-node-move-preparation.execution.ts b/projects/f-flow/src/f-draggable/f-node-move/move-preparation/move-node-preparation.ts similarity index 51% rename from projects/f-flow/src/f-draggable/f-node-move/move-preparation/f-node-move-preparation.execution.ts rename to projects/f-flow/src/f-draggable/f-node-move/move-preparation/move-node-preparation.ts index 5331866f..b0b6ef42 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/move-preparation/f-node-move-preparation.execution.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/move-preparation/move-node-preparation.ts @@ -1,5 +1,5 @@ import { inject, Injectable } from '@angular/core'; -import { FNodeMovePreparationRequest } from './f-node-move-preparation.request'; +import { MoveNodePreparationRequest } from './move-node-preparation-request'; import { Point } from '@foblex/2d'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; import { FComponentsStore } from '../../../f-storage'; @@ -17,35 +17,35 @@ import { MoveSummaryDragHandler } from '../move-summary-drag-handler'; import { IPointerEvent } from '../../../drag-toolkit'; @Injectable() -@FExecutionRegister(FNodeMovePreparationRequest) -export class FNodeMovePreparationExecution - implements IExecution -{ +@FExecutionRegister(MoveNodePreparationRequest) +export class MoveNodePreparation implements IExecution { private readonly _mediator = inject(FMediator); private readonly _store = inject(FComponentsStore); private readonly _dragContext = inject(FDraggableDataContext); - private get _scale(): number { - return this._store.fCanvas?.transform.scale || 1; - } - - private get _fHost(): HTMLElement { - return this._store.fFlow!.hostElement; - } - - private _fNode: FNodeBase | undefined; + public handle({ event, fTrigger }: MoveNodePreparationRequest): void { + if ( + !this._dragContext.isEmpty() || + !this._isDragHandle(event.targetElement) || + !this._isValidTrigger(event, fTrigger) + ) { + return; + } - public handle({ event, fTrigger }: FNodeMovePreparationRequest): void { - if (!this._isValid(event) || !this._isValidTrigger(event, fTrigger)) { + const node = this._findDraggableNode(event.targetElement); + if (!node) { return; } - const summaryDragHandler = this._calculateDraggedItems(this._fNode!); + const scale = this._store.fCanvas?.transform.scale ?? 1; + const flowHost = this._store.flowHost; + + const summaryDragHandler = this._buildDragHandler(node); - this._dragContext.onPointerDownScale = this._scale; + this._dragContext.onPointerDownScale = scale; this._dragContext.onPointerDownPosition = Point.fromPoint(event.getPosition()) - .elementTransform(this._fHost) - .div(this._scale); + .elementTransform(flowHost) + .div(scale); this._dragContext.draggableItems = [summaryDragHandler]; if (this._store.fLineAlignment) { @@ -53,22 +53,18 @@ export class FNodeMovePreparationExecution } } - private _isValid(event: IPointerEvent): boolean { - return ( - this._dragContext.isEmpty() && - this._isDragHandleElement(event.targetElement) && - !!this._getNode(event.targetElement) - ); - } - - private _isDragHandleElement(element: HTMLElement): boolean { + private _isDragHandle(element: HTMLElement): boolean { return isClosestElementHasClass(element, '.f-drag-handle'); } - private _getNode(element: HTMLElement): FNodeBase | undefined { - this._fNode = this._store.fNodes.find((x) => x.isContains(element) && !x.fDraggingDisabled()); + private _findDraggableNode(target: HTMLElement): FNodeBase | undefined { + for (const node of this._store.fNodes) { + if (!node.fDraggingDisabled() && node.isContains(target)) { + return node; + } + } - return this._fNode; + return undefined; } private _isValidTrigger(event: IPointerEvent, fTrigger: FEventTrigger): boolean { @@ -76,21 +72,22 @@ export class FNodeMovePreparationExecution } //We drag nodes from selection model - private _calculateDraggedItems(fNode: FNodeBase): MoveSummaryDragHandler { - let result: MoveSummaryDragHandler; - if (!fNode.fSelectionDisabled()) { + private _buildDragHandler(node: FNodeBase): MoveSummaryDragHandler { + if (!node.fSelectionDisabled()) { // Need to select node before drag - setTimeout(() => { - this._mediator.execute(new SelectAndUpdateNodeLayerRequest(fNode)); - }); - - result = this._dragModelFromSelection(); - } else { - // User can drag node that can't be selected - result = this._dragModelFromSelection(fNode); + this._selectBeforeDrag(node); + + return this._dragModelFromSelection(); } - return result; + // User can drag node that can't be selected + return this._dragModelFromSelection(node); + } + + private _selectBeforeDrag(node: FNodeBase): void { + queueMicrotask(() => { + this._mediator.execute(new SelectAndUpdateNodeLayerRequest(node)); + }); } private _dragModelFromSelection(nodeWithDisabledSelection?: FNodeBase): MoveSummaryDragHandler { diff --git a/projects/f-flow/src/f-draggable/f-node-move/move-summary-drag-handler.ts b/projects/f-flow/src/f-draggable/f-node-move/move-summary-drag-handler.ts index bee02ed9..c68169bf 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/move-summary-drag-handler.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/move-summary-drag-handler.ts @@ -1,12 +1,11 @@ import { IPoint, IRect, RectExtensions } from '@foblex/2d'; import { IFDragHandler } from '../f-drag-handler'; import { MoveDragHandler } from './move-drag-handler'; -import { Injector } from "@angular/core"; -import { SnapLinesDragHandler } from "./create-snap-lines/snap-lines.drag-handler"; -import { ISnapResult } from "../../f-line-alignment"; +import { Injector } from '@angular/core'; +import { SnapLinesDragHandler } from './create-snap-lines/snap-lines.drag-handler'; +import { ISnapResult } from '../../f-line-alignment'; export class MoveSummaryDragHandler implements IFDragHandler { - public readonly fEventType = 'move-node'; public readonly fData: any; diff --git a/projects/f-flow/src/f-draggable/f-node-move/providers.ts b/projects/f-flow/src/f-draggable/f-node-move/providers.ts index f73e9895..a54c34f5 100644 --- a/projects/f-flow/src/f-draggable/f-node-move/providers.ts +++ b/projects/f-flow/src/f-draggable/f-node-move/providers.ts @@ -1,15 +1,14 @@ import { CREATE_MOVE_NODE_DRAG_MODEL_FROM_SELECTION_PROVIDERS } from './create-drag-model-from-selection'; import { FNodeMoveFinalizeExecution } from './move-finalize'; -import { FNodeMovePreparationExecution } from './move-preparation'; +import { MoveNodePreparation } from './move-preparation'; import { CreateSnapLines } from './create-snap-lines'; export const NODE_PROVIDERS = [ - ...CREATE_MOVE_NODE_DRAG_MODEL_FROM_SELECTION_PROVIDERS, CreateSnapLines, FNodeMoveFinalizeExecution, - FNodeMovePreparationExecution, + MoveNodePreparation, ]; diff --git a/projects/f-flow/src/f-draggable/f-node-rotate/rotate-preparation/f-node-rotate-preparation.execution.ts b/projects/f-flow/src/f-draggable/f-node-rotate/rotate-preparation/f-node-rotate-preparation.execution.ts index c3e602cb..84081f72 100644 --- a/projects/f-flow/src/f-draggable/f-node-rotate/rotate-preparation/f-node-rotate-preparation.execution.ts +++ b/projects/f-flow/src/f-draggable/f-node-rotate/rotate-preparation/f-node-rotate-preparation.execution.ts @@ -18,7 +18,7 @@ import { SourceConnectionDragHandler, TargetConnectionDragHandler, } from '../../f-node-move'; -import { FConnectionBase } from '../../../f-connection'; +import { FConnectionBase } from '../../../f-connection-v2'; @Injectable() @FExecutionRegister(FNodeRotatePreparationRequest) @@ -94,7 +94,7 @@ export class FNodeRotatePreparationExecution }[] { return this._mediator .execute(new CalculateInputConnectionsRequest(this._fNode!)) - .map((x: FConnectionBase) => { + .map((x) => { const connector = this._store.fInputs.find((y) => y.fId() === x.fInputId())?.hostElement; if (!connector) { @@ -115,7 +115,7 @@ export class FNodeRotatePreparationExecution }[] { return this._mediator .execute(new CalculateOutputConnectionsRequest(this._fNode!)) - .map((x: FConnectionBase) => { + .map((x) => { const connector = this._store.fOutputs.find((y) => y.fId() === x.fOutputId())?.hostElement; if (!connector) { diff --git a/projects/f-flow/src/f-draggable/f-single-select/f-single-select.request.ts b/projects/f-flow/src/f-draggable/f-single-select/f-single-select.request.ts deleted file mode 100644 index 4ac1a707..00000000 --- a/projects/f-flow/src/f-draggable/f-single-select/f-single-select.request.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FEventTrigger } from '../../domain'; -import { IPointerEvent } from "../../drag-toolkit"; - -export class FSingleSelectRequest { - static readonly fToken = Symbol('FSingleSelectRequest'); - - constructor( - public event: IPointerEvent, - public fMultiSelectTrigger: FEventTrigger, - ) { - } -} diff --git a/projects/f-flow/src/f-draggable/f-single-select/index.ts b/projects/f-flow/src/f-draggable/f-single-select/index.ts deleted file mode 100644 index c0b8db12..00000000 --- a/projects/f-flow/src/f-draggable/f-single-select/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './providers'; - -export * from './f-single-select.execution'; - -export * from './f-single-select.request'; diff --git a/projects/f-flow/src/f-draggable/f-single-select/providers.ts b/projects/f-flow/src/f-draggable/f-single-select/providers.ts deleted file mode 100644 index 659f676d..00000000 --- a/projects/f-flow/src/f-draggable/f-single-select/providers.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { FSingleSelectExecution } from './f-single-select.execution'; - -export const SINGLE_SELECT_PROVIDERS = [ - - FSingleSelectExecution, -]; diff --git a/projects/f-flow/src/f-draggable/index.ts b/projects/f-flow/src/f-draggable/index.ts index 2d986d4e..d6a8aa87 100644 --- a/projects/f-flow/src/f-draggable/index.ts +++ b/projects/f-flow/src/f-draggable/index.ts @@ -14,7 +14,9 @@ export * from './f-node-resize'; export * from './f-node-rotate'; -export * from './f-single-select'; +export * from './single-select'; + +export * from './pinch-to-zoom'; export * from './f-drag-blocker.directive'; @@ -31,5 +33,3 @@ export * from './i-f-drag-and-drop-plugin'; export * from './is-drag-blocker'; export * from './providers'; - - diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/constants/constants.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/constants/constants.ts new file mode 100644 index 00000000..9a61c2a7 --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/constants/constants.ts @@ -0,0 +1,5 @@ +export const PINCH_NORMALIZATION_FACTOR = 100; +export const PINCH_NORMALIZATION_RATIO = 1 / PINCH_NORMALIZATION_FACTOR; +export const PINCH_MOVEMENT_THRESHOLD = 0.5; +export const NORMALIZED_MIN = 0.1; +export const NORMALIZED_MAX = 1; diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/constants/index.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/constants/index.ts new file mode 100644 index 00000000..c94f80f8 --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/constants/index.ts @@ -0,0 +1 @@ +export * from './constants'; diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/finalize/index.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/finalize/index.ts new file mode 100644 index 00000000..01782b95 --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/finalize/index.ts @@ -0,0 +1,3 @@ +export * from './pinch-to-zoom-finalize'; + +export * from './pinch-to-zoom-finalize-request'; diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/finalize/pinch-to-zoom-finalize-request.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/finalize/pinch-to-zoom-finalize-request.ts new file mode 100644 index 00000000..bb017cbb --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/finalize/pinch-to-zoom-finalize-request.ts @@ -0,0 +1,6 @@ +import { IPointerEvent } from '../../../drag-toolkit'; + +export class PinchToZoomFinalizeRequest { + static readonly fToken = Symbol('PinchToZoomFinalizeRequest'); + constructor(public readonly event: IPointerEvent) {} +} diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/finalize/pinch-to-zoom-finalize.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/finalize/pinch-to-zoom-finalize.ts new file mode 100644 index 00000000..1620d85e --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/finalize/pinch-to-zoom-finalize.ts @@ -0,0 +1,24 @@ +import { inject, Injectable } from '@angular/core'; +import { PinchToZoomFinalizeRequest } from './pinch-to-zoom-finalize-request'; +import { FExecutionRegister, IExecution } from '@foblex/mediator'; +import { FDraggableDataContext } from '../../f-draggable-data-context'; +import { FNodeRotateDragHandler, FNodeRotateFinalizeRequest } from '../../f-node-rotate'; + +@Injectable() +@FExecutionRegister(PinchToZoomFinalizeRequest) +export class PinchToZoomFinalize implements IExecution { + private readonly _dragContext = inject(FDraggableDataContext); + + public handle(_request: FNodeRotateFinalizeRequest): void { + if (!this._isValid()) { + return; + } + this._dragContext.draggableItems.forEach((x) => { + x.onPointerUp?.(); + }); + } + + private _isValid(): boolean { + return this._dragContext.draggableItems.some((x) => x instanceof FNodeRotateDragHandler); + } +} diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/index.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/index.ts new file mode 100644 index 00000000..08ae3c38 --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/index.ts @@ -0,0 +1,6 @@ +export * from './constants'; +export * from './finalize'; +export * from './preparation'; +export * from './utils'; +export * from './pinch-to-zoom-handler'; +export * from './providers'; diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/pinch-to-zoom-handler.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/pinch-to-zoom-handler.ts new file mode 100644 index 00000000..5127d9b9 --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/pinch-to-zoom-handler.ts @@ -0,0 +1,94 @@ +import { Injector } from '@angular/core'; +import { IPoint, Point } from '@foblex/2d'; +import { IFDragHandler } from '../f-drag-handler'; +import { calculateTouchCenter, calculateTouchDistance } from './utils'; +import { IPointerEvent } from '../../drag-toolkit'; +import { PINCH_MOVEMENT_THRESHOLD } from './constants'; +import { FZoomBase } from '../../f-zoom'; +import { F_ZOOM_TAG } from '../../domain'; +import { FComponentsStore } from '../../f-storage'; +import { FCanvasBase } from '../../f-canvas'; + +export class PinchToZoomHandler implements IFDragHandler { + private readonly _store: FComponentsStore; + + public readonly fEventType = 'pinch-to-zoom'; + + private get _flowHost(): HTMLElement { + return this._store.fFlow?.hostElement as HTMLElement; + } + + private get _canvas(): FCanvasBase { + return this._store.fCanvas as FCanvasBase; + } + + private get _zoomComponent(): FZoomBase { + return this._store.fComponents[F_ZOOM_TAG] as FZoomBase; + } + + private _pinchStartDistance: number | null = null; + private _pinchStartScale: number | null = null; + + constructor( + _injector: Injector, + private _touches: TouchList, + ) { + this._store = _injector.get(FComponentsStore); + } + + public prepareDragSequence(): void { + const d = calculateTouchDistance(this._touches); + if (d == null) return; + + this._pinchStartDistance = d; + this._pinchStartScale = this._canvas.transform.scale; + } + + public onPointerMove(_: IPoint, event: IPointerEvent): void { + if ( + event.touches.length !== 2 || + this._pinchStartDistance == null || + this._pinchStartScale == null + ) { + return; + } + + const d = calculateTouchDistance(event.touches); + const center = calculateTouchCenter(event.touches); + if (d == null || center == null) { + this._resetPinch(); + + return; + } + + if (Math.abs(d - this._pinchStartDistance) < PINCH_MOVEMENT_THRESHOLD) { + return; + } + + event.preventDefault(); + + const ratio = d / this._pinchStartDistance; + const nextScale = this._clamp(this._pinchStartScale * ratio); + + this._canvas.setScale(nextScale, this._castPositionToFlow(center)); + this._canvas.redraw(); + } + + private _clamp(value: number): number { + return Math.max(this._zoomComponent.minimum, Math.min(value, this._zoomComponent.maximum)); + } + + private _castPositionToFlow(position: IPoint): IPoint { + return Point.fromPoint(position).elementTransform(this._flowHost); + } + + private _resetPinch(): void { + this._pinchStartScale = null; + this._pinchStartDistance = null; + } + + public onPointerUp(): void { + this._resetPinch(); + this._canvas.emitCanvasChangeEvent(); + } +} diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/preparation/index.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/preparation/index.ts new file mode 100644 index 00000000..9ed578eb --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/preparation/index.ts @@ -0,0 +1,3 @@ +export * from './pinch-to-zoom-preparation'; + +export * from './pinch-to-zoom-preparation-request'; diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/preparation/pinch-to-zoom-preparation-request.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/preparation/pinch-to-zoom-preparation-request.ts new file mode 100644 index 00000000..7d3b896a --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/preparation/pinch-to-zoom-preparation-request.ts @@ -0,0 +1,6 @@ +import { IPointerEvent } from '../../../drag-toolkit'; + +export class PinchToZoomPreparationRequest { + static readonly fToken = Symbol('PinchToZoomPreparationRequest'); + constructor(public readonly event: IPointerEvent) {} +} diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/preparation/pinch-to-zoom-preparation.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/preparation/pinch-to-zoom-preparation.ts new file mode 100644 index 00000000..95e7165a --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/preparation/pinch-to-zoom-preparation.ts @@ -0,0 +1,41 @@ +import { inject, Injectable, Injector } from '@angular/core'; +import { PinchToZoomPreparationRequest } from './pinch-to-zoom-preparation-request'; +import { FExecutionRegister, IExecution } from '@foblex/mediator'; +import { IPointerEvent } from '../../../drag-toolkit'; +import { FDraggableDataContext } from '../../f-draggable-data-context'; +import { Point } from '@foblex/2d'; +import { PinchToZoomHandler } from '../pinch-to-zoom-handler'; +import { FComponentsStore } from '../../../f-storage'; +import { F_ZOOM_TAG } from '../../../domain'; + +@Injectable() +@FExecutionRegister(PinchToZoomPreparationRequest) +export class PinchToZoomPreparation implements IExecution { + private readonly _injector = inject(Injector); + private readonly _store = inject(FComponentsStore); + private readonly _dragContext = inject(FDraggableDataContext); + + public handle({ event }: PinchToZoomPreparationRequest): void { + if (!this._isValid(event)) { + return; + } + + this._dragContext.onPointerDownScale = 1; + this._dragContext.onPointerDownPosition = new Point(); + + this._dragContext.draggableItems = [new PinchToZoomHandler(this._injector, event.touches)]; + } + + private _isValid(event: IPointerEvent): boolean { + return ( + this._dragContext.isEmpty() && + event.touches?.length === 2 && + !event.isEventInLockedContext && + this._isZoomComponent() + ); + } + + private _isZoomComponent(): boolean { + return !!this._store.fComponents[F_ZOOM_TAG]; + } +} diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/providers.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/providers.ts new file mode 100644 index 00000000..0b1617f1 --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/providers.ts @@ -0,0 +1,4 @@ +import { PinchToZoomPreparation } from './preparation'; +import { PinchToZoomFinalize } from './finalize'; + +export const PINCH_TO_ZOOM_PROVIDERS = [PinchToZoomPreparation, PinchToZoomFinalize]; diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/utils/calculate-touch-center.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/utils/calculate-touch-center.ts new file mode 100644 index 00000000..4b15dea8 --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/utils/calculate-touch-center.ts @@ -0,0 +1,15 @@ +import { IPoint, PointExtensions } from '@foblex/2d'; + +export function calculateTouchCenter(touches: TouchList): IPoint | null { + if (touches.length !== 2) { + return null; + } + + const firstTouch = touches[0]; + const secondTouch = touches[1]; + + return PointExtensions.initialize( + (firstTouch.clientX + secondTouch.clientX) / 2, + (firstTouch.clientY + secondTouch.clientY) / 2, + ); +} diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/utils/calculate-touch-distance.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/utils/calculate-touch-distance.ts new file mode 100644 index 00000000..c3c32056 --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/utils/calculate-touch-distance.ts @@ -0,0 +1,13 @@ +export function calculateTouchDistance(touches: TouchList): number | null { + if (touches.length !== 2) { + return null; + } + + const firstTouch = touches[0]; + const secondTouch = touches[1]; + + return Math.hypot( + secondTouch.clientX - firstTouch.clientX, + secondTouch.clientY - firstTouch.clientY, + ); +} diff --git a/projects/f-flow/src/f-draggable/pinch-to-zoom/utils/index.ts b/projects/f-flow/src/f-draggable/pinch-to-zoom/utils/index.ts new file mode 100644 index 00000000..faa785f1 --- /dev/null +++ b/projects/f-flow/src/f-draggable/pinch-to-zoom/utils/index.ts @@ -0,0 +1,2 @@ +export * from './calculate-touch-center'; +export * from './calculate-touch-distance'; diff --git a/projects/f-flow/src/f-draggable/providers.ts b/projects/f-flow/src/f-draggable/providers.ts index d3474c40..110ed192 100644 --- a/projects/f-flow/src/f-draggable/providers.ts +++ b/projects/f-flow/src/f-draggable/providers.ts @@ -1,6 +1,6 @@ import { CANVAS_PROVIDERS } from './f-canvas'; import { CONNECTIONS_PROVIDERS } from './f-connection'; -import { SINGLE_SELECT_PROVIDERS } from './f-single-select'; +import { SINGLE_SELECT_PROVIDERS } from './single-select'; import { NODE_PROVIDERS } from './f-node-move'; import { NODE_RESIZE_PROVIDERS } from './f-node-resize'; import { F_MINIMAP_DRAG_AND_DROP_PROVIDERS } from '../f-minimap/domain/providers'; @@ -9,9 +9,9 @@ import { F_SELECTION_AREA_DRAG_AND_DROP_PROVIDERS } from '../f-selection-area'; import { DRAG_AND_DROP_COMMON_PROVIDERS } from './domain'; import { NODE_DROP_TO_GROUP_PROVIDERS } from './f-drop-to-group'; import { NODE_ROTATE_PROVIDERS } from './f-node-rotate'; +import { PINCH_TO_ZOOM_PROVIDERS } from './pinch-to-zoom'; export const F_DRAGGABLE_PROVIDERS = [ - ...CANVAS_PROVIDERS, ...CONNECTIONS_PROVIDERS, @@ -33,4 +33,6 @@ export const F_DRAGGABLE_PROVIDERS = [ ...F_SELECTION_AREA_DRAG_AND_DROP_PROVIDERS, ...F_MINIMAP_DRAG_AND_DROP_PROVIDERS, + + ...PINCH_TO_ZOOM_PROVIDERS, ]; diff --git a/projects/f-flow/src/f-draggable/single-select/index.ts b/projects/f-flow/src/f-draggable/single-select/index.ts new file mode 100644 index 00000000..69bebb4a --- /dev/null +++ b/projects/f-flow/src/f-draggable/single-select/index.ts @@ -0,0 +1,5 @@ +export * from './providers'; + +export * from './single-select'; + +export * from './single-select-request'; diff --git a/projects/f-flow/src/f-draggable/single-select/providers.ts b/projects/f-flow/src/f-draggable/single-select/providers.ts new file mode 100644 index 00000000..9ebf3cdd --- /dev/null +++ b/projects/f-flow/src/f-draggable/single-select/providers.ts @@ -0,0 +1,3 @@ +import { SingleSelect } from './single-select'; + +export const SINGLE_SELECT_PROVIDERS = [SingleSelect]; diff --git a/projects/f-flow/src/f-draggable/single-select/single-select-request.ts b/projects/f-flow/src/f-draggable/single-select/single-select-request.ts new file mode 100644 index 00000000..40f2c9ac --- /dev/null +++ b/projects/f-flow/src/f-draggable/single-select/single-select-request.ts @@ -0,0 +1,11 @@ +import { FEventTrigger } from '../../domain'; +import { IPointerEvent } from '../../drag-toolkit'; + +export class SingleSelectRequest { + static readonly fToken = Symbol('SingleSelectRequest'); + + constructor( + public readonly event: IPointerEvent, + public readonly trigger: FEventTrigger, + ) {} +} diff --git a/projects/f-flow/src/f-draggable/f-single-select/f-single-select.execution.ts b/projects/f-flow/src/f-draggable/single-select/single-select.ts similarity index 79% rename from projects/f-flow/src/f-draggable/f-single-select/f-single-select.execution.ts rename to projects/f-flow/src/f-draggable/single-select/single-select.ts index 5aa7cc84..b4908c68 100644 --- a/projects/f-flow/src/f-draggable/f-single-select/f-single-select.execution.ts +++ b/projects/f-flow/src/f-draggable/single-select/single-select.ts @@ -1,13 +1,13 @@ import { inject, Injectable } from '@angular/core'; import { FExecutionRegister, FMediator, IExecution } from '@foblex/mediator'; -import { FSingleSelectRequest } from './f-single-select.request'; +import { SingleSelectRequest } from './single-select-request'; import { isValidEventTrigger, UpdateItemAndChildrenLayersRequest } from '../../domain'; -import { FConnectionBase } from '../../f-connection'; import { FComponentsStore } from '../../f-storage'; import { FDraggableDataContext } from '../f-draggable-data-context'; import { ISelectable } from '../../mixins'; import { FNodeBase } from '../../f-node'; import { IPointerEvent } from '../../drag-toolkit'; +import { FConnectionBase } from '../../f-connection-v2'; /** * Implements the functionality for selecting elements in a graphical interface. @@ -48,14 +48,18 @@ import { IPointerEvent } from '../../drag-toolkit'; */ @Injectable() -@FExecutionRegister(FSingleSelectRequest) -export class FSingleSelectExecution implements IExecution { - private readonly _fMediator = inject(FMediator); +@FExecutionRegister(SingleSelectRequest) +export class SingleSelect implements IExecution { + private readonly _mediator = inject(FMediator); private readonly _store = inject(FComponentsStore); private readonly _dragContext = inject(FDraggableDataContext); - public handle(request: FSingleSelectRequest): void { - if (!this._isValid(request)) { + private get _flowHost(): HTMLElement { + return this._store.flowHost; + } + + public handle(request: SingleSelectRequest): void { + if (!this._isValid(request.event)) { return; } @@ -66,12 +70,12 @@ export class FSingleSelectExecution implements IExecution - c.isContains(element) || - c.fConnectionCenter()?.nativeElement?.contains(element) || - Array.from(c.fConnectionContents()?.values() ?? []).some((content) => - content.hostElement?.contains(element), - ), - ); + return this._store.fConnections.find((c) => c.isContains(element)); } private _updateItemAndChildrenLayers(fItem?: ISelectable): void { if (fItem) { - this._fMediator.execute( - new UpdateItemAndChildrenLayersRequest(fItem, fItem.hostElement.parentElement!), + this._mediator.execute( + new UpdateItemAndChildrenLayersRequest( + fItem, + fItem.hostElement.parentElement as HTMLElement, + ), ); } } - private _isMultiSelect(request: FSingleSelectRequest): boolean { - return isValidEventTrigger(request.event.originalEvent, request.fMultiSelectTrigger); + private _isMultiSelect({ event, trigger }: SingleSelectRequest): boolean { + return isValidEventTrigger(event.originalEvent, trigger); } private _singleSelect(fItem?: ISelectable): void { diff --git a/projects/f-flow/src/f-flow.module.ts b/projects/f-flow/src/f-flow.module.ts index 58bd4dcb..5cf76ca3 100644 --- a/projects/f-flow/src/f-flow.module.ts +++ b/projects/f-flow/src/f-flow.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { F_CONNECTION_PROVIDERS } from './f-connection'; +import { F_CONNECTION_IMPORTS_EXPORTS, F_CONNECTION_PROVIDERS } from './f-connection'; import { F_NODE_PROVIDERS } from './f-node'; import { F_BACKGROUND_PROVIDERS } from './f-backgroud'; import { F_CONNECTORS_PROVIDERS } from './f-connectors'; @@ -32,6 +32,8 @@ import { F_EXTERNAL_ITEM_PROVIDERS } from './f-external-item'; ...F_ZOOM_PROVIDERS, ...F_EXTERNAL_ITEM_PROVIDERS, + ...F_CONNECTION_IMPORTS_EXPORTS, + CommonModule, ], exports: [ @@ -41,6 +43,8 @@ import { F_EXTERNAL_ITEM_PROVIDERS } from './f-external-item'; ...F_ZOOM_PROVIDERS, ...F_EXTERNAL_ITEM_PROVIDERS, + ...F_CONNECTION_IMPORTS_EXPORTS, + ...F_CONNECTION_PROVIDERS, ...F_CONNECTORS_PROVIDERS, ...F_LINE_ALIGNMENT_PROVIDERS, @@ -52,5 +56,4 @@ import { F_EXTERNAL_ITEM_PROVIDERS } from './f-external-item'; FDraggableDirective, ], }) -export class FFlowModule { -} +export class FFlowModule {} diff --git a/projects/f-flow/src/f-flow/f-flow.component.scss b/projects/f-flow/src/f-flow/f-flow.component.scss index 7c984204..c1b48db6 100644 --- a/projects/f-flow/src/f-flow/f-flow.component.scss +++ b/projects/f-flow/src/f-flow/f-flow.component.scss @@ -32,7 +32,6 @@ z-index: 1; } - .f-connection-center, .f-connection-content { z-index: 3; } diff --git a/projects/f-flow/src/f-flow/f-flow.component.ts b/projects/f-flow/src/f-flow/f-flow.component.ts index 791a2d1b..08e74e90 100644 --- a/projects/f-flow/src/f-flow/f-flow.component.ts +++ b/projects/f-flow/src/f-flow/f-flow.component.ts @@ -1,7 +1,14 @@ import { AfterContentInit, ChangeDetectionStrategy, - Component, DestroyRef, ElementRef, inject, input, OnDestroy, OnInit, output, + Component, + DestroyRef, + ElementRef, + inject, + input, + OnDestroy, + OnInit, + output, } from '@angular/core'; import { F_FLOW, FFlowBase } from './f-flow-base'; import { @@ -13,14 +20,15 @@ import { SelectAllRequest, SelectRequest, IFFlowState, - GetFlowStateRequest, RemoveFlowFromStoreRequest, AddFlowToStoreRequest, SortItemLayersRequest, ICurrentSelection, + CalculateFlowStateRequest, + RemoveFlowFromStoreRequest, + AddFlowToStoreRequest, + SortItemLayersRequest, + ICurrentSelection, } from '../domain'; import { IPoint, IRect } from '@foblex/2d'; import { FMediator } from '@foblex/mediator'; -import { - FDraggableDataContext, -} from '../f-draggable'; -import { FConnectionFactory } from '../f-connection'; +import { FDraggableDataContext } from '../f-draggable'; import { NotifyDataChangedRequest, F_STORAGE_PROVIDERS, @@ -30,7 +38,11 @@ import { import { BrowserService } from '@foblex/platform'; import { COMMON_PROVIDERS } from '../domain'; import { F_DRAGGABLE_PROVIDERS } from '../f-draggable'; -import { FChannelHub } from '../reactivity'; +import { FChannelHub, takeOne } from '../reactivity'; +import { + ConnectionBehaviourBuilder, + ConnectionLineBuilder, +} from '../f-connection-v2'; let uniqueId = 0; @@ -41,13 +53,14 @@ let uniqueId = 0; standalone: true, host: { '[attr.id]': 'fId()', - class: "f-component f-flow", + class: 'f-component f-flow', }, providers: [ FMediator, ...F_STORAGE_PROVIDERS, FDraggableDataContext, - FConnectionFactory, + ConnectionLineBuilder, + ConnectionBehaviourBuilder, ...COMMON_PROVIDERS, ...F_DRAGGABLE_PROVIDERS, { provide: F_FLOW, useExisting: FFlowComponent }, @@ -55,9 +68,8 @@ let uniqueId = 0; changeDetection: ChangeDetectionStrategy.OnPush, }) export class FFlowComponent extends FFlowBase implements OnInit, AfterContentInit, OnDestroy { - private readonly _destroyRef = inject(DestroyRef); - private readonly _fMediator = inject(FMediator); + private readonly _mediator = inject(FMediator); private readonly _browserService = inject(BrowserService); private readonly _elementReference = inject(ElementRef); @@ -72,7 +84,7 @@ export class FFlowComponent extends FFlowBase implements OnInit, AfterContentIni private _isLoaded: boolean = false; public ngOnInit(): void { - this._fMediator.execute(new AddFlowToStoreRequest(this)); + this._mediator.execute(new AddFlowToStoreRequest(this)); } public ngAfterContentInit(): void { @@ -84,21 +96,21 @@ export class FFlowComponent extends FFlowBase implements OnInit, AfterContentIni } private _listenCountChanges(): void { - this._fMediator.execute( - new ListenCountChangesRequest(), - ).listen(this._destroyRef, () => { - this._fMediator.execute(new SortItemLayersRequest()) - }); + this._mediator + .execute(new ListenCountChangesRequest()) + .listen(this._destroyRef, () => { + this._mediator.execute(new SortItemLayersRequest()); + }); } private _listenDataChanges(): void { - this._fMediator.execute( - new ListenDataChangesRequest(), - ).listen(this._destroyRef, () => { - this._fMediator.execute(new RedrawConnectionsRequest()); + this._mediator + .execute(new ListenDataChangesRequest()) + .listen(this._destroyRef, () => { + this._mediator.execute(new RedrawConnectionsRequest()); - this._emitLoaded(); - }); + this._emitLoaded(); + }); } private _emitLoaded(): void { @@ -109,7 +121,7 @@ export class FFlowComponent extends FFlowBase implements OnInit, AfterContentIni } public redraw(): void { - this._fMediator.execute(new NotifyDataChangedRequest()); + this._mediator.execute(new NotifyDataChangedRequest()); } public reset(): void { @@ -117,25 +129,30 @@ export class FFlowComponent extends FFlowBase implements OnInit, AfterContentIni } public getNodesBoundingBox(): IRect | null { - return this._fMediator.execute(new CalculateNodesBoundingBoxNormalizedPositionRequest()); + return this._mediator.execute( + new CalculateNodesBoundingBoxNormalizedPositionRequest(), + ); } public getSelection(): ICurrentSelection { - return this._fMediator.execute(new GetCurrentSelectionRequest()); + return this._mediator.execute(new GetCurrentSelectionRequest()); } public getPositionInFlow(position: IPoint): IRect { - return this._fMediator.execute(new GetNormalizedPointRequest(position)); + return this._mediator.execute(new GetNormalizedPointRequest(position)); } public getState(): IFFlowState { - return this._fMediator.execute(new GetFlowStateRequest()); + return this._mediator.execute(new CalculateFlowStateRequest()); } public selectAll(): void { - setTimeout(() => { - this._fMediator.execute(new SelectAllRequest()); - }); + this._mediator + .execute(new ListenDataChangesRequest()) + .pipe(takeOne()) + .listen(this._destroyRef, () => { + this._mediator.execute(new SelectAllRequest()); + }); } /** @@ -151,16 +168,19 @@ export class FFlowComponent extends FFlowBase implements OnInit, AfterContentIni * triggering a `fSelectionChange` event on the next user interaction. */ public select(nodes: string[], connections: string[], isSelectedChanged: boolean = true): void { - setTimeout(() => { - this._fMediator.execute(new SelectRequest(nodes, connections, isSelectedChanged)); - }); + this._mediator + .execute(new ListenDataChangesRequest()) + .pipe(takeOne()) + .listen(this._destroyRef, () => { + this._mediator.execute(new SelectRequest(nodes, connections, isSelectedChanged)); + }); } public clearSelection(): void { - this._fMediator.execute(new ClearSelectionRequest()); + this._mediator.execute(new ClearSelectionRequest()); } public ngOnDestroy(): void { - this._fMediator.execute(new RemoveFlowFromStoreRequest()); + this._mediator.execute(new RemoveFlowFromStoreRequest()); } } diff --git a/projects/f-flow/src/f-line-alignment/f-line-alignment-base.ts b/projects/f-flow/src/f-line-alignment/f-line-alignment-base.ts index fe66ad8c..d327ab17 100644 --- a/projects/f-flow/src/f-line-alignment/f-line-alignment-base.ts +++ b/projects/f-flow/src/f-line-alignment/f-line-alignment-base.ts @@ -1,12 +1,9 @@ -import { Directive, InjectionToken, InputSignalWithTransform } from '@angular/core'; -import { IHasHostElement } from '../i-has-host-element'; +import { Directive, InjectionToken, Signal } from '@angular/core'; export const F_LINE_ALIGNMENT = new InjectionToken('F_LINE_ALIGNMENT'); @Directive() -export abstract class FLineAlignmentBase implements IHasHostElement { - +export abstract class FLineAlignmentBase { public abstract hostElement: HTMLElement; - - public abstract fAlignThreshold: InputSignalWithTransform; + public abstract fAlignThreshold: Signal; } diff --git a/projects/f-flow/src/f-line-alignment/f-line-alignment.component.ts b/projects/f-flow/src/f-line-alignment/f-line-alignment.component.ts index fa6f0db7..8a371204 100644 --- a/projects/f-flow/src/f-line-alignment/f-line-alignment.component.ts +++ b/projects/f-flow/src/f-line-alignment/f-line-alignment.component.ts @@ -1,40 +1,37 @@ -import { Component, ElementRef, inject, input, numberAttribute, OnDestroy, OnInit } from '@angular/core'; -import { F_LINE_ALIGNMENT, FLineAlignmentBase } from './f-line-alignment-base'; import { - RemoveLineAlignmentFromStoreRequest, - AddLineAlignmentToStoreRequest, -} from '../domain'; + Component, + ElementRef, + inject, + input, + numberAttribute, + OnDestroy, + OnInit, +} from '@angular/core'; +import { F_LINE_ALIGNMENT, FLineAlignmentBase } from './f-line-alignment-base'; +import { RemoveLineAlignmentFromStoreRequest, AddLineAlignmentToStoreRequest } from '../domain'; import { FMediator } from '@foblex/mediator'; @Component({ - selector: "f-line-alignment", - template: "", - styleUrls: [ "./f-line-alignment.component.scss" ], - exportAs: "fComponent", + selector: 'f-line-alignment', + template: '', + styleUrls: ['./f-line-alignment.component.scss'], + exportAs: 'fComponent', host: { 'class': 'f-line-alignment f-component', }, - providers: [ - { provide: F_LINE_ALIGNMENT, useExisting: FLineAlignmentComponent }, - ], + providers: [{ provide: F_LINE_ALIGNMENT, useExisting: FLineAlignmentComponent }], }) -export class FLineAlignmentComponent - extends FLineAlignmentBase implements OnInit, OnDestroy { - - public override fAlignThreshold = input(10, { transform: numberAttribute }); +export class FLineAlignmentComponent extends FLineAlignmentBase implements OnInit, OnDestroy { + public override readonly fAlignThreshold = input(10, { transform: numberAttribute }); - private readonly _fMediator = inject(FMediator); - private readonly _elementReference = inject(ElementRef); - - public override get hostElement(): HTMLElement { - return this._elementReference.nativeElement; - } + private readonly _mediator = inject(FMediator); + public readonly hostElement = inject(ElementRef).nativeElement; public ngOnInit(): void { - this._fMediator.execute(new AddLineAlignmentToStoreRequest(this)); + this._mediator.execute(new AddLineAlignmentToStoreRequest(this)); } public ngOnDestroy(): void { - this._fMediator.execute(new RemoveLineAlignmentFromStoreRequest()); + this._mediator.execute(new RemoveLineAlignmentFromStoreRequest()); } } diff --git a/projects/f-flow/src/f-minimap/f-minimap.component.ts b/projects/f-flow/src/f-minimap/f-minimap.component.ts index 89554cca..393e3ff1 100644 --- a/projects/f-flow/src/f-minimap/f-minimap.component.ts +++ b/projects/f-flow/src/f-minimap/f-minimap.component.ts @@ -1,7 +1,12 @@ import { - AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, - inject, input, viewChild, -} from "@angular/core"; + AfterViewInit, + ChangeDetectionStrategy, + Component, + DestroyRef, + inject, + input, + viewChild, +} from '@angular/core'; import { FMediator } from '@foblex/mediator'; import { FMinimapFlowDirective } from './f-minimap-flow.directive'; import { FMinimapCanvasDirective } from './f-minimap-canvas.directive'; @@ -11,30 +16,27 @@ import { MinimapDragFinalizeRequest, MinimapDragPreparationRequest } from './dom import { ListenTransformChangesRequest } from '../f-storage'; import { debounceTime, FChannelHub, notifyOnStart } from '../reactivity'; import { BrowserService } from '@foblex/platform'; -import { IPointerEvent } from "../drag-toolkit"; +import { IPointerEvent } from '../drag-toolkit'; @Component({ selector: 'f-minimap', templateUrl: './f-minimap.component.html', - styleUrls: [ './f-minimap.component.scss' ], + styleUrls: ['./f-minimap.component.scss'], exportAs: 'fComponent', host: { 'class': 'f-component f-minimap', }, - providers: [ - { provide: F_BEFORE_MAIN_PLUGIN, useExisting: FMinimapComponent }, - ], + providers: [{ provide: F_BEFORE_MAIN_PLUGIN, useExisting: FMinimapComponent }], changeDetection: ChangeDetectionStrategy.OnPush, }) export class FMinimapComponent implements AfterViewInit, IFDragAndDropPlugin { - private readonly _destroyRef = inject(DestroyRef); private readonly _mediator = inject(FMediator); private readonly _browser = inject(BrowserService); - public readonly _canvas= viewChild.required(FMinimapCanvasDirective); + public readonly _canvas = viewChild.required(FMinimapCanvasDirective); public readonly _flow = viewChild.required(FMinimapFlowDirective); - public readonly _minimapView= viewChild.required(FMinimapViewDirective); + public readonly _minimapView = viewChild.required(FMinimapViewDirective); public readonly fMinSize = input(1000); @@ -43,11 +45,12 @@ export class FMinimapComponent implements AfterViewInit, IFDragAndDropPlugin { } private _listenTransformChanges(): void { - this._mediator.execute(new ListenTransformChangesRequest()).pipe( - notifyOnStart(), debounceTime(2), - ).listen(this._destroyRef, () => { - this._redraw() - }); + this._mediator + .execute(new ListenTransformChangesRequest()) + .pipe(notifyOnStart(), debounceTime(2)) + .listen(this._destroyRef, () => { + this._redraw(); + }); } private _redraw(): void { diff --git a/projects/f-flow/src/f-selection-area/domain/selection-area.drag-handle.ts b/projects/f-flow/src/f-selection-area/domain/selection-area.drag-handle.ts index 87ac4279..3313d8e9 100644 --- a/projects/f-flow/src/f-selection-area/domain/selection-area.drag-handle.ts +++ b/projects/f-flow/src/f-selection-area/domain/selection-area.drag-handle.ts @@ -1,36 +1,37 @@ -import { IPoint, Point, PointExtensions, RectExtensions } from '@foblex/2d'; +import { IPoint, ITransformModel, Point, PointExtensions, RectExtensions } from '@foblex/2d'; import { FComponentsStore, NotifyTransformChangedRequest } from '../../f-storage'; -import { GetCanBeSelectedItemsRequest, ICanBeSelectedElementAndRect } from '../../domain'; +import { CalculateSelectableItemsRequest, ICanBeSelectedElementAndRect } from '../../domain'; import { FMediator } from '@foblex/mediator'; import { FDraggableDataContext, IFDragHandler } from '../../f-draggable'; import { FSelectionAreaBase } from '../f-selection-area-base'; import { ISelectable } from '../../mixins'; export class SelectionAreaDragHandle implements IFDragHandler { - - public fEventType: string = 'selection-area'; + public readonly fEventType = 'selection-area'; private _canBeSelected: ICanBeSelectedElementAndRect[] = []; private _selectedByMove: ISelectable[] = []; - private get _fCanvasPosition(): IPoint { - return Point.fromPoint(this._store.fCanvas!.transform.position) - .add(this._store.fCanvas!.transform.scaledPosition); + private get _transform(): ITransformModel { + return this._store.fCanvas?.transform as ITransformModel; + } + + private get _canvasPosition(): IPoint { + return Point.fromPoint(this._transform.position).add(this._transform.scaledPosition); } constructor( private _store: FComponentsStore, - private _fSelectionArea: FSelectionAreaBase, + private _selectionArea: FSelectionAreaBase, private _dragContext: FDraggableDataContext, - private _fMediator: FMediator, - ) { - } + private _mediator: FMediator, + ) {} public prepareDragSequence(): void { - this._canBeSelected = this._fMediator.execute(new GetCanBeSelectedItemsRequest()); + this._canBeSelected = this._mediator.execute(new CalculateSelectableItemsRequest()); - this._fSelectionArea.show(); - this._fSelectionArea.draw( + this._selectionArea.show(); + this._selectionArea.draw( RectExtensions.initialize( this._dragContext.onPointerDownPosition.x, this._dragContext.onPointerDownPosition.y, @@ -46,34 +47,30 @@ export class SelectionAreaDragHandle implements IFDragHandler { const width = Math.abs(difference.x); const height = Math.abs(difference.y); - const fSelectionAreaRect = RectExtensions.initialize(point.x, point.y, width, height); + const _selectionAreaRect = RectExtensions.initialize(point.x, point.y, width, height); - this._fSelectionArea.draw(fSelectionAreaRect); + this._selectionArea.draw(_selectionAreaRect); this._selectedByMove = []; this._canBeSelected.forEach((item) => { item.element.unmarkAsSelected(); - const fItemRect = RectExtensions.addPoint(item.fRect, this._fCanvasPosition); + const _itemRect = RectExtensions.addPoint(item.fRect, this._canvasPosition); - const isIntersect = RectExtensions.intersectionWithRect(fItemRect, fSelectionAreaRect); + const isIntersect = RectExtensions.intersectionWithRect(_itemRect, _selectionAreaRect); if (isIntersect) { - item.element.markAsSelected(); this._selectedByMove.push(item.element); } }); - this._fMediator.execute(new NotifyTransformChangedRequest()); + this._mediator.execute(new NotifyTransformChangedRequest()); } private _getMinimumPoint(point1: IPoint, point2: IPoint): IPoint { - return PointExtensions.initialize( - Math.min(point1.x, point2.x), - Math.min(point1.y, point2.y), - ); + return PointExtensions.initialize(Math.min(point1.x, point2.x), Math.min(point1.y, point2.y)); } public onPointerUp(): void { - this._fSelectionArea.hide(); + this._selectionArea.hide(); this._dragContext.selectedItems.push(...this._selectedByMove); if (this._selectedByMove.length > 0) { this._dragContext.isSelectedChanged = true; diff --git a/projects/f-flow/src/f-storage/f-components-store.ts b/projects/f-flow/src/f-storage/f-components-store.ts index 0786ead2..ef969812 100644 --- a/projects/f-flow/src/f-storage/f-components-store.ts +++ b/projects/f-flow/src/f-storage/f-components-store.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { FConnectionBase, FMarkerBase } from '../f-connection'; import { FFlowBase } from '../f-flow'; import { FCanvasBase } from '../f-canvas'; import { FBackgroundBase } from '../f-backgroud'; @@ -8,7 +7,7 @@ import { FConnectorBase } from '../f-connectors'; import { FDraggableBase } from '../f-draggable'; import { FChannel } from '../reactivity'; import { FLineAlignmentBase } from '../f-line-alignment'; -import { IMap } from '../domain'; +import { FConnectionBase, FConnectionMarkerBase } from '../f-connection-v2'; @Injectable() export class FComponentsStore { @@ -19,10 +18,10 @@ export class FComponentsStore { public readonly countChanges$ = new FChannel(); public get flowHost(): HTMLElement { - return this.fFlow?.hostElement!; + return this.fFlow?.hostElement as HTMLElement; } - public fComponents: IMap = {}; + public fComponents: Record = {}; public fFlow: FFlowBase | undefined; @@ -38,7 +37,7 @@ export class FComponentsStore { public fSnapConnection: FConnectionBase | undefined; - public fMarkers: FMarkerBase[] = []; + public fMarkers: FConnectionMarkerBase[] = []; public fOutputs: FConnectorBase[] = []; diff --git a/projects/f-flow/src/f-storage/f-connectors-store.ts b/projects/f-flow/src/f-storage/f-connectors-store.ts deleted file mode 100644 index 697d4d59..00000000 --- a/projects/f-flow/src/f-storage/f-connectors-store.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Injectable } from '@angular/core'; -import { EFConnectableSide } from '../f-connectors'; - -@Injectable() -export class FConnectorsStore { - private readonly _connectors: Record> = {}; -} diff --git a/projects/f-flow/src/f-storage/features/listen-components-data-changed/index.ts b/projects/f-flow/src/f-storage/features/listen-components-data-changed/index.ts index 4644b555..863baa7f 100644 --- a/projects/f-flow/src/f-storage/features/listen-components-data-changed/index.ts +++ b/projects/f-flow/src/f-storage/features/listen-components-data-changed/index.ts @@ -1,3 +1,3 @@ -export * from './listen-data-changes.execution'; +export * from './listen-data-changes'; export * from './listen-data-changes-request'; diff --git a/projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes-request.ts b/projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes-request.ts index 91f0bbd2..8feead97 100644 --- a/projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes-request.ts +++ b/projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes-request.ts @@ -1,4 +1,3 @@ export class ListenDataChangesRequest { static readonly fToken = Symbol('ListenDataChangesRequest'); - } diff --git a/projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes.execution.ts b/projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes.ts similarity index 85% rename from projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes.execution.ts rename to projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes.ts index 3b7fbca0..3eb695b2 100644 --- a/projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes.execution.ts +++ b/projects/f-flow/src/f-storage/features/listen-components-data-changed/listen-data-changes.ts @@ -6,9 +6,7 @@ import { debounceTime, FChannelHub, notifyOnStart } from '../../../reactivity'; @Injectable() @FExecutionRegister(ListenDataChangesRequest) -export class ListenDataChangesExecution - implements IExecution -{ +export class ListenDataChanges implements IExecution { private readonly _store = inject(FComponentsStore); public handle(_request: ListenDataChangesRequest): FChannelHub { diff --git a/projects/f-flow/src/f-storage/features/listen-count-changes/index.ts b/projects/f-flow/src/f-storage/features/listen-count-changes/index.ts index ebba0cec..4b6d34b4 100644 --- a/projects/f-flow/src/f-storage/features/listen-count-changes/index.ts +++ b/projects/f-flow/src/f-storage/features/listen-count-changes/index.ts @@ -1,3 +1,3 @@ -export * from './listen-count-changes.execution'; +export * from './listen-count-changes'; export * from './listen-count-changes-request'; diff --git a/projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes-request.ts b/projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes-request.ts index cdcd19b3..9c116ae1 100644 --- a/projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes-request.ts +++ b/projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes-request.ts @@ -1,4 +1,3 @@ export class ListenCountChangesRequest { static readonly fToken = Symbol('ListenCountChangesRequest'); - } diff --git a/projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes.execution.ts b/projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes.ts similarity index 85% rename from projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes.execution.ts rename to projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes.ts index c02a834b..10c6d0ac 100644 --- a/projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes.execution.ts +++ b/projects/f-flow/src/f-storage/features/listen-count-changes/listen-count-changes.ts @@ -6,9 +6,7 @@ import { debounceTime, FChannelHub, notifyOnStart } from '../../../reactivity'; @Injectable() @FExecutionRegister(ListenCountChangesRequest) -export class ListenCountChangesExecution - implements IExecution -{ +export class ListenCountChanges implements IExecution { private readonly _store = inject(FComponentsStore); public handle(_request: ListenCountChangesRequest): FChannelHub { diff --git a/projects/f-flow/src/f-storage/features/listen-transform-changes/index.ts b/projects/f-flow/src/f-storage/features/listen-transform-changes/index.ts index 9b071547..e81ac116 100644 --- a/projects/f-flow/src/f-storage/features/listen-transform-changes/index.ts +++ b/projects/f-flow/src/f-storage/features/listen-transform-changes/index.ts @@ -1,3 +1,3 @@ -export * from './listen-transform-changes.execution'; +export * from './listen-transform-changes'; -export * from './listen-transform-changes.request'; +export * from './listen-transform-changes-request'; diff --git a/projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes.request.ts b/projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes-request.ts similarity index 99% rename from projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes.request.ts rename to projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes-request.ts index c8cf37e1..e3e5b57b 100644 --- a/projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes.request.ts +++ b/projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes-request.ts @@ -1,4 +1,3 @@ export class ListenTransformChangesRequest { static readonly fToken = Symbol('ListenTransformChangesRequest'); - } diff --git a/projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes.execution.ts b/projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes.ts similarity index 91% rename from projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes.execution.ts rename to projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes.ts index caa6bb6d..20d3ccd7 100644 --- a/projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes.execution.ts +++ b/projects/f-flow/src/f-storage/features/listen-transform-changes/listen-transform-changes.ts @@ -1,4 +1,4 @@ -import { ListenTransformChangesRequest } from './listen-transform-changes.request'; +import { ListenTransformChangesRequest } from './listen-transform-changes-request'; import { inject, Injectable } from '@angular/core'; import { FComponentsStore } from '../../index'; import { FExecutionRegister, IExecution } from '@foblex/mediator'; @@ -6,7 +6,7 @@ import { FChannelHub } from '../../../reactivity'; @Injectable() @FExecutionRegister(ListenTransformChangesRequest) -export class ListenTransformChangesExecution +export class ListenTransformChanges implements IExecution { private readonly _store = inject(FComponentsStore); diff --git a/projects/f-flow/src/f-storage/features/notify-data-changed/index.ts b/projects/f-flow/src/f-storage/features/notify-data-changed/index.ts index 7572e29d..bdf7bb3e 100644 --- a/projects/f-flow/src/f-storage/features/notify-data-changed/index.ts +++ b/projects/f-flow/src/f-storage/features/notify-data-changed/index.ts @@ -1,3 +1,3 @@ -export * from './notify-data-changed.execution'; +export * from './notify-data-changed'; export * from './notify-data-changed-request'; diff --git a/projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed-request.ts b/projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed-request.ts index c06f05fe..44b97800 100644 --- a/projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed-request.ts +++ b/projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed-request.ts @@ -1,4 +1,3 @@ export class NotifyDataChangedRequest { static readonly fToken = Symbol('NotifyDataChangedRequest'); - } diff --git a/projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed.execution.ts b/projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed.ts similarity index 82% rename from projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed.execution.ts rename to projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed.ts index dc8f6dd7..8942b022 100644 --- a/projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed.execution.ts +++ b/projects/f-flow/src/f-storage/features/notify-data-changed/notify-data-changed.ts @@ -5,7 +5,7 @@ import { FComponentsStore } from '../../../f-storage'; @Injectable() @FExecutionRegister(NotifyDataChangedRequest) -export class NotifyDataChangedExecution implements IExecution { +export class NotifyDataChanged implements IExecution { private readonly _store = inject(FComponentsStore); public handle(_request: NotifyDataChangedRequest): void { diff --git a/projects/f-flow/src/f-storage/features/notify-transform-changed/index.ts b/projects/f-flow/src/f-storage/features/notify-transform-changed/index.ts index 02489a0c..ec3f5ac3 100644 --- a/projects/f-flow/src/f-storage/features/notify-transform-changed/index.ts +++ b/projects/f-flow/src/f-storage/features/notify-transform-changed/index.ts @@ -1,3 +1,3 @@ -export * from './notify-transform-changed.execution'; +export * from './notify-transform-changed'; -export * from './notify-transform-changed.request'; +export * from './notify-transform-changed-request'; diff --git a/projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed.request.ts b/projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed-request.ts similarity index 99% rename from projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed.request.ts rename to projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed-request.ts index 78f68d9d..19df0377 100644 --- a/projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed.request.ts +++ b/projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed-request.ts @@ -1,4 +1,3 @@ export class NotifyTransformChangedRequest { static readonly fToken = Symbol('NotifyTransformChangedRequest'); - } diff --git a/projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed.execution.ts b/projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed.ts similarity index 89% rename from projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed.execution.ts rename to projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed.ts index bc439008..264cf088 100644 --- a/projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed.execution.ts +++ b/projects/f-flow/src/f-storage/features/notify-transform-changed/notify-transform-changed.ts @@ -1,11 +1,11 @@ -import { NotifyTransformChangedRequest } from './notify-transform-changed.request'; +import { NotifyTransformChangedRequest } from './notify-transform-changed-request'; import { inject, Injectable } from '@angular/core'; import { FExecutionRegister, IExecution } from '@foblex/mediator'; import { FComponentsStore } from '../../f-components-store'; @Injectable() @FExecutionRegister(NotifyTransformChangedRequest) -export class NotifyTransformChangedExecution +export class NotifyTransformChanged implements IExecution { private readonly _store = inject(FComponentsStore); diff --git a/projects/f-flow/src/f-storage/index.ts b/projects/f-flow/src/f-storage/index.ts index 211d9fc3..d7bbedea 100644 --- a/projects/f-flow/src/f-storage/index.ts +++ b/projects/f-flow/src/f-storage/index.ts @@ -10,6 +10,4 @@ export * from './features/notify-transform-changed'; export * from './f-components-store'; -export * from './f-connectors-store'; - export * from './providers'; diff --git a/projects/f-flow/src/f-storage/providers.ts b/projects/f-flow/src/f-storage/providers.ts index 44673218..db0c77a9 100644 --- a/projects/f-flow/src/f-storage/providers.ts +++ b/projects/f-flow/src/f-storage/providers.ts @@ -1,21 +1,20 @@ -import { NotifyDataChangedExecution } from './features/notify-data-changed'; -import { ListenDataChangesExecution } from './features/listen-components-data-changed'; +import { NotifyDataChanged } from './features/notify-data-changed'; +import { ListenDataChanges } from './features/listen-components-data-changed'; import { FComponentsStore } from './f-components-store'; -import { ListenCountChangesExecution } from './features/listen-count-changes'; -import { ListenTransformChangesExecution } from './features/listen-transform-changes'; -import { NotifyTransformChangedExecution } from './features/notify-transform-changed'; +import { ListenCountChanges } from './features/listen-count-changes'; +import { ListenTransformChanges } from './features/listen-transform-changes'; +import { NotifyTransformChanged } from './features/notify-transform-changed'; export const F_STORAGE_PROVIDERS = [ + NotifyDataChanged, - NotifyDataChangedExecution, + ListenCountChanges, - ListenCountChangesExecution, + ListenDataChanges, - ListenDataChangesExecution, + ListenTransformChanges, - ListenTransformChangesExecution, - - NotifyTransformChangedExecution, + NotifyTransformChanged, FComponentsStore, ]; diff --git a/projects/f-flow/src/f-zoom/f-zoom.directive.ts b/projects/f-flow/src/f-zoom/f-zoom.directive.ts index a504eb23..1322782c 100644 --- a/projects/f-flow/src/f-zoom/f-zoom.directive.ts +++ b/projects/f-flow/src/f-zoom/f-zoom.directive.ts @@ -17,7 +17,6 @@ import { FMediator } from '@foblex/mediator'; import { AddZoomToStoreRequest, defaultEventTrigger, - Deprecated, FEventTrigger, GetCanvasRequest, GetFlowHostElementRequest, @@ -189,14 +188,6 @@ export class FZoomDirective extends FZoomBase implements OnInit, AfterViewInit, this._mediator.execute(new SetZoomRequest(position, step, direction, animated)); } - /** - * @deprecated Method "getScale" is deprecated. Use "getZoomValue" instead. This method will be removed in version 18.0.0.`, - */ - @Deprecated('getZoomValue') - public getScale(): number { - return this.getZoomValue(); - } - public getZoomValue(): number { return this._canvas.transform.scale || 1; } diff --git a/projects/f-flow/src/public-api.ts b/projects/f-flow/src/public-api.ts index d05f8b5a..9d4a28ff 100644 --- a/projects/f-flow/src/public-api.ts +++ b/projects/f-flow/src/public-api.ts @@ -7,6 +7,7 @@ export * from './domain'; export * from './drag-toolkit'; export * from './f-connection'; +export * from './f-connection-v2'; export * from './f-connectors'; diff --git a/projects/f-flow/src/reactivity/debounce-time.ts b/projects/f-flow/src/reactivity/debounce-time.ts index 964892a0..d25fbd35 100644 --- a/projects/f-flow/src/reactivity/debounce-time.ts +++ b/projects/f-flow/src/reactivity/debounce-time.ts @@ -1,12 +1,21 @@ import { FChannelOperator } from './types'; export function debounceTime(delay: number): FChannelOperator { - let timeoutId: ReturnType; - return (callback) => { - return () => { - clearTimeout(timeoutId); - timeoutId = setTimeout(() => callback(), delay); + let timeoutId: ReturnType | null = null; + + return { + callback: () => { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + timeoutId = null; + callback(); + }, delay); + }, + cleanup: () => { + if (timeoutId) clearTimeout(timeoutId); + timeoutId = null; + }, }; }; } diff --git a/projects/f-flow/src/reactivity/f-channel-hub.ts b/projects/f-flow/src/reactivity/f-channel-hub.ts index ce6e49b9..642dea14 100644 --- a/projects/f-flow/src/reactivity/f-channel-hub.ts +++ b/projects/f-flow/src/reactivity/f-channel-hub.ts @@ -3,7 +3,6 @@ import { FChannelListener, FChannelOperator } from './types'; import { DestroyRef } from '@angular/core'; export class FChannelHub { - private readonly _channels: FChannel[] = []; private _operators: FChannelOperator[] = []; @@ -20,18 +19,41 @@ export class FChannelHub { } public listen(destroyRef: DestroyRef, callback: FChannelListener): void { - let modifiedCallback = callback; - - this._operators.forEach(operator => { - modifiedCallback = operator(modifiedCallback); - }); - - const unsubscribeCallbacks = this._channels.map(channel => - channel.listen(() => modifiedCallback()), - ); - - destroyRef.onDestroy(() => { - unsubscribeCallbacks.forEach(unsubscribe => unsubscribe()); - }); + let current = callback; + + const cleanups: (() => void)[] = []; + const onSubscribes: ((finalCb: FChannelListener) => void)[] = []; + const teardownSetters: ((teardown: () => void) => void)[] = []; + + for (const operator of [...this._operators].reverse()) { + const res = operator(current); + current = res.callback; + + if (res.cleanup) cleanups.push(res.cleanup); + if (res.onSubscribe) onSubscribes.push(res.onSubscribe); + if (res.setTeardown) teardownSetters.push(res.setTeardown); + } + + const unsubs = this._channels.map((ch) => ch.listen(() => current())); + + let tornDown = false; + const teardown = () => { + if (tornDown) return; + tornDown = true; + + unsubs.forEach((u) => u()); + cleanups.forEach((c) => c()); + }; + + teardownSetters + .slice() + .reverse() + .forEach((set) => set(teardown)); + onSubscribes + .slice() + .reverse() + .forEach((fn) => fn(current)); + + destroyRef.onDestroy(teardown); } } diff --git a/projects/f-flow/src/reactivity/index.ts b/projects/f-flow/src/reactivity/index.ts index 5ec42ac4..79734358 100644 --- a/projects/f-flow/src/reactivity/index.ts +++ b/projects/f-flow/src/reactivity/index.ts @@ -9,3 +9,5 @@ export * from './types'; export * from './f-channel-hub'; export * from './notify-on-start'; + +export * from './take-one'; diff --git a/projects/f-flow/src/reactivity/notify-on-start.ts b/projects/f-flow/src/reactivity/notify-on-start.ts index 6d356d22..b06c6863 100644 --- a/projects/f-flow/src/reactivity/notify-on-start.ts +++ b/projects/f-flow/src/reactivity/notify-on-start.ts @@ -1,9 +1,16 @@ import { FChannelOperator } from './types'; export function notifyOnStart(): FChannelOperator { - return callback => { - callback(); + return (callback) => { + let teardown: (() => void) | null = null; - return callback; + return { + setTeardown: (t) => (teardown = t), + callback, + onSubscribe: (finalCallback) => { + if (!teardown) return; + finalCallback(); + }, + }; }; } diff --git a/projects/f-flow/src/reactivity/take-one.ts b/projects/f-flow/src/reactivity/take-one.ts new file mode 100644 index 00000000..98b404d1 --- /dev/null +++ b/projects/f-flow/src/reactivity/take-one.ts @@ -0,0 +1,19 @@ +import { FChannelOperator } from './types'; + +export function takeOne(): FChannelOperator { + return (callback) => { + let taken = false; + let teardown: (() => void) | null = null; + + return { + setTeardown: (t) => (teardown = t), + callback: () => { + if (taken) return; + taken = true; + + callback(); + teardown?.(); + }, + }; + }; +} diff --git a/projects/f-flow/src/reactivity/types.ts b/projects/f-flow/src/reactivity/types.ts index db37f9a9..60a3285a 100644 --- a/projects/f-flow/src/reactivity/types.ts +++ b/projects/f-flow/src/reactivity/types.ts @@ -1,3 +1,8 @@ export type FChannelListener = () => void; -export type FChannelOperator = (callback: FChannelListener) => FChannelListener; +export type FChannelOperator = (callback: FChannelListener) => { + callback: FChannelListener; + cleanup?: () => void; + onSubscribe?: (finalCallback: FChannelListener) => void; + setTeardown?: (teardown: () => void) => void; +}; diff --git a/projects/f-flow/src/utils/calculate-pointer-in-flow.ts b/projects/f-flow/src/utils/calculate-pointer-in-flow.ts new file mode 100644 index 00000000..de967a7d --- /dev/null +++ b/projects/f-flow/src/utils/calculate-pointer-in-flow.ts @@ -0,0 +1,13 @@ +import { IPoint, ITransformModel, Point } from '@foblex/2d'; + +export function calculatePointerInFlow( + event: { getPosition: () => IPoint }, + flowHost: HTMLElement, + transform: ITransformModel, +): Point { + return Point.fromPoint(event.getPosition()) + .elementTransform(flowHost) + .sub(transform.scaledPosition) + .sub(transform.position) + .div(transform.scale); +} diff --git a/projects/f-flow/src/utils/index.ts b/projects/f-flow/src/utils/index.ts index 4e385a34..8de96365 100644 --- a/projects/f-flow/src/utils/index.ts +++ b/projects/f-flow/src/utils/index.ts @@ -1,2 +1,3 @@ +export * from './calculate-pointer-in-flow'; export * from './infinity-min-max'; export * from './string-attribute'; diff --git a/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.html b/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.html deleted file mode 100644 index 5d9c5bb2..00000000 --- a/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - -
drag me
-
drag me
- - -
drag me
-
drag me
- - -
drag me
-
drag me
-
-
diff --git a/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.scss b/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.scss deleted file mode 100644 index 73e25022..00000000 --- a/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -@use "../flow-common"; - -::ng-deep connection-behaviour { - @include flow-common.connection; -} - -.f-node { - @include flow-common.node; -} - -.f-node { - opacity: 0.5; -} - - diff --git a/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.ts b/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.ts deleted file mode 100644 index b31bae0e..00000000 --- a/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { EFConnectionBehavior, FFlowModule } from '@foblex/flow'; - -@Component({ - selector: 'connection-behaviour', - styleUrls: [ './connection-behaviour.component.scss' ], - templateUrl: './connection-behaviour.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - FFlowModule, - ] -}) -export class ConnectionBehaviourComponent { - - public eConnectionBehaviour = EFConnectionBehavior; -} diff --git a/projects/f-guides-examples/connection-type/connection-type.component.html b/projects/f-guides-examples/connection-type/connection-type.component.html deleted file mode 100644 index 075ea4c3..00000000 --- a/projects/f-guides-examples/connection-type/connection-type.component.html +++ /dev/null @@ -1,19 +0,0 @@ - - - -
drag me
-
drag me
- - -
drag me
-
drag me
- - -
- Segment -
-
-
drag me
-
drag me
-
-
diff --git a/projects/f-guides-examples/connection-type/connection-type.component.scss b/projects/f-guides-examples/connection-type/connection-type.component.scss deleted file mode 100644 index 7278a7ab..00000000 --- a/projects/f-guides-examples/connection-type/connection-type.component.scss +++ /dev/null @@ -1,9 +0,0 @@ -@use "../flow-common"; - -::ng-deep connection-type { - @include flow-common.connection; -} - -.f-node { - @include flow-common.node; -} diff --git a/projects/f-guides-examples/connection-type/connection-type.component.ts b/projects/f-guides-examples/connection-type/connection-type.component.ts deleted file mode 100644 index 92a83453..00000000 --- a/projects/f-guides-examples/connection-type/connection-type.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { FFlowModule } from '@foblex/flow'; - -@Component({ - selector: 'connection-type', - styleUrls: [ './connection-type.component.scss' ], - templateUrl: './connection-type.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - FFlowModule, - ] -}) -export class ConnectionTypeComponent { - -} diff --git a/projects/f-guides-examples/drag-to-reassign/drag-to-reassign.component.html b/projects/f-guides-examples/drag-to-reassign/drag-to-reassign.component.html deleted file mode 100644 index 4c7123e3..00000000 --- a/projects/f-guides-examples/drag-to-reassign/drag-to-reassign.component.html +++ /dev/null @@ -1,13 +0,0 @@ - - - @for (connection of connections;track connection.inputId) { - - - } - -
Output
-
Input
-
Input
-
-
diff --git a/projects/f-guides-examples/drag-to-reassign/drag-to-reassign.component.scss b/projects/f-guides-examples/drag-to-reassign/drag-to-reassign.component.scss deleted file mode 100644 index 3b7c5143..00000000 --- a/projects/f-guides-examples/drag-to-reassign/drag-to-reassign.component.scss +++ /dev/null @@ -1,15 +0,0 @@ -@use "../flow-common"; - -::ng-deep drag-to-reassign { - @include flow-common.connection; - .f-connection { - .f-connection-drag-handle { - fill: var(--connection-color); - } - } -} - -.f-node { - @include flow-common.node; -} - diff --git a/projects/f-guides-examples/drag-to-reassign/drag-to-reassign.component.ts b/projects/f-guides-examples/drag-to-reassign/drag-to-reassign.component.ts deleted file mode 100644 index 506f5e22..00000000 --- a/projects/f-guides-examples/drag-to-reassign/drag-to-reassign.component.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core'; -import { FFlowModule, FReassignConnectionEvent } from '@foblex/flow'; - -@Component({ - selector: 'drag-to-reassign', - styleUrls: [ './drag-to-reassign.component.scss' ], - templateUrl: './drag-to-reassign.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - standalone: true, - imports: [ - FFlowModule - ] -}) -export class DragToReassignComponent { - - public connections: { outputId: string, inputId: string }[] = [ - { outputId: '1', inputId: '2' }, - ]; - - constructor( - private changeDetectorRef: ChangeDetectorRef - ) { - } - - public reassignConnection(event: FReassignConnectionEvent): void { - if(!event.newFInputId) { - return; - } - this.connections = [ { outputId: event.fOutputId, inputId: event.newFInputId } ]; - this.changeDetectorRef.detectChanges(); - } -} diff --git a/projects/f-pro-examples/tournament-bracket/calculate-maximum-items-in-column.ts b/projects/f-pro-examples/tournament-bracket/calculate-maximum-items-in-column.ts index d38e4016..833ce307 100644 --- a/projects/f-pro-examples/tournament-bracket/calculate-maximum-items-in-column.ts +++ b/projects/f-pro-examples/tournament-bracket/calculate-maximum-items-in-column.ts @@ -1,8 +1,7 @@ -import { IMap } from '@foblex/flow'; import { ITournamentBracketItem } from './i-tournament-bracket-item'; -export function calculateMaximumItemsInColumn(columns: IMap, items: ITournamentBracketItem[]): number { - const result: IMap = {}; +export function calculateMaximumItemsInColumn(columns: Record, items: ITournamentBracketItem[]): number { + const result: Record = {}; Object.entries(columns).forEach((value: [ phase: string, columnIndex: number ]) => { if (!result[ value[ 1 ] ]) { diff --git a/projects/f-pro-examples/tournament-bracket/get-phases-by-column-index.ts b/projects/f-pro-examples/tournament-bracket/get-phases-by-column-index.ts index 0a1c8d5f..778fe0b3 100644 --- a/projects/f-pro-examples/tournament-bracket/get-phases-by-column-index.ts +++ b/projects/f-pro-examples/tournament-bracket/get-phases-by-column-index.ts @@ -1,5 +1,3 @@ -import { IMap } from '@foblex/flow'; - -export function getPhasesByColumnIndex(columns: IMap, columnIndex: number): string[] { +export function getPhasesByColumnIndex(columns: Record, columnIndex: number): string[] { return Object.entries(columns).filter((value) => value[ 1 ] === columnIndex).map((value) => value[ 0 ]); } diff --git a/projects/f-pro-examples/tournament-bracket/tournament-bracket.ts b/projects/f-pro-examples/tournament-bracket/tournament-bracket.ts index 1cec29f8..15c43019 100644 --- a/projects/f-pro-examples/tournament-bracket/tournament-bracket.ts +++ b/projects/f-pro-examples/tournament-bracket/tournament-bracket.ts @@ -1,5 +1,4 @@ import { ITournamentBracketItem } from './i-tournament-bracket-item'; -import { IMap } from '@foblex/flow'; import { getPhasesByColumnIndex } from './get-phases-by-column-index'; import { getItemsInPhase } from './get-items-in-phase'; import { PointExtensions } from '@foblex/2d'; @@ -12,7 +11,7 @@ export class TournamentBracket { private nodeWidth: number, private nodeHeight: number, private verticalGap: number, - private horizontalGap: number + private horizontalGap: number, ) { } @@ -70,7 +69,7 @@ export class TournamentBracket { } } -export const BRACKET_COLUMNS: IMap = { +export const BRACKET_COLUMNS: Record = { [ 'space1' ]: 0, [ 'UB Quarterfinal'.toLowerCase() ]: 1, [ 'space2' ]: 2, diff --git a/projects/f-pro-examples/visual-programming/components/flow/vp-flow.component.html b/projects/f-pro-examples/visual-programming/components/flow/vp-flow.component.html deleted file mode 100644 index dbfa9982..00000000 --- a/projects/f-pro-examples/visual-programming/components/flow/vp-flow.component.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - @for (connection of viewModel.connections; track connection) { - - - - - - - - - - - - - - - } - @for (node of viewModel.nodes; track node.id) { - - - } - - - - - diff --git a/projects/f-pro-examples/visual-programming/components/flow/vp-flow.component.scss b/projects/f-pro-examples/visual-programming/components/flow/vp-flow.component.scss deleted file mode 100644 index b75b2740..00000000 --- a/projects/f-pro-examples/visual-programming/components/flow/vp-flow.component.scss +++ /dev/null @@ -1,90 +0,0 @@ -:host { - position: relative; - display: flex; - width: 100%; - height: 100%; -} - -::ng-deep vp-flow { - - .f-line-alignment { - .f-line { - background-color: var(--vp-line-alignment-color); - } - } - - .f-background { - circle { - fill: var(--vp-line-alignment-color); - } - } - - .f-connection { - - .f-connection-drag-handle { - fill: transparent; - } - - .f-connection-selection { - stroke-width: 20px; - fill: none; - } - - &:hover { - .f-connection-selection { - stroke: var(--vp-connection-selection-color); - } - } - - .f-connection-path { - stroke-width: 2px; - fill: none; - } - - &.f-connection-for-create { - .f-connection-path { - stroke: var(--vp-connection-for-create-path); - stroke-width: 2px; - } - } - - .f-connection-text { - fill: var(--vp-connection-text-color); - font-size: 12px; - } - - &.f-selected { - .f-connection-path { - stroke: var(--vp-selected-connection-color); - } - - .f-connection-text { - fill: var(--vp-selected-connection-color); - } - } - } - - .f-selection-area { - background-color: var(--vp-selection-area-color); - } - - .f-minimap { - background-color: var(--vp-minimap-color); - bottom: 16px; - right: 16px; - width: 140px; - height: 120px; - - .f-minimap-node { - fill: var(--vp-minimap-node-color); - - &.f-selected { - fill: var(--vp-minimap-selected-node-color); - } - } - - .f-minimap-view { - fill: var(--vp-minimap-view-color) - } - } -} diff --git a/projects/f-pro-examples/visual-programming/components/flow/vp-flow.component.ts b/projects/f-pro-examples/visual-programming/components/flow/vp-flow.component.ts deleted file mode 100644 index 11cb5698..00000000 --- a/projects/f-pro-examples/visual-programming/components/flow/vp-flow.component.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, OnInit, - ViewChild, -} from '@angular/core'; -import { - FCreateNodeEvent, EFMarkerType, - FCanvasComponent, FFlowModule, FZoomDirective, - FReassignConnectionEvent, FCreateConnectionEvent -} from '@foblex/flow'; -import { IPoint, Point } from '@foblex/2d'; -import { ENodeType } from '../../domain'; -import { VpPaletteComponent } from '../palette/vp-palette.component'; -import { VpNodeComponent } from '../node/vp-node.component'; -import { FlowService } from '../../domain'; -import { IFlowViewModel } from '../../domain'; -import { VpToolbarComponent } from '../toolbar/vp-toolbar.component'; - -@Component({ - selector: 'vp-flow', - templateUrl: './vp-flow.component.html', - styleUrls: [ - '../styles/_icon-button.scss', - '../styles/_variables.scss', - './vp-flow.component.scss' - ], - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - providers: [ - FlowService - ], - imports: [ - FFlowModule, - VpToolbarComponent, - VpPaletteComponent, - VpNodeComponent, - FZoomDirective - ] -}) -export class VpFlowComponent implements OnInit { - - protected viewModel: IFlowViewModel = { - nodes: [], - connections: [] - }; - - @ViewChild(FCanvasComponent, { static: true }) - public fCanvasComponent!: FCanvasComponent; - - @ViewChild(FZoomDirective, { static: true }) - public fZoomDirective!: FZoomDirective; - - protected readonly eMarkerType = EFMarkerType; - - constructor( - private apiService: FlowService, - private changeDetectorRef: ChangeDetectorRef - ) { - } - - public ngOnInit(): void { - this.getData(); - } - - public onInitialized(): void { - this.fCanvasComponent.fitToScreen(new Point(40, 40), false); - } - - private getData(): void { - this.viewModel = this.apiService.getFlow(); - this.changeDetectorRef.markForCheck(); - } - - public onNodeAdded(event: FCreateNodeEvent): void { - this.apiService.addNode(event.data as ENodeType, event.rect); - this.getData(); - } - - public onReassignConnection(event: FReassignConnectionEvent): void { - if (!event.newTargetId) { - return; - } - this.apiService.reassignConnection(event.oldSourceId, event.oldTargetId, event.newTargetId); - this.getData(); - } - - public onConnectionAdded(event: FCreateConnectionEvent): void { - if (!event.fInputId) { - return; - } - this.apiService.addConnection(event.fOutputId, event.fInputId); - this.getData(); - } - - public onNodePositionChanged(point: IPoint, node: any): void { - node.position = point; - this.apiService.moveNode(node.id, point); - } -} diff --git a/projects/f-pro-examples/visual-programming/components/node/vp-node.component.html b/projects/f-pro-examples/visual-programming/components/node/vp-node.component.html deleted file mode 100644 index 4f773fb7..00000000 --- a/projects/f-pro-examples/visual-programming/components/node/vp-node.component.html +++ /dev/null @@ -1,17 +0,0 @@ -@if (node) { -
-
- @if (node.output) { -
-
- } - - @if (node.input) { -
- } -
- {{ node.type[0] }} -
-
-} - diff --git a/projects/f-pro-examples/visual-programming/components/node/vp-node.component.scss b/projects/f-pro-examples/visual-programming/components/node/vp-node.component.scss deleted file mode 100644 index ddfccfcb..00000000 --- a/projects/f-pro-examples/visual-programming/components/node/vp-node.component.scss +++ /dev/null @@ -1,79 +0,0 @@ -:host { - display: block; - width: 34px; - height: 34px; - - .interactive-node-body { - display: flex; - width: 100%; - height: 100%; - border-radius: 50%; - position: relative; - background-color: var(--vp-node-color); - border: 2px solid; - - .icon { - margin: auto; - line-height: 15px; - text-transform: uppercase; - font-size: 14px; - font-weight: 600; - color: inherit; - } - } - - .selected-background { - position: absolute; - width: 50px; - height: 50px; - left: -8px; - top: -8px; - display: none; - background-color: var(--vp-selected-node-background-color); - border-radius: 4px; - } - - .f-node-output, .f-node-input { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - border-radius: 17px; - } - - .f-node-outlet { - position: absolute; - right: -20px; - top: 50%; - transform: translateY(-50%); - width: 10px; - height: 10px; - background-color: var(--vp-outlet-color); - border-radius: 50%; - border: 2px solid var(--vp-outlet-border-color); - display: none; - } - - &:hover { - .selected-background { - background-color: var(--vp-hover-node-background-color); - display: block; - } - } - - &.f-selected { - .selected-background { - display: block; - } - - .interactive-node-body { - border-color: var(--vp-selected-node-border-color) !important; - } - - .f-node-outlet { - display: block; - } - } -} - diff --git a/projects/f-pro-examples/visual-programming/components/node/vp-node.component.ts b/projects/f-pro-examples/visual-programming/components/node/vp-node.component.ts deleted file mode 100644 index f44a7bd8..00000000 --- a/projects/f-pro-examples/visual-programming/components/node/vp-node.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { FFlowModule } from '@foblex/flow'; -import { IFlowNodeViewModel } from '../../domain'; - -@Component({ - selector: 'vp-node', - templateUrl: './vp-node.component.html', - styleUrls: [ './vp-node.component.scss' ], - standalone: true, - imports: [ - FFlowModule, - ], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class VpNodeComponent { - - @Input() - public node: IFlowNodeViewModel | undefined; -} diff --git a/projects/f-pro-examples/visual-programming/components/palette/vp-palette.component.html b/projects/f-pro-examples/visual-programming/components/palette/vp-palette.component.html deleted file mode 100644 index 44034c3f..00000000 --- a/projects/f-pro-examples/visual-programming/components/palette/vp-palette.component.html +++ /dev/null @@ -1,7 +0,0 @@ -@for (item of palette; track item;) { -
-
{{ item.type[0] }}
-
-} diff --git a/projects/f-pro-examples/visual-programming/components/palette/vp-palette.component.scss b/projects/f-pro-examples/visual-programming/components/palette/vp-palette.component.scss deleted file mode 100644 index e6205a56..00000000 --- a/projects/f-pro-examples/visual-programming/components/palette/vp-palette.component.scss +++ /dev/null @@ -1,36 +0,0 @@ -:host { - display: flex; - flex-direction: column; - justify-content: center; - width: 49px; - min-width: 49px; - height: 100%; - border-right: 1px solid var(--vp-palette-border-color); - overflow: hidden; - overflow-y: auto; - z-index: 1; - gap: 8px; - padding: 8px; -} - -::ng-deep .visual-programming-external-item { - position: relative; - display: flex; - width: 32px; - height: 32px; - overflow: hidden; - border-radius: 4px; - background-color: var(--vp-palette-node-color); - border: 2px solid; - cursor: move; - z-index: 50; - - .icon { - margin: auto; - line-height: 15px; - text-transform: uppercase; - font-size: 14px; - font-weight: 600; - color: inherit; - } -} diff --git a/projects/f-pro-examples/visual-programming/components/palette/vp-palette.component.ts b/projects/f-pro-examples/visual-programming/components/palette/vp-palette.component.ts deleted file mode 100644 index 766aa0a2..00000000 --- a/projects/f-pro-examples/visual-programming/components/palette/vp-palette.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - ChangeDetectionStrategy, Component, -} from '@angular/core'; -import { FExternalItemDirective, FFlowModule } from '@foblex/flow'; -import { ENodeType } from '../../domain'; -import { NODE_CONFIGURATION } from '../../domain'; - -@Component({ - selector: 'vp-palette', - templateUrl: './vp-palette.component.html', - styleUrls: [ './vp-palette.component.scss' ], - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - FFlowModule, - FExternalItemDirective, - ] -}) -export class VpPaletteComponent { - - protected palette = Object.keys(NODE_CONFIGURATION).map((key) => { - return { - type: key, - color: NODE_CONFIGURATION[ key as ENodeType ].color, - text: NODE_CONFIGURATION[ key as ENodeType ].text, - } - }); -} diff --git a/projects/f-pro-examples/visual-programming/components/styles/_icon-button.scss b/projects/f-pro-examples/visual-programming/components/styles/_icon-button.scss deleted file mode 100644 index 60f8cc5e..00000000 --- a/projects/f-pro-examples/visual-programming/components/styles/_icon-button.scss +++ /dev/null @@ -1,38 +0,0 @@ -::ng-deep { - .vp-icon-button { - display: inline-block; - text-align: center; - font-weight: 500; - white-space: nowrap; - border: none; - border-radius: 2px; - padding: 3px; - font-size: 14px; - pointer-events: all; - - color: var(--vp-button-text-color); - background-color: var(--vp-button-default-bg); - cursor: pointer; - - &:hover { - background-color: var(--vp-button-default-hover-bg); - } - - &:active { - background-color: var(--vp-button-default-active-bg); - } - - mat-icon { - display: block; - width: 16px; - height: 16px; - font-size: 16px; - color: inherit; - } - - span { - color: inherit; - font-weight: 300; - } - } -} diff --git a/projects/f-pro-examples/visual-programming/components/styles/_variables.scss b/projects/f-pro-examples/visual-programming/components/styles/_variables.scss deleted file mode 100644 index a176c354..00000000 --- a/projects/f-pro-examples/visual-programming/components/styles/_variables.scss +++ /dev/null @@ -1,57 +0,0 @@ -::ng-deep :root { - --vp-line-alignment-color: #e2e2e3; - --vp-connection-selection-color: rgba(142, 150, 170, 0.14); - --vp-connection-for-create-path: #5672cd; - --vp-connection-text-color: rgba(60, 60, 67, 0.56); - --vp-selected-connection-color: #5672cd; - --vp-selection-area-color: rgba(100, 108, 255, 0.14); - - --vp-minimap-color: #ffffff; - --vp-minimap-node-color: rgba(60, 60, 67); - --vp-minimap-selected-node-color: #3451b2; - --vp-minimap-view-color: rgba(100, 108, 255, 0.14); - - --vp-node-color: #ffffff; - --vp-selected-node-background-color: rgba(142, 150, 170, 0.14); - --vp-hover-node-background-color: rgba(142, 150, 170, 0.14); - --vp-selected-node-border-color: #5672cd; - --vp-outlet-color: #ffffff; - --vp-outlet-border-color: rgba(60, 60, 67); - - --vp-palette-border-color: #e2e2e3; - --vp-palette-node-color: #ffffff; - - --vp-button-text-color: rgba(60, 60, 67); - --vp-button-default-bg: #ebebef; - --vp-button-default-hover-bg: #e4e4e9; - --vp-button-default-active-bg: #dddde3; - - &.dark { - --vp-line-alignment-color: #2e2e32; - --vp-connection-selection-color: rgba(101, 117, 133, 0.16); - --vp-connection-for-create-path: #3e63dd; - --vp-connection-text-color: rgba(235, 235, 245, 0.38); - --vp-selected-connection-color: #3e63dd; - --vp-selection-area-color: rgba(100, 108, 255, 0.16); - - --vp-minimap-color: #1b1b1f; - --vp-minimap-node-color: rgba(255, 255, 245, 0.86); - --vp-minimap-selected-node-color: #a8b1ff; - --vp-minimap-view-color: rgba(100, 108, 255, 0.16); - - --vp-node-color: #1b1b1f; - --vp-selected-node-background-color: rgba(101, 117, 133, 0.16); - --vp-hover-node-background-color: rgba(101, 117, 133, 0.16); - --vp-selected-node-border-color: #3e63dd; - --vp-outlet-color: #1b1b1f; - --vp-outlet-border-color: rgba(255, 255, 245, 0.86); - - --vp-palette-border-color: #000000; - --vp-palette-node-color: #1b1b1f; - - --vp-button-text-color: rgba(255, 255, 245, 0.86); - --vp-button-default-bg: #32363f; - --vp-button-default-hover-bg: #414853; - --vp-button-default-active-bg: #515c67; - } -} diff --git a/projects/f-pro-examples/visual-programming/components/toolbar/vp-toolbar.component.html b/projects/f-pro-examples/visual-programming/components/toolbar/vp-toolbar.component.html deleted file mode 100644 index dcebf592..00000000 --- a/projects/f-pro-examples/visual-programming/components/toolbar/vp-toolbar.component.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/projects/f-pro-examples/visual-programming/components/toolbar/vp-toolbar.component.scss b/projects/f-pro-examples/visual-programming/components/toolbar/vp-toolbar.component.scss deleted file mode 100644 index c41be322..00000000 --- a/projects/f-pro-examples/visual-programming/components/toolbar/vp-toolbar.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -:host { - display: flex; - justify-content: flex-end; - align-items: center; - flex-wrap: wrap; - gap: 8px; - position: absolute; - right: 16px; - top: 16px; -} diff --git a/projects/f-pro-examples/visual-programming/components/toolbar/vp-toolbar.component.ts b/projects/f-pro-examples/visual-programming/components/toolbar/vp-toolbar.component.ts deleted file mode 100644 index db218f49..00000000 --- a/projects/f-pro-examples/visual-programming/components/toolbar/vp-toolbar.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, -} from '@angular/core'; -import { VpFlowComponent } from '../flow/vp-flow.component'; -import { MatIcon } from '@angular/material/icon'; - -@Component({ - selector: 'vp-toolbar', - templateUrl: './vp-toolbar.component.html', - styleUrls: [ './vp-toolbar.component.scss' ], - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - MatIcon - ] -}) -export class VpToolbarComponent { - - constructor( - private flowComponent: VpFlowComponent - ) { - } - - public onZoomIn(): void { - this.flowComponent.fZoomDirective.zoomIn(); - } - - public onZoomOut(): void { - this.flowComponent.fZoomDirective.zoomOut(); - } - - public onFitToScreen(): void { - this.flowComponent.fCanvasComponent.fitToScreen(); - } - - public onOneToOne(): void { - this.flowComponent.fCanvasComponent.resetScaleAndCenter(); - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/configuration.ts b/projects/f-pro-examples/visual-programming/domain/configuration.ts deleted file mode 100644 index ec04b378..00000000 --- a/projects/f-pro-examples/visual-programming/domain/configuration.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ENodeType } from './e-node-type'; - -export const NODE_CONFIGURATION = { - [ ENodeType.Input ]: { - color: '#e0575b', - text: 'Input', - }, - [ ENodeType.Assign ]: { - color: '#9f6a00', - text: 'Assign', - }, - [ ENodeType.Switch ]: { - color: '#8e5cd9', - text: 'Switch', - }, - [ ENodeType.Cycle ]: { - color: '#8e5cd9', - text: 'Cycle', - }, - [ ENodeType.Error ]: { - color: '#ec8a82', - text: 'Error', - }, - [ ENodeType.Database ]: { - color: '#30a46c', - text: 'Database', - }, - [ ENodeType.Hash ]: { - color: '#8e5cd9', - text: 'Function', - }, -}; diff --git a/projects/f-pro-examples/visual-programming/domain/connection/add-new/create-connection.handler.ts b/projects/f-pro-examples/visual-programming/domain/connection/add-new/create-connection.handler.ts deleted file mode 100644 index b817b029..00000000 --- a/projects/f-pro-examples/visual-programming/domain/connection/add-new/create-connection.handler.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { IHandler } from '@foblex/mediator'; -import { CreateConnectionRequest } from './create-connection.request'; -import { IFlowStorage } from '../../flow.storage'; -import { IFlowConnectionStorageModel } from '../i-flow-connection-storage-model'; - -export class CreateConnectionHandler implements IHandler { - - constructor( - private flow: IFlowStorage - ) { - } - - public handle(request: CreateConnectionRequest): void { - const index = this.getConnectionIndex(request); - if (index > -1) { - throw new Error('Connection already exists'); - } - - this.flow.connections.push( - this.createConnection(request.outputId, request.inputId) - ); - } - - private getConnectionIndex(request: CreateConnectionRequest): number { - return this.flow.connections.findIndex((x) => { - return x.from === request.outputId && x.to === request.inputId; - }); - } - - private createConnection(outputId: string, inputId: string): IFlowConnectionStorageModel { - return { - from: outputId, - to: inputId, - }; - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/connection/add-new/create-connection.request.ts b/projects/f-pro-examples/visual-programming/domain/connection/add-new/create-connection.request.ts deleted file mode 100644 index 80ff8696..00000000 --- a/projects/f-pro-examples/visual-programming/domain/connection/add-new/create-connection.request.ts +++ /dev/null @@ -1,8 +0,0 @@ -export class CreateConnectionRequest { - - constructor( - public readonly outputId: string, - public readonly inputId: string, - ) { - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/connection/i-flow-connection-storage-model.ts b/projects/f-pro-examples/visual-programming/domain/connection/i-flow-connection-storage-model.ts deleted file mode 100644 index 5c2c924d..00000000 --- a/projects/f-pro-examples/visual-programming/domain/connection/i-flow-connection-storage-model.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface IFlowConnectionStorageModel { - - from: string; - - to: string; -} diff --git a/projects/f-pro-examples/visual-programming/domain/connection/i-flow-connection-view-model.ts b/projects/f-pro-examples/visual-programming/domain/connection/i-flow-connection-view-model.ts deleted file mode 100644 index 1ef27bee..00000000 --- a/projects/f-pro-examples/visual-programming/domain/connection/i-flow-connection-view-model.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IFlowConnectionStorageModel } from './i-flow-connection-storage-model'; - -export interface IFlowConnectionViewModel extends IFlowConnectionStorageModel { - - color1: string; - - color2: string; - - text: string; -} diff --git a/projects/f-pro-examples/visual-programming/domain/connection/map/map-to-connection-view-model.handler.ts b/projects/f-pro-examples/visual-programming/domain/connection/map/map-to-connection-view-model.handler.ts deleted file mode 100644 index fde0b7a4..00000000 --- a/projects/f-pro-examples/visual-programming/domain/connection/map/map-to-connection-view-model.handler.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { IFlowStorage } from '../../flow.storage'; -import { IHandler } from '@foblex/mediator'; -import { IFlowConnectionViewModel } from '../i-flow-connection-view-model'; -import { IFlowConnectionStorageModel } from '../i-flow-connection-storage-model'; -import { IFlowNodeStorageModel } from '../../node/i-flow-node-storage-model'; -import { NODE_CONFIGURATION } from '../../configuration'; - -export class MapToConnectionViewModelHandler implements IHandler{ - - constructor( - private flow: IFlowStorage - ) { - } - - public handle(): IFlowConnectionViewModel[] { - return this.getConnections().map((x) => { - return this.mapConnection(x, this.getFromNode(x), this.getToNode(x)); - }); - } - - private getConnections(): IFlowConnectionStorageModel[] { - return this.flow.connections; - } - - private mapConnection( - connection: IFlowConnectionStorageModel, fromNode: IFlowNodeStorageModel, toNode: IFlowNodeStorageModel - ): IFlowConnectionViewModel { - return { - ...connection, - color1: NODE_CONFIGURATION[ fromNode.type ].color, - color2: NODE_CONFIGURATION[ toNode.type ].color, - text: NODE_CONFIGURATION[ fromNode.type ].text, - }; - } - - private getFromNode(connection: IFlowConnectionStorageModel): IFlowNodeStorageModel { - const result = this.getNodes().find((node) => node.output === connection.from); - if (!result) { - throw new Error('From node not found'); - } - return result; - } - - private getToNode(connection: IFlowConnectionStorageModel): IFlowNodeStorageModel { - const result = this.getNodes().find((node) => node.input === connection.to); - if (!result) { - throw new Error('To node not found'); - } - return result; - } - - private getNodes(): IFlowNodeStorageModel[] { - return this.flow.nodes; - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/connection/reassign/reassign-connection.handler.ts b/projects/f-pro-examples/visual-programming/domain/connection/reassign/reassign-connection.handler.ts deleted file mode 100644 index e5e8de89..00000000 --- a/projects/f-pro-examples/visual-programming/domain/connection/reassign/reassign-connection.handler.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { IHandler } from '@foblex/mediator'; -import { ReassignConnectionRequest } from './reassign-connection.request'; -import { IFlowStorage } from '../../flow.storage'; -import { IFlowConnectionStorageModel } from '../i-flow-connection-storage-model'; - -export class ReassignConnectionHandler implements IHandler { - - constructor( - private flow: IFlowStorage - ) { - } - - public handle(request: ReassignConnectionRequest): void { - const index = this.getConnectionIndex(request); - if (index === -1) { - throw new Error('Connection not found'); - } - - this.flow.connections.splice(index, 1, - this.createConnection(request.outputId, request.newInputId) - ); - } - - private getConnectionIndex(request: ReassignConnectionRequest): number { - return this.flow.connections.findIndex((x) => { - return x.from === request.outputId && x.to === request.oldInputId; - }); - } - - private createConnection(outputId: string, inputId: string): IFlowConnectionStorageModel { - return { - from: outputId, - to: inputId, - }; - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/connection/reassign/reassign-connection.request.ts b/projects/f-pro-examples/visual-programming/domain/connection/reassign/reassign-connection.request.ts deleted file mode 100644 index fa3875f3..00000000 --- a/projects/f-pro-examples/visual-programming/domain/connection/reassign/reassign-connection.request.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class ReassignConnectionRequest { - - constructor( - public readonly outputId: string, - public readonly oldInputId: string, - public readonly newInputId: string, - ) { - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/e-node-type.ts b/projects/f-pro-examples/visual-programming/domain/e-node-type.ts deleted file mode 100644 index 75fdb84a..00000000 --- a/projects/f-pro-examples/visual-programming/domain/e-node-type.ts +++ /dev/null @@ -1,16 +0,0 @@ -export enum ENodeType { - - Input = 'input', - - Assign = 'settings', - - Cycle = 'cycle', - - Switch = 'switch', - - Error = 'error', - - Database = 'database', - - Hash = 'hash', -} diff --git a/projects/f-pro-examples/visual-programming/domain/flow.service.ts b/projects/f-pro-examples/visual-programming/domain/flow.service.ts deleted file mode 100644 index aecd76bb..00000000 --- a/projects/f-pro-examples/visual-programming/domain/flow.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Injectable } from '@angular/core'; -import { FLOW_STORAGE, IFlowStorage } from './flow.storage'; -import { MapToNodeViewModelHandler } from './node/map/map-to-node-view-model.handler'; -import { MapToConnectionViewModelHandler } from './connection/map/map-to-connection-view-model.handler'; -import { IPoint } from '@foblex/2d'; -import { MoveNodeHandler } from './node/move-nodes/move-node.handler'; -import { MoveNodeRequest } from './node/move-nodes/move-node.request'; -import { AddNewNodeToFlowHandler } from './node/add-new/add-new-node-to-flow.handler'; -import { AddNewNodeToFlowRequest } from './node/add-new/add-new-node-to-flow.request'; -import { ENodeType } from './e-node-type'; -import { IFlowViewModel } from './i-flow-view-model'; -import { ReassignConnectionHandler } from './connection/reassign/reassign-connection.handler'; -import { ReassignConnectionRequest } from './connection/reassign/reassign-connection.request'; -import { CreateConnectionRequest } from './connection/add-new/create-connection.request'; -import { CreateConnectionHandler } from './connection/add-new/create-connection.handler'; - -@Injectable() -export class FlowService { - - private flow: IFlowStorage = FLOW_STORAGE; - - public getFlow(): IFlowViewModel { - return { - nodes: new MapToNodeViewModelHandler(this.flow).handle(), - connections: new MapToConnectionViewModelHandler(this.flow).handle(), - } - } - - public addNode(type: ENodeType, position: IPoint): void { - new AddNewNodeToFlowHandler(this.flow).handle( - new AddNewNodeToFlowRequest(type, position) - ); - } - - public moveNode(id: string, position: IPoint): void { - new MoveNodeHandler(this.flow).handle( - new MoveNodeRequest(id, position) - ); - } - - public addConnection(outputId: string, inputId: string): void { - new CreateConnectionHandler(this.flow).handle( - new CreateConnectionRequest(outputId, inputId) - ); - } - - public reassignConnection(outputId: string, oldInputId: string, newInputId: string): void { - new ReassignConnectionHandler(this.flow).handle( - new ReassignConnectionRequest(outputId, oldInputId, newInputId) - ); - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/flow.storage.ts b/projects/f-pro-examples/visual-programming/domain/flow.storage.ts deleted file mode 100644 index 1fc93ce1..00000000 --- a/projects/f-pro-examples/visual-programming/domain/flow.storage.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { ENodeType } from './e-node-type'; -import { IFlowNodeStorageModel } from './node/i-flow-node-storage-model'; -import { IFlowConnectionStorageModel } from './connection/i-flow-connection-storage-model'; - -export interface IFlowStorage { - - nodes: IFlowNodeStorageModel[]; - - connections: IFlowConnectionStorageModel[]; -} - -export const FLOW_STORAGE: IFlowStorage = { - - nodes: [ { - id: 'input1', - output: 'input_output', - type: ENodeType.Input, - position: { x: 300, y: 100 } - }, { - id: 'assign', - input: 'assign_input', - output: 'assign_output', - type: ENodeType.Assign, - position: { x: 300, y: 180 } - }, { - id: 'cycle1', - output: 'cycle1_output', - input: 'cycle1_input', - type: ENodeType.Cycle, - position: { x: 300, y: 260 } - }, { - id: 'switch1', - output: 'switch1_output', - input: 'switch1_input', - type: ENodeType.Switch, - position: { x: 540, y: 260 } - }, { - id: 'db_connector1', - output: 'db_connector1_output', - input: 'db_connector1_input', - type: ENodeType.Database, - position: { x: 580, y: 520 } - - }, { - id: 'db_connector2', - output: 'db_connector2_output', - input: 'db_connector2_input', - type: ENodeType.Database, - position: { x: 660, y: 520 } - }, { - id: 'db_connector3', - output: 'db_connector3_output', - input: 'db_connector3_input', - type: ENodeType.Database, - position: { x: 740, y: 520 } - }, { - id: 'function1', - output: 'function1_output', - input: 'function1_input', - type: ENodeType.Hash, - position: { x: 360, y: 400 } - }, { - id: 'function2', - output: 'function2_output', - input: 'function2_input', - type: ENodeType.Hash, - position: { x: 440, y: 400 } - }, { - id: 'function3', - output: 'function3_output', - input: 'function3_input', - type: ENodeType.Hash, - position: { x: 520, y: 400 } - }, { - id: 'exception1', - output: 'exception1_output', - input: 'exception1_input', - type: ENodeType.Error, - position: { x: 700, y: 260 } - }, { - id: 'cycle2', - output: 'cycle2_output', - input: 'cycle2_input', - type: ENodeType.Cycle, - position: { x: 300, y: 500 } - }, { - id: 'output', - input: 'output_input', - type: ENodeType.Hash, - position: { x: 300, y: 580 } - } ], - connections: [ - { from: 'input_output', to: 'assign_input' }, - { from: 'assign_output', to: 'cycle1_input' }, - { from: 'cycle1_output', to: 'switch1_input' }, - { from: 'switch1_output', to: 'exception1_input' }, - { from: 'cycle1_output', to: 'cycle2_input' }, - { from: 'cycle2_output', to: 'output_input' }, - { from: 'switch1_output', to: 'db_connector1_input' }, - { from: 'switch1_output', to: 'db_connector2_input' }, - { from: 'switch1_output', to: 'db_connector3_input' }, - { from: 'db_connector1_output', to: 'function1_input' }, - { from: 'db_connector2_output', to: 'function2_input' }, - { from: 'db_connector3_output', to: 'function3_input' }, - { from: 'function1_output', to: 'cycle1_input' }, - { from: 'function2_output', to: 'cycle1_input' }, - { from: 'function3_output', to: 'cycle1_input' }, - ] -}; diff --git a/projects/f-pro-examples/visual-programming/domain/i-flow-view-model.ts b/projects/f-pro-examples/visual-programming/domain/i-flow-view-model.ts deleted file mode 100644 index 0eb27f06..00000000 --- a/projects/f-pro-examples/visual-programming/domain/i-flow-view-model.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IFlowConnectionViewModel } from './connection/i-flow-connection-view-model'; -import { IFlowNodeViewModel } from './node/i-flow-node-view-model'; - -export interface IFlowViewModel { - - nodes: IFlowNodeViewModel[]; - - connections: IFlowConnectionViewModel[]; -} diff --git a/projects/f-pro-examples/visual-programming/domain/index.ts b/projects/f-pro-examples/visual-programming/domain/index.ts deleted file mode 100644 index 8c805c67..00000000 --- a/projects/f-pro-examples/visual-programming/domain/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -export * from './connection/add-new/create-connection.handler'; -export * from './connection/add-new/create-connection.request'; - -export * from './connection/map/map-to-connection-view-model.handler'; - -export * from './connection/reassign/reassign-connection.handler'; -export * from './connection/reassign/reassign-connection.request'; - -export * from './connection/i-flow-connection-storage-model'; -export * from './connection/i-flow-connection-view-model'; - -export * from './node/add-new/add-new-node-to-flow.handler'; -export * from './node/add-new/add-new-node-to-flow.request'; - -export * from './node/map/map-to-node-view-model.handler'; - -export * from './node/move-nodes/move-node.handler'; -export * from './node/move-nodes/move-node.request'; - -export * from './node/i-flow-node-storage-model'; -export * from './node/i-flow-node-view-model'; - -export * from './configuration'; -export * from './e-node-type'; -export * from './flow.service'; -export * from './flow.storage'; -export * from './i-flow-view-model'; diff --git a/projects/f-pro-examples/visual-programming/domain/node/add-new/add-new-node-to-flow.handler.ts b/projects/f-pro-examples/visual-programming/domain/node/add-new/add-new-node-to-flow.handler.ts deleted file mode 100644 index 9eb7eb8e..00000000 --- a/projects/f-pro-examples/visual-programming/domain/node/add-new/add-new-node-to-flow.handler.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IHandler } from '@foblex/mediator'; -import { AddNewNodeToFlowRequest } from './add-new-node-to-flow.request'; -import { IFlowStorage } from '../../flow.storage'; -import { generateGuid } from '@foblex/utils'; - -export class AddNewNodeToFlowHandler implements IHandler { - - constructor( - private flow: IFlowStorage - ) { - } - - public handle(request: AddNewNodeToFlowRequest): void { - this.flow.nodes.push({ - id: generateGuid(), - input: generateGuid(), - output: generateGuid(), - type: request.type, - position: request.position, - }); - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/node/add-new/add-new-node-to-flow.request.ts b/projects/f-pro-examples/visual-programming/domain/node/add-new/add-new-node-to-flow.request.ts deleted file mode 100644 index 76b7ab9e..00000000 --- a/projects/f-pro-examples/visual-programming/domain/node/add-new/add-new-node-to-flow.request.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IPoint } from '@foblex/2d'; -import { ENodeType } from '../../e-node-type'; - -export class AddNewNodeToFlowRequest { - - constructor( - public readonly type: ENodeType, - public readonly position: IPoint, - ) { - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/node/i-flow-node-storage-model.ts b/projects/f-pro-examples/visual-programming/domain/node/i-flow-node-storage-model.ts deleted file mode 100644 index 6d137b58..00000000 --- a/projects/f-pro-examples/visual-programming/domain/node/i-flow-node-storage-model.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ENodeType } from '../e-node-type'; -import { IPoint } from '@foblex/2d'; - -export interface IFlowNodeStorageModel { - - id: string; - - input?: string; - - output?: string; - - type: ENodeType; - - position: IPoint; -} diff --git a/projects/f-pro-examples/visual-programming/domain/node/i-flow-node-view-model.ts b/projects/f-pro-examples/visual-programming/domain/node/i-flow-node-view-model.ts deleted file mode 100644 index 239624d9..00000000 --- a/projects/f-pro-examples/visual-programming/domain/node/i-flow-node-view-model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IFlowNodeStorageModel } from './i-flow-node-storage-model'; - -export interface IFlowNodeViewModel extends IFlowNodeStorageModel { - - color: string; -} diff --git a/projects/f-pro-examples/visual-programming/domain/node/map/map-to-node-view-model.handler.ts b/projects/f-pro-examples/visual-programming/domain/node/map/map-to-node-view-model.handler.ts deleted file mode 100644 index 5bad6f3b..00000000 --- a/projects/f-pro-examples/visual-programming/domain/node/map/map-to-node-view-model.handler.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { IHandler } from '@foblex/mediator'; -import { IFlowNodeViewModel } from '../i-flow-node-view-model'; -import { IFlowStorage } from '../../flow.storage'; -import { NODE_CONFIGURATION } from '../../configuration'; - -export class MapToNodeViewModelHandler implements IHandler { - - constructor( - private flow: IFlowStorage - ) { - } - - public handle(): IFlowNodeViewModel[] { - return this.flow.nodes.map((node) => { - return { - ...node, - color: NODE_CONFIGURATION[ node.type ].color, - }; - }); - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/node/move-nodes/move-node.handler.ts b/projects/f-pro-examples/visual-programming/domain/node/move-nodes/move-node.handler.ts deleted file mode 100644 index 950d294f..00000000 --- a/projects/f-pro-examples/visual-programming/domain/node/move-nodes/move-node.handler.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IHandler } from '@foblex/mediator'; -import { MoveNodeRequest } from './move-node.request'; -import { IFlowStorage } from '../../flow.storage'; - -export class MoveNodeHandler implements IHandler { - - constructor( - private flow: IFlowStorage - ) { - } - - public handle(request: MoveNodeRequest): void { - const node = this.flow.nodes.find((x) => x.id === request.id); - if (!node) { - throw new Error(`Node with id ${ request.id } not found`); - } - node.position = request.position; - } -} diff --git a/projects/f-pro-examples/visual-programming/domain/node/move-nodes/move-node.request.ts b/projects/f-pro-examples/visual-programming/domain/node/move-nodes/move-node.request.ts deleted file mode 100644 index 41d4d9d7..00000000 --- a/projects/f-pro-examples/visual-programming/domain/node/move-nodes/move-node.request.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IPoint } from '@foblex/2d'; - -export class MoveNodeRequest { - - constructor( - public readonly id: string, - public readonly position: IPoint, - ) { - } -} diff --git a/projects/f-pro-examples/visual-programming/index.ts b/projects/f-pro-examples/visual-programming/index.ts deleted file mode 100644 index a3b00919..00000000 --- a/projects/f-pro-examples/visual-programming/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './components/flow/vp-flow.component'; - -export * from './components/node/vp-node.component'; - -export * from './components/palette/vp-palette.component'; - -export * from './components/toolbar/vp-toolbar.component'; - -export * from './domain'; diff --git a/public/markdown/examples/connection-behaviours.md b/public/markdown/examples/connection-behaviours.md index df9d98fa..395371d7 100644 --- a/public/markdown/examples/connection-behaviours.md +++ b/public/markdown/examples/connection-behaviours.md @@ -7,12 +7,8 @@ This guide shows how to set up different connection behaviours, allowing for dif ## Example ::: ng-component [height]="600" -[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.html -[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.ts -[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-behaviours/connection-behaviours.component.scss +[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-behaviours/connection-behaviours.html +[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-behaviours/connection-behaviours.ts +[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-behaviours/connection-behaviours.scss [common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss ::: - - - - diff --git a/public/markdown/examples/connection-center.md b/public/markdown/examples/connection-center.md deleted file mode 100644 index 07892c5b..00000000 --- a/public/markdown/examples/connection-center.md +++ /dev/null @@ -1,20 +0,0 @@ -# Connection Center - -## ⚠️ Deprecated - -The `fConnectionCenter` directive is deprecated and should not be used in new projects. -Instead, use the more flexible [fConnectionContent](./examples/connection-content) directive, which allows you to place multiple elements (labels, icons, buttons) anywhere along a connection with full control over position, offset, and alignment. - -## Description - -The `fConnectionCenter` directive allows you to insert any custom content inside a block, which will be positioned relative to the center of the connection line. This is useful for adding labels, icons, or other elements that need to be dynamically displayed at the center of a connection. -The directive provides flexible styling and positioning, allowing you to fully customize the appearance and behavior of the centered content. - -## Example - -::: ng-component [height]="600" -[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-center/connection-center.component.html -[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-center/connection-center.component.ts -[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-center/connection-center.component.scss -[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss -::: diff --git a/public/markdown/examples/connection-markers.md b/public/markdown/examples/connection-markers.md index d86e18a7..b0b31d69 100644 --- a/public/markdown/examples/connection-markers.md +++ b/public/markdown/examples/connection-markers.md @@ -2,17 +2,14 @@ ## Description -This guide shows how to set up different [connection markers](./docs/f-connection-marker-directive), allowing various markers to be displayed on connections between connectors. To enable this feature, you need to add an SVG element with the [f-connection-marker directive](./docs/f-connection-marker-directive) inside the -[f-connection component](./docs/f-connection-component). +This guide shows how to set up different [connection markers](./docs/f-connection-marker-directive), allowing various markers to be displayed on connections between connectors. To enable this feature, you need to add an SVG element with the [f-connection-marker directive](./docs/f-connection-marker-directive) inside the +[f-connection component](./docs/f-connection-component). ## Example ::: ng-component [height]="600" -[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-markers/connection-markers.component.html -[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-markers/connection-markers.component.ts -[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-markers/connection-markers.component.scss +[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-markers/connection-markers.html +[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-markers/connection-markers.ts +[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-markers/connection-markers.scss [common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss ::: - - - diff --git a/public/markdown/examples/connection-text.md b/public/markdown/examples/connection-text.md deleted file mode 100644 index 2c9bca06..00000000 --- a/public/markdown/examples/connection-text.md +++ /dev/null @@ -1,20 +0,0 @@ -# Connection Text - -## ⚠️ Deprecated - -The `fText` property on connections is deprecated. -For adding text, labels, or any custom markup along a connection, you should now use the more powerful [fConnectionContent](./examples/connection-content) directive. It provides full control over position, offset, alignment, and supports placing multiple elements on a single connection. - -## Description - -This guide demonstrates how to add text to connections in Foblex Flow for Angular. To add text to a connection, you need to set the `fText` property of the connection component. The text will be displayed in the middle of the connection line. Also, you can set `fTextStartOffset` parameter to adjust the text position along the connection line. -You can customize the text appearance using css styles. - -## Example - -::: ng-component [height]="600" -[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-text/connection-text.component.html -[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-text/connection-text.component.ts -[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-text/connection-text.component.scss -[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss -::: diff --git a/public/markdown/examples/connection-waypoints.md b/public/markdown/examples/connection-waypoints.md new file mode 100644 index 00000000..2341e958 --- /dev/null +++ b/public/markdown/examples/connection-waypoints.md @@ -0,0 +1,39 @@ +# Connection Waypoints + +## Description + +This example demonstrates the new Connection Waypoints feature in Foblex Flow. + +Waypoints are user-defined points placed on a connection that let you manually shape the route between two nodes. +They work with any built-in connection type - Straight, Segment, Bezier, and Adaptive Curve - and always stay fully interactive. + +What you can do in this example: +- **See waypoint candidates** (green markers) generated along each connection segment. +- **Add a waypoint** by clicking a candidate — it becomes part of the connection route. +- **Move existing waypoints** (blue markers) to reshape the connection in real time. +- **Bind waypoints to your app state** via **[(waypoints)]**, keeping full control over the data model. +- Listen to (fConnectionWaypointsChanged) to react when the user changes the route. + +Usage is simple - just place **** inside ****: +- **[(waypoints)]** - two-way binding for the waypoint list (your state). +- **radius** - visual size of candidates and waypoints. + +How to use +- Add a waypoint: drag a green candidate point onto the connection. +- Move a waypoint: drag an existing waypoint (blue point). +- Remove a waypoint: right-click on a waypoint. + +You can also control the feature in the toolbar: +- Show Waypoints - toggles waypoint rendering. +- Enable Waypoints - enables/disables waypoint interaction. + +This feature is designed to be **data-driven**: the library provides interaction and intent, while the waypoint array belongs to your application. + +## Example + +::: ng-component [height]="600" +[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-waypoints/connection-waypoints.html +[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-waypoints/connection-waypoints.ts +[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-waypoints/connection-waypoints.scss +[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss +::: diff --git a/public/markdown/examples/zoom.md b/public/markdown/examples/zoom.md index fac5bedf..00b93ac9 100644 --- a/public/markdown/examples/zoom.md +++ b/public/markdown/examples/zoom.md @@ -2,8 +2,19 @@ ## Description -This guide shows how to `zoom in` and `out` of the [canvas](./docs/f-canvas-component) using the `mouse wheel`, `double click`, and `buttons`. -To enable zooming, you need to add the [fZoom directive](./docs/f-zoom-directive) to the [f-canvas](./docs/f-canvas-component). +This guide shows how to `zoom in` and `zoom out` of the [canvas](./docs/f-canvas-component) using: + +- **Mouse wheel** +- **Double click** +- **Buttons** +- **Pinch-to-zoom** (trackpad / touchscreen) + +To enable zooming, add the [fZoom directive](./docs/f-zoom-directive) to the [f-canvas](./docs/f-canvas-component). + +### Notes + +- **Pinch-to-zoom** works on devices that support multi-touch gestures (touch screens and trackpads). +- The directive applies zoom with sensible limits to keep interactions smooth and predictable. ## Example @@ -13,6 +24,3 @@ To enable zooming, you need to add the [fZoom directive](./docs/f-zoom-directive [component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/zoom/zoom.component.scss [common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss ::: - - - diff --git a/public/markdown/guides/f-connection-component.md b/public/markdown/guides/f-connection-component.md index 4ec0d1f9..22c1692f 100644 --- a/public/markdown/guides/f-connection-component.md +++ b/public/markdown/guides/f-connection-component.md @@ -24,10 +24,6 @@ The **FConnectionComponent** is a component that represents a connection between - `fType: InputSignal;` The visual type of the connection, such as straight, bezier and etc. Accepts a value from [EFConnectionType]() enum or string for custom connection. Default: `EFConnectionType.STRAIGHT` -- `fText: InputSignal;` The text displayed on the connection. Default: `null` - -- `fTextStartOffset: InputSignal;` The offset of the text from the start of the connection. Default: `50%` - - `fOffset: InputSignal;` Minimum length of the connection before a curve can occur. Default: `12` - `fRadius: InputSignal;` Radius used for curves. Default: `8` @@ -61,37 +57,37 @@ Add the `f-connection` component to the [f-canvas](f-canvas-component). Provide #### Different Connection Types Examples of different connection types. The connection type can be set using the `fType` input. Valid values are `straight`, `bezier` and `segment from [EFConnectionType](e-f-connection-type) enum. -::: ng-component -[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/connection-type/connection-type.component.html -[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/connection-type/connection-type.component.ts -[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/connection-type/connection-type.component.scss -[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/_flow-common.scss +::: ng-component [height]="600" +[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-types/connection-types.html +[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-types/connection-types.ts +[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-types/connection-types.scss +[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss ::: #### Different Connection Behaviours Examples of different connection behaviours. The connection behaviour can be set using the `fBehavior` input. Valid values are: `fixed`, `fixed_center` and `floating` from [EFConnectionBehaviour](e-f-connection-behaviour) enum. -::: ng-component -[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.html -[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.ts -[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/connection-behaviour/connection-behaviour.component.scss -[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/_flow-common.scss +::: ng-component [height]="600" +[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-behaviours/connection-behaviours.html +[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-behaviours/connection-behaviours.ts +[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/connection-behaviours/connection-behaviours.scss +[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss ::: #### Custom Connection Type Examples of providing custom connection types. The connection type can be set using an array of providers. -::: ng-component +::: ng-component [height]="600" [component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/custom-connection-type/custom-connection-type.component.html [component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/custom-connection-type/custom-connection-type.component.ts [component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/custom-connection-type/custom-connection-type.component.scss -[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/_flow-common.scss +[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss ::: -#### Reassign Connection +#### Drag to Reassign Each connection can be reassigned to another [fNodeInput](f-node-input-directive). The `fReassignDisabled` property can be used to disable this feature. Each connection has a `DragHandle` at the end, drag it to reassign the connection to another [fNodeInput](f-node-input-directive). -::: ng-component +::: ng-component [height]="600" [component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.html [component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.ts [component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component.scss diff --git a/public/markdown/guides/f-drag-handle-directive.md b/public/markdown/guides/f-drag-handle-directive.md index c05be895..07be6be6 100644 --- a/public/markdown/guides/f-drag-handle-directive.md +++ b/public/markdown/guides/f-drag-handle-directive.md @@ -46,9 +46,9 @@ This code snippet shows how to disable dragging for a node. This example demonstrates how to use the **fDragHandle** directive to specify the handle for dragging a node. -::: ng-component -[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/node/node-with-drag-handle-example/node-with-drag-handle-example.component.html -[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/node/node-with-drag-handle-example/node-with-drag-handle-example.component.ts -[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/node/node-with-drag-handle-example/node-with-drag-handle-example.component.scss -[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-guides-examples/_flow-common.scss +::: ng-component [height]="600" +[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/nodes/drag-handle/drag-handle.html +[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/nodes/drag-handle/drag-handle.ts +[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/nodes/drag-handle/drag-handle.scss +[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss ::: diff --git a/public/previews/examples/connection-waypoints.dark.png b/public/previews/examples/connection-waypoints.dark.png new file mode 100644 index 00000000..a5dff99f Binary files /dev/null and b/public/previews/examples/connection-waypoints.dark.png differ diff --git a/public/previews/examples/connection-waypoints.light.png b/public/previews/examples/connection-waypoints.light.png new file mode 100644 index 00000000..06d1c7ab Binary files /dev/null and b/public/previews/examples/connection-waypoints.light.png differ diff --git a/routes.txt b/routes.txt index 887cb4a3..7ab4c213 100644 --- a/routes.txt +++ b/routes.txt @@ -47,8 +47,6 @@ /examples/custom-connection-type /examples/connection-behaviours /examples/connection-markers -/examples/connection-text -/examples/connection-center /examples/connection-content /examples/custom-connections /examples/connection-connectable-side diff --git a/server.ts b/server.ts index c929e155..bbf04d70 100644 --- a/server.ts +++ b/server.ts @@ -4,9 +4,6 @@ import express from 'express'; import { fileURLToPath } from 'node:url'; import { dirname, join, resolve } from 'node:path'; import bootstrap from './src/main.server'; -// import 'dotenv/config'; -// import cookieParser from 'cookie-parser'; -// import {setupServerRoutes} from "./server/setup-server-routes"; export function app(): express.Express { const server = express(); @@ -16,9 +13,6 @@ export function app(): express.Express { const commonEngine = new CommonEngine(); - // server.use(cookieParser()); - // setupServerRoutes(server); - server.set('view engine', 'html'); server.set('views', browserDistFolder); diff --git a/server/auth.middleware.ts b/server/auth.middleware.ts deleted file mode 100644 index f91268d4..00000000 --- a/server/auth.middleware.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import jwt from 'jsonwebtoken'; -import { JWT_SECRET } from './env'; - -export function authMiddleware(req: Request, res: Response, next: NextFunction): void { - const token = req.cookies['token']; - if (!token) { - res.status(401).send('Unauthorized'); - return; - } - - try { - const decoded = jwt.verify(token, JWT_SECRET); - (req as any).user = decoded; - next(); - return; - } catch { - res.status(401).send('Invalid token'); - return; - } -} diff --git a/server/auth.routes.ts b/server/auth.routes.ts deleted file mode 100644 index d1fccd6b..00000000 --- a/server/auth.routes.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Router } from 'express'; -import axios from 'axios'; -import jwt from 'jsonwebtoken'; -import {GITHUB_CALLBACK_URL, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, JWT_SECRET} from "./env"; - -export const authRouter = Router(); - -authRouter.get('/auth/github', (req, res) => { - const redirectUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${GITHUB_CALLBACK_URL}&scope=read:user%20user:email`; - res.redirect(redirectUrl); -}); - -authRouter.get('/auth/github/callback', async (req, res) => { - const code = req.query['code']; - - try { - const tokenRes = await axios.post( - 'https://github.com/login/oauth/access_token', - { - client_id: GITHUB_CLIENT_ID, - client_secret: GITHUB_CLIENT_SECRET, - code, - }, - { headers: { Accept: 'application/json' } } - ); - - const accessToken = tokenRes.data.access_token; - - const userRes = await axios.get('https://api.github.com/user', { - headers: { Authorization: `token ${accessToken}` }, - }); - - const user = userRes.data; - - const jwtToken = jwt.sign( - { id: user.id, login: user.login, avatar: user.avatar_url }, - JWT_SECRET, - { expiresIn: '7d' } - ); - - res.cookie('token', jwtToken, { - httpOnly: true, - secure: true, - sameSite: 'lax', - maxAge: 7 * 24 * 60 * 60 * 1000, - }); - - res.redirect('/'); - } catch (err) { - console.error('GitHub Auth Error:', err); - res.status(500).send('Authentication failed'); - } -}); diff --git a/server/env.ts b/server/env.ts deleted file mode 100644 index 9ab6fed1..00000000 --- a/server/env.ts +++ /dev/null @@ -1,12 +0,0 @@ -function getEnvVar(key: string): string { - const value = process.env[key]; - if (!value) { - throw new Error(`Missing environment variable: ${key}`); - } - return value; -} - -export const GITHUB_CLIENT_ID = getEnvVar('GITHUB_CLIENT_ID'); -export const GITHUB_CLIENT_SECRET = getEnvVar('GITHUB_CLIENT_SECRET'); -export const GITHUB_CALLBACK_URL = getEnvVar('GITHUB_CALLBACK_URL'); -export const JWT_SECRET = getEnvVar('JWT_SECRET'); diff --git a/server/setup-server-routes.ts b/server/setup-server-routes.ts deleted file mode 100644 index 19b6e5e8..00000000 --- a/server/setup-server-routes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Router } from 'express'; -import { authRouter } from './auth.routes'; -import { authMiddleware } from './auth.middleware'; - -export function setupServerRoutes(router: Router) { - router.use(authRouter); - - router.get('/api/me', authMiddleware, (req, res) => { - res.json((req as any).user); - }); -} diff --git a/src/app/app.component.html b/src/app/app.component.html index 6550df27..246030b1 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,4 @@ - + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5cccf32a..715bb26e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,69 +1,25 @@ -import {Component, DestroyRef, inject, OnInit} from '@angular/core'; -import {ActivatedRoute, RouterOutlet} from '@angular/router'; -import {MatIconRegistry} from '@angular/material/icon'; -import {takeScreenshot} from './take-screenshot'; -import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; -import {startWith, take} from "rxjs"; -import {MatDialog} from "@angular/material/dialog"; -import {AssistantComponent} from "./assistant/assistant.component"; -import {Router} from '@angular/router'; +import { Component, inject, OnInit } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { MatIconRegistry } from '@angular/material/icon'; @Component({ selector: 'app-root', standalone: true, - imports: [ - RouterOutlet, - ], + imports: [RouterOutlet], host: { 'ngSkipHydration': '', }, templateUrl: './app.component.html', - styleUrl: './app.component.scss' + styleUrl: './app.component.scss', }) export class AppComponent implements OnInit { - private readonly _iconRegistry = inject(MatIconRegistry); - private readonly _route = inject(ActivatedRoute); - private readonly _router = inject(Router); - private readonly _dialog = inject(MatDialog); - private readonly _destroyRef = inject(DestroyRef); public ngOnInit() { this._setDefaultFontSetClass(); - // this._listenForSearch(); } private _setDefaultFontSetClass(): void { this._iconRegistry.setDefaultFontSetClass('material-symbols-outlined'); } - - // private _listenForSearch(): void { - // this._route.fragment.pipe( - // takeUntilDestroyed(this._destroyRef), startWith(this._route.snapshot.fragment) - // ).subscribe(fragment => { - // if (fragment === 'search') { - // this._openAssistant(); - // } - // }); - // } - - // private _openAssistant(): void { - // this._dialog.open(AssistantComponent, { - // width: '800px', maxHeight: '80vh', - // }).afterClosed().pipe( - // take(1), takeUntilDestroyed(this._destroyRef) - // ).subscribe(() => this._clearSearch()); - // } - // - // private _clearSearch(): void { - // this._router.navigate([], { - // fragment: '', - // queryParamsHandling: 'preserve', - // replaceUrl: true, // чтобы не добавлять новую запись в историю - // }); - // } - // - // protected takeScreenshot(): void { - // takeScreenshot('f-flow').then(); - // } } diff --git a/src/app/assistant/assistant.component.html b/src/app/assistant/assistant.component.html deleted file mode 100644 index 90ade9a7..00000000 --- a/src/app/assistant/assistant.component.html +++ /dev/null @@ -1,13 +0,0 @@ -
- - - @if (!user()) { - - } @else if(question()) { - - } -
-@if (answer()) { -
-} - diff --git a/src/app/assistant/assistant.component.scss b/src/app/assistant/assistant.component.scss deleted file mode 100644 index 474eb2a9..00000000 --- a/src/app/assistant/assistant.component.scss +++ /dev/null @@ -1,82 +0,0 @@ -.assistant-close { - appearance: none; - border-radius: 4px; - color: #67676c; - cursor: pointer; - stroke-width: 1.4; - background: 0 0; - border: none; - padding: 2px; - transition: all .1s ease-in-out; - display: flex; - - &:hover { - background: #646cff24; - color: #3451b2; - } -} - -.f-button { - border: none; - font-weight: 500; -} - -.assistant-header { - display: flex; - justify-content: flex-start; - align-items: center; - - height: 56px; - padding: 12px 16px; - border-radius: 4px 4px 0 0; - margin: 0; - - .f-icon { - color: var(--indigo-1); - justify-content: center; - align-items: center; - display: flex - } - - input { - padding: 0 0 0 8px; - appearance: none; - color: var(--primary-text); - font: inherit; - background: 0 0; - border: 0; - outline: none; - flex: 1; - width: 100%; - height: 100%; - font-size: 19px; - font-weight: 300; - - &::placeholder { - color: var(--tertiary-text); - } - } - - &.not-empty { - border-bottom: 1px solid var(--divider-color); - } -} - -.assistant-body { - padding: 16px 48px; -} - -//.signin-btn { -// font-size: 13px; -// padding: 6px 12px; -// background: var(--button-primary-bg); -// color: var(--button-primary-text); -// border: none; -// border-radius: var(--border-radius); -// cursor: pointer; -// transition: background-color 0.2s; -//} -// -//.signin-btn:hover { -// background: var(--button-primary-hover-bg); -//} diff --git a/src/app/assistant/assistant.component.ts b/src/app/assistant/assistant.component.ts deleted file mode 100644 index 17fc9e72..00000000 --- a/src/app/assistant/assistant.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {ChangeDetectionStrategy, Component, inject, model} from "@angular/core"; -import {FormsModule} from "@angular/forms"; -import {MatInput} from "@angular/material/input"; -import {MatButtonModule} from "@angular/material/button"; -import {UserService} from "../auth/user-service"; -import {toSignal} from "@angular/core/rxjs-interop"; -import {SafeHtml} from "@angular/platform-browser"; - -@Component({ - selector: 'assistant', - templateUrl: './assistant.component.html', - styleUrls: ['./assistant.component.scss'], - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - FormsModule, - MatInput, - MatButtonModule, - ] -}) -export class AssistantComponent { - - protected readonly user = toSignal(inject(UserService).user$); - - protected readonly question = model(); - protected readonly answer = model(); -} diff --git a/src/app/auth/user-service.ts b/src/app/auth/user-service.ts deleted file mode 100644 index abd5cd84..00000000 --- a/src/app/auth/user-service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; - -export interface User { - id: number; - login: string; - avatar: string; -} - -@Injectable({ providedIn: 'root' }) -export class UserService { - private readonly _user$ = new BehaviorSubject(null); - public readonly user$ = this._user$.asObservable(); - - constructor(private http: HttpClient) { - this.http.get('/api/me').subscribe({ - next: user => this._user$.next(user), - error: () => this._user$.next(null) - }); - } - - login() { - window.location.href = '/auth/github'; - } - - logout() { - document.cookie = 'token=; Max-Age=0; path=/'; - window.location.reload(); - } -} diff --git a/src/app/documentation.config.ts b/src/app/documentation.config.ts index bffec9b0..0ff3bff6 100644 --- a/src/app/documentation.config.ts +++ b/src/app/documentation.config.ts @@ -1,18 +1,21 @@ import { defineLazyComponent, - defineNavigationGroup, INavigationGroup, + defineNavigationGroup, + INavigationGroup, provide404Markdown, provideComponents, provideDirectory, provideFooterNavigation, provideHeader, provideHeaderMediaLinks, - provideHeaderNavigation, provideHeaderSearch, + provideHeaderNavigation, + provideHeaderSearch, provideLanguage, provideLogo, provideNavigation, provideTitle, - provideTableOfContent, provideMeta, + provideTableOfContent, + provideMeta, } from '@foblex/m-render'; export const DOCUMENTATION_CONFIGURATION = { @@ -28,58 +31,159 @@ export const DOCUMENTATION_CONFIGURATION = { nodeGroup(), connectorGroup(), connectionGroup(), - extendsGroup() + extendsGroup(), ), provideComponents([ - defineLazyComponent('simple-flow', () => import('../../projects/f-guides-examples/simple-flow/simple-flow.component')), - defineLazyComponent('draggable-flow', () => import('../../projects/f-guides-examples/draggable-flow/draggable-flow.component')), - defineLazyComponent('connection-type', () => import('../../projects/f-guides-examples/connection-type/connection-type.component')), - defineLazyComponent('connection-behaviour', () => import('../../projects/f-guides-examples/connection-behaviour/connection-behaviour.component')), - defineLazyComponent('custom-connection-type', () => import('../../projects/f-examples/connections/custom-connection-type/custom-connection-type.component')), - defineLazyComponent('drag-to-connect', () => import('../../projects/f-examples/connections/drag-to-connect/drag-to-connect.component')), - defineLazyComponent('drag-to-reassign', () => import('../../projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component')), - defineLazyComponent('drag-snap-connection', () => import('../../projects/f-guides-examples/drag-snap-connection/drag-snap-connection.component')), - defineLazyComponent('connectable-side', () => import('../../projects/f-guides-examples/connectable-side/connectable-side.component')), - defineLazyComponent('connection-from-outlet', () => import('../../projects/f-guides-examples/connection-from-outlet/connection-from-outlet.component')), - defineLazyComponent('connection-markers', () => import('../../projects/f-guides-examples/connection-markers/connection-markers.component')), - defineLazyComponent('zoom-example', () => import('../../projects/f-guides-examples/zoom-example/zoom-example.component')), - defineLazyComponent('background-example', () => import('../../projects/f-guides-examples/background-example/background-example.component')), - defineLazyComponent('line-alignment-example', () => import('../../projects/f-guides-examples/line-alignment-example/line-alignment-example.component')), - defineLazyComponent('minimap-basic-example', () => import('../../projects/f-guides-examples/minimap-basic-example/minimap-basic-example.component')), - defineLazyComponent('minimap-scaled-example', () => import('../../projects/f-guides-examples/minimap-scaled-example/minimap-scaled-example.component')), - defineLazyComponent('node-with-connectors', () => import('../../projects/f-guides-examples/node-with-connectors/node-with-connectors.component')), - defineLazyComponent('node-with-position-example', () => import('../../projects/f-guides-examples/node/node-with-position-example/node-with-position-example.component')), - defineLazyComponent('adding-dragging-functionality-example', () => import('../../projects/f-guides-examples/node/adding-dragging-functionality-example/adding-dragging-functionality-example.component')), - defineLazyComponent('node-with-drag-handle-example', () => import('../../projects/f-guides-examples/node/node-with-drag-handle-example/node-with-drag-handle-example.component')), + defineLazyComponent( + 'simple-flow', + () => import('../../projects/f-guides-examples/simple-flow/simple-flow.component'), + ), + defineLazyComponent( + 'draggable-flow', + () => import('../../projects/f-guides-examples/draggable-flow/draggable-flow.component'), + ), + defineLazyComponent( + 'connection-types', + () => import('../../projects/f-examples/connections/connection-types/connection-types'), + ), + defineLazyComponent( + 'connection-behaviours', + () => + import( + '../../projects/f-examples/connections/connection-behaviours/connection-behaviours' + ), + ), + defineLazyComponent( + 'custom-connection-type', + () => + import( + '../../projects/f-examples/connections/custom-connection-type/custom-connection-type.component' + ), + ), + defineLazyComponent( + 'drag-to-connect', + () => import('../../projects/f-examples/connections/drag-to-connect/drag-to-connect'), + ), + defineLazyComponent( + 'drag-to-reassign', + () => import('../../projects/f-examples/connections/drag-to-reassign/drag-to-reassign'), + ), + defineLazyComponent( + 'drag-snap-connection', + () => + import( + '../../projects/f-guides-examples/drag-snap-connection/drag-snap-connection.component' + ), + ), + defineLazyComponent( + 'connectable-side', + () => + import('../../projects/f-guides-examples/connectable-side/connectable-side.component'), + ), + defineLazyComponent( + 'connection-from-outlet', + () => + import( + '../../projects/f-guides-examples/connection-from-outlet/connection-from-outlet.component' + ), + ), + defineLazyComponent( + 'connection-markers', + () => + import( + '../../projects/f-guides-examples/connection-markers/connection-markers.component' + ), + ), + defineLazyComponent( + 'zoom-example', + () => import('../../projects/f-guides-examples/zoom-example/zoom-example.component'), + ), + defineLazyComponent( + 'background-example', + () => + import( + '../../projects/f-guides-examples/background-example/background-example.component' + ), + ), + defineLazyComponent( + 'line-alignment-example', + () => + import( + '../../projects/f-guides-examples/line-alignment-example/line-alignment-example.component' + ), + ), + defineLazyComponent( + 'minimap-basic-example', + () => + import( + '../../projects/f-guides-examples/minimap-basic-example/minimap-basic-example.component' + ), + ), + defineLazyComponent( + 'minimap-scaled-example', + () => + import( + '../../projects/f-guides-examples/minimap-scaled-example/minimap-scaled-example.component' + ), + ), + defineLazyComponent( + 'node-with-connectors', + () => + import( + '../../projects/f-guides-examples/node-with-connectors/node-with-connectors.component' + ), + ), + defineLazyComponent( + 'node-with-position-example', + () => + import( + '../../projects/f-guides-examples/node/node-with-position-example/node-with-position-example.component' + ), + ), + defineLazyComponent( + 'adding-dragging-functionality-example', + () => + import( + '../../projects/f-guides-examples/node/adding-dragging-functionality-example/adding-dragging-functionality-example.component' + ), + ), + defineLazyComponent( + 'drag-handle', + () => import('../../projects/f-examples/nodes/drag-handle/drag-handle'), + ), ]), provideTableOfContent({ title: 'In this articles', - range: {start: 2, end: 6}, + range: { start: 2, end: 6 }, }), provideHeader( provideHeaderSearch(false), - provideHeaderNavigation([{ - link: '/docs/get-started', - active: '/docs', - text: 'Docs', - }, { - link: '/examples/overview', - active: '/examples', - text: 'Examples', - }, { - link: '/showcase/overview', - active: '/showcase', - text: 'Showcase', - }]), + provideHeaderNavigation([ + { + link: '/docs/get-started', + active: '/docs', + text: 'Docs', + }, + { + link: '/examples/overview', + active: '/examples', + text: 'Examples', + }, + { + link: '/showcase/overview', + active: '/showcase', + text: 'Showcase', + }, + ]), provideHeaderMediaLinks([ - {icon: 'github', link: 'https://github.com/Foblex/f-flow'}, - {icon: 'twitter', link: 'https://x.com/foblexflow'}, + { icon: 'github', link: 'https://github.com/Foblex/f-flow' }, + { icon: 'twitter', link: 'https://x.com/foblexflow' }, ]), ), provideFooterNavigation({ editLink: { pattern: 'https://github.com/foblex/f-flow/edit/main/public/docs/', - text: 'Edit this page on GitHub' + text: 'Edit this page on GitHub', }, previous: 'Previous Page', next: 'Next Page', @@ -90,114 +194,154 @@ export const DOCUMENTATION_CONFIGURATION = { title: 'Angular Library for Flow-Based UIs - Foblex Flow', app_name: 'Foblex Flow', locale: 'en', - description: 'Foblex Flow is an Angular library that simplifies the creation of flow-based UIs, providing components for building interactive UIs with nodes and connections', + description: + 'Foblex Flow is an Angular library that simplifies the creation of flow-based UIs, providing components for building interactive UIs with nodes and connections', image: 'https://flow.foblex.com/site-preview.png', image_type: 'image/png', image_width: 2986, - image_height: 1926 + image_height: 1926, }), ], }; - function introductionGroup(): INavigationGroup { - return defineNavigationGroup('Introduction', [{ - link: 'intro', - text: 'Introducing Foblex Flow', - description: "Introducing Foblex Flow — Angular library for building flowcharts, graphs and interactive node-based UIs with connections", - }, { - link: 'get-started', - text: 'Installation and Rendering', - description: "Get started with Foblex Flow — Angular flowchart & diagram library. Install, render nodes and connections, copy-paste snippets.", - }]) + return defineNavigationGroup('Introduction', [ + { + link: 'intro', + text: 'Introducing Foblex Flow', + description: + 'Introducing Foblex Flow — Angular library for building flowcharts, graphs and interactive node-based UIs with connections', + }, + { + link: 'get-started', + text: 'Installation and Rendering', + description: + 'Get started with Foblex Flow — Angular flowchart & diagram library. Install, render nodes and connections, copy-paste snippets.', + }, + ]); } function containerGroup(): INavigationGroup { - return defineNavigationGroup('Containers', [{ - link: 'f-flow-component', - text: 'Flow', - description: 'Angular Flow Component — core diagram builder. Inputs, outputs, events and patterns for creating scalable flowcharts & graphs.', - }, { - link: 'f-canvas-component', - text: 'Canvas', - description: 'Angular Canvas Component — scalable container for flowcharts. Supports zoom, pan, fitting, and rendering interactive diagrams.', - }]) + return defineNavigationGroup('Containers', [ + { + link: 'f-flow-component', + text: 'Flow', + description: + 'Angular Flow Component — core diagram builder. Inputs, outputs, events and patterns for creating scalable flowcharts & graphs.', + }, + { + link: 'f-canvas-component', + text: 'Canvas', + description: + 'Angular Canvas Component — scalable container for flowcharts. Supports zoom, pan, fitting, and rendering interactive diagrams.', + }, + ]); } function nodeGroup(): INavigationGroup { - return defineNavigationGroup('Node', [{ - link: 'f-node-directive', - text: 'Node', - description: 'Angular Node Directive — create draggable, selectable and styled nodes. Build interactive diagrams and node-based UIs.', - image: 'https://flow.foblex.com/f-node-directive.png', - image_type: 'image/png', - image_width: 1458, - image_height: 959, - }, { - link: 'f-drag-handle-directive', - text: 'Drag Handle', - description: 'Angular Drag Handle Directive — define draggable areas for nodes. Precise drag control for custom flowcharts and diagrams.', - }]) + return defineNavigationGroup('Node', [ + { + link: 'f-node-directive', + text: 'Node', + description: + 'Angular Node Directive — create draggable, selectable and styled nodes. Build interactive diagrams and node-based UIs.', + image: 'https://flow.foblex.com/f-node-directive.png', + image_type: 'image/png', + image_width: 1458, + image_height: 959, + }, + { + link: 'f-drag-handle-directive', + text: 'Drag Handle', + description: + 'Angular Drag Handle Directive — define draggable areas for nodes. Precise drag control for custom flowcharts and diagrams.', + }, + ]); } function connectionGroup(): INavigationGroup { - return defineNavigationGroup('Connection', [{ - link: 'f-connection-component', - text: 'Connection', - description: 'Angular Connection Component — customizable connectors between nodes. Supports styles, behaviors and interactive flows.', - }, { - link: 'f-connection-for-create-component', - text: 'Create Connection', - description: 'Angular Create Connection Component — drag to create node links. Build dynamic workflows in Angular diagrams.', - }, { - link: 'f-connection-marker-directive', - text: 'Connection Marker', - description: 'Angular Connection Marker Directive — add start or end markers. SVG customization for flowchart connectors.', - }, { - link: 'f-snap-connection-component', - text: 'Snap Connection', - description: 'Angular Snap Connection Component — auto-snap links to inputs. Intuitive drag-to-connect in flowcharts & node editors.', - }]) + return defineNavigationGroup('Connection', [ + { + link: 'f-connection-component', + text: 'Connection', + description: + 'Angular Connection Component — customizable connectors between nodes. Supports styles, behaviors and interactive flows.', + }, + { + link: 'f-connection-for-create-component', + text: 'Create Connection', + description: + 'Angular Create Connection Component — drag to create node links. Build dynamic workflows in Angular diagrams.', + }, + { + link: 'f-connection-marker-directive', + text: 'Connection Marker', + description: + 'Angular Connection Marker Directive — add start or end markers. SVG customization for flowchart connectors.', + }, + { + link: 'f-snap-connection-component', + text: 'Snap Connection', + description: + 'Angular Snap Connection Component — auto-snap links to inputs. Intuitive drag-to-connect in flowcharts & node editors.', + }, + ]); } function connectorGroup(): INavigationGroup { - return defineNavigationGroup('Connectors', [{ - link: 'f-node-output-directive', - text: 'Output', - description: 'Angular Node Output Directive — manage multiple outputs and sides. Flexible connectors for complex flowcharts & diagrams.', - }, { - link: 'f-node-input-directive', - text: 'Input', - description: 'Angular Node Input Directive — define node inputs with connect rules. Control connections in Angular diagrams & workflows.', - }, { - link: 'f-node-outlet-directive', - text: 'Outlet', - description: 'Angular Node Outlet Directive — single outlet for node connections. Simplify node management in flowcharts & graphs.', - }]); + return defineNavigationGroup('Connectors', [ + { + link: 'f-node-output-directive', + text: 'Output', + description: + 'Angular Node Output Directive — manage multiple outputs and sides. Flexible connectors for complex flowcharts & diagrams.', + }, + { + link: 'f-node-input-directive', + text: 'Input', + description: + 'Angular Node Input Directive — define node inputs with connect rules. Control connections in Angular diagrams & workflows.', + }, + { + link: 'f-node-outlet-directive', + text: 'Outlet', + description: + 'Angular Node Outlet Directive — single outlet for node connections. Simplify node management in flowcharts & graphs.', + }, + ]); } function extendsGroup(): INavigationGroup { - return defineNavigationGroup('Extends', [{ - link: 'f-draggable-directive', - text: 'Drag and Drop', - description: 'Angular Draggable Directive — enable drag-and-drop for nodes. Move, create and reassign elements in flowcharts.' - }, { - link: 'f-zoom-directive', - text: 'Zoom', - description: 'Angular Zoom Directive — zoom and pan canvas with mouse or API. Navigate large diagrams and workflows in Angular.' - }, { - link: 'f-background-component', - text: 'Background', - description: 'Angular Background Component — add grid or pattern backgrounds. Scales and adapts with flowchart transformations.' - }, { - link: 'f-line-alignment-component', - text: 'Line Alignment', - description: 'Angular Line Alignment Component — show guides while dragging. Align nodes precisely in flowcharts and diagrams.' - }, { - link: 'f-minimap-component', - text: 'Minimap', - description: 'Angular Minimap Component — mini view of the whole diagram. Pan, zoom and explore complex Angular workflows.' - }]); + return defineNavigationGroup('Extends', [ + { + link: 'f-draggable-directive', + text: 'Drag and Drop', + description: + 'Angular Draggable Directive — enable drag-and-drop for nodes. Move, create and reassign elements in flowcharts.', + }, + { + link: 'f-zoom-directive', + text: 'Zoom', + description: + 'Angular Zoom Directive — zoom and pan canvas with mouse or API. Navigate large diagrams and workflows in Angular.', + }, + { + link: 'f-background-component', + text: 'Background', + description: + 'Angular Background Component — add grid or pattern backgrounds. Scales and adapts with flowchart transformations.', + }, + { + link: 'f-line-alignment-component', + text: 'Line Alignment', + description: + 'Angular Line Alignment Component — show guides while dragging. Align nodes precisely in flowcharts and diagrams.', + }, + { + link: 'f-minimap-component', + text: 'Minimap', + description: + 'Angular Minimap Component — mini view of the whole diagram. Pan, zoom and explore complex Angular workflows.', + }, + ]); } - - diff --git a/src/app/examples.config.ts b/src/app/examples.config.ts index e9901bf1..4be51ab9 100644 --- a/src/app/examples.config.ts +++ b/src/app/examples.config.ts @@ -11,10 +11,10 @@ import { provideHeaderSearch, provideLanguage, provideLogo, + provideMeta, provideNavigation, - provideTitle, provideTableOfContent, - provideMeta, + provideTitle, } from '@foblex/m-render'; export const EXAMPLES_CONFIGURATION = { @@ -40,7 +40,7 @@ export const EXAMPLES_CONFIGURATION = { ), defineLazyComponent( 'drag-handle', - () => import('../../projects/f-examples/nodes/drag-handle/drag-handle.component'), + () => import('../../projects/f-examples/nodes/drag-handle/drag-handle'), ), defineLazyComponent( 'resize-handle', @@ -111,15 +111,11 @@ export const EXAMPLES_CONFIGURATION = { ), defineLazyComponent( 'drag-to-connect', - () => - import('../../projects/f-examples/connections/drag-to-connect/drag-to-connect.component'), + () => import('../../projects/f-examples/connections/drag-to-connect/drag-to-connect'), ), defineLazyComponent( 'drag-to-reassign', - () => - import( - '../../projects/f-examples/connections/drag-to-reassign/drag-to-reassign.component' - ), + () => import('../../projects/f-examples/connections/drag-to-reassign/drag-to-reassign'), ), defineLazyComponent( 'create-node-on-connection-drop', @@ -139,7 +135,7 @@ export const EXAMPLES_CONFIGURATION = { 'assign-node-to-connection-on-drop', () => import( - '../../projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop.component' + '../../projects/f-examples/connections/assign-node-to-connection-on-drop/assign-node-to-connection-on-drop' ), ), defineLazyComponent( @@ -150,6 +146,11 @@ export const EXAMPLES_CONFIGURATION = { 'connection-types', () => import('../../projects/f-examples/connections/connection-types/connection-types'), ), + defineLazyComponent( + 'connection-waypoints', + () => + import('../../projects/f-examples/connections/connection-waypoints/connection-waypoints'), + ), defineLazyComponent( 'custom-connection-type', () => @@ -161,28 +162,14 @@ export const EXAMPLES_CONFIGURATION = { 'connection-behaviours', () => import( - '../../projects/f-examples/connections/connection-behaviours/connection-behaviours.component' + '../../projects/f-examples/connections/connection-behaviours/connection-behaviours' ), ), defineLazyComponent( 'connection-markers', - () => - import( - '../../projects/f-examples/connections/connection-markers/connection-markers.component' - ), - ), - defineLazyComponent( - 'connection-text', - () => - import('../../projects/f-examples/connections/connection-text/connection-text.component'), - ), - defineLazyComponent( - 'connection-center', - () => - import( - '../../projects/f-examples/connections/connection-center/connection-center.component' - ), + () => import('../../projects/f-examples/connections/connection-markers/connection-markers'), ), + defineLazyComponent( 'connection-content', () => import('../../projects/f-examples/connections/connection-content/connection-content'), @@ -240,13 +227,6 @@ export const EXAMPLES_CONFIGURATION = { '../../projects/f-examples/extensions/background-example/background-example.component' ), ), - defineLazyComponent( - 'vp-flow', - () => - import( - '../../projects/f-pro-examples/visual-programming/components/flow/vp-flow.component' - ), - ), defineLazyComponent( 'db-management-flow', () => @@ -452,10 +432,6 @@ function nodesGroup() { image_height: 1198, image_type: 'image/png', date: new Date('2025-08-23 16:04:08'), - badge: { - text: 'Updated', - type: 'info', - }, }, { link: 'drag-to-group', @@ -469,10 +445,6 @@ function nodesGroup() { image_height: 1208, image_type: 'image/png', date: new Date('2025-08-23 16:04:08'), - badge: { - text: 'Updated', - type: 'info', - }, }, { link: 'stress-test', @@ -565,10 +537,6 @@ function connectorGroup() { image_height: 1200, image_type: 'image/png', date: new Date('2025-09-13 15:04:46'), - badge: { - text: 'New', - type: 'success', - }, }, { link: 'connectable-side', @@ -616,10 +584,6 @@ function connectionGroup() { image_height: 600, image_type: 'image/png', date: new Date('2025-08-23 16:04:08'), - badge: { - text: 'Updated', - type: 'info', - }, }, { link: 'create-node-on-connection-drop', @@ -682,10 +646,6 @@ function connectionGroup() { image_height: 1200, image_type: 'image/png', date: new Date('2025-10-12 20:12:01'), - badge: { - text: 'Updated', - type: 'info', - }, }, { link: 'custom-connection-type', @@ -723,38 +683,6 @@ function connectionGroup() { image_type: 'image/png', date: new Date('2024-10-06 14:49:44'), }, - { - link: 'connection-text', - text: 'Connection Text', - description: - 'Add labels to connections with smart placement and scaling. Centered or offset text — Angular example.', - image: './previews/examples/connection-text.light.png', - image_dark: './previews/examples/connection-text.dark.png', - image_width: 781, - image_height: 600, - image_type: 'image/png', - date: new Date('2025-01-31 17:16:33'), - badge: { - text: 'Deprecated', - type: 'danger', - }, - }, - { - link: 'connection-center', - text: 'Connection Center', - description: - 'Place centered widgets (badges/buttons) on edges. Interaction patterns and Angular code.', - image: './previews/examples/connection-center.light.png', - image_dark: './previews/examples/connection-center.dark.png', - image_width: 781, - image_height: 600, - image_type: 'image/png', - date: new Date('2025-01-31 18:23:26'), - badge: { - text: 'Deprecated', - type: 'danger', - }, - }, { link: 'connection-content', text: 'Connection Content', @@ -772,6 +700,24 @@ function connectionGroup() { type: 'success', }, }, + { + link: 'connection-waypoints', + pageTitle: + 'Connection Waypoints in Foblex Flow – Add and Drag Waypoints for Any Connection Type', + text: 'Connection Waypoints', + description: + 'Learn how to control connection routing with Waypoints in Angular diagrams. Add waypoints from candidates, drag them to reshape connections, and bind waypoint data via [(waypoints)]. Works with straight, segment, bezier, and adaptive-curve connections.', + image: './previews/examples/connection-waypoints.light.png', + image_dark: './previews/examples/connection-waypoints.dark.png', + image_width: 1570, + image_height: 1202, + image_type: 'image/png', + date: new Date('2026-01-25 12:00:00'), + badge: { + text: 'New', + type: 'success', + }, + }, { link: 'custom-connections', text: 'Custom Connections', @@ -871,13 +817,17 @@ function extensionGroup() { link: 'zoom', text: 'Zoom', description: - 'Add zoom controls and wheel zoom with sensible limits. Smooth UX and performance tips for Angular diagrams.', + 'Zoom your canvas via mouse wheel, double click, buttons, and pinch-to-zoom (trackpad/touchscreen) with smooth limits and responsive UX.', image: './previews/examples/zoom.light.png', image_dark: './previews/examples/zoom.dark.png', image_width: 821, image_height: 600, image_type: 'image/png', - date: new Date('2024-10-06 13:57:22'), + date: new Date('2026-01-25 00:00:00'), + badge: { + text: 'Updated', + type: 'info', + }, }, { link: 'background', diff --git a/src/app/home-page/home-page-background/home-page-background.component.html b/src/app/home-page/home-page-background/home-page-background.component.html index 7d936046..6fc4a402 100644 --- a/src/app/home-page/home-page-background/home-page-background.component.html +++ b/src/app/home-page/home-page-background/home-page-background.component.html @@ -1,39 +1,61 @@ -@if(isBrowser) { - +@if (isBrowser) { + - + - - @for (connection of connections;track connection;let i = $index) { - - + + @for (connection of connections; track connection) { + + - - + + } - @for (node of nodes;track node.uid) { -
-
+ @for (node of nodes; track node.uid) { +
}
- } diff --git a/src/app/home-page/home-page-background/home-page-background.component.ts b/src/app/home-page/home-page-background/home-page-background.component.ts index 09fe6db8..a53d39c1 100644 --- a/src/app/home-page/home-page-background/home-page-background.component.ts +++ b/src/app/home-page/home-page-background/home-page-background.component.ts @@ -1,101 +1,82 @@ import { ChangeDetectionStrategy, - ChangeDetectorRef, - Component, OnDestroy, OnInit, + Component, + DestroyRef, + inject, + OnInit, + signal, + viewChild, ViewChild, } from '@angular/core'; -import { - EFConnectionBehavior, - EFMarkerType, - FFlowComponent, FFlowModule -} from '@foblex/flow'; -import { IPoint, PointExtensions, RectExtensions } from '@foblex/2d'; +import { EFConnectionBehavior, EFMarkerType, FFlowComponent, FFlowModule } from '@foblex/flow'; +import { IPoint, ITransformModel, PointExtensions, RectExtensions } from '@foblex/2d'; import { debounceTime, fromEvent, startWith, Subscription } from 'rxjs'; import { IHeroFlowNode } from './domain/i-hero-flow-node'; -import { IHeroFlowConnection } from './domain/i-hero-flow-connection'; import { HERO_FLOW_CONFIGURATION } from './domain/hero-flow.configuration'; -import { - GetNewCanvasTransformHandler -} from './domain/get-new-canvas-transform-handler/get-new-canvas-transform.handler'; -import { - GetNewCanvasTransformRequest -} from './domain/get-new-canvas-transform-handler/get-new-canvas-transform.request'; +import { GetNewCanvasTransformHandler } from './domain/get-new-canvas-transform-handler/get-new-canvas-transform.handler'; +import { GetNewCanvasTransformRequest } from './domain/get-new-canvas-transform-handler/get-new-canvas-transform.request'; import { HomePageBackgroundNodeComponent } from './home-page-background-node/home-page-background-node.component'; import { BrowserService } from '@foblex/platform'; +import { IS_BROWSER_PLATFORM } from '@foblex/m-render'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'home-page-background', templateUrl: './home-page-background.component.html', - styleUrls: [ './home-page-background.component.scss' ], + styleUrls: ['./home-page-background.component.scss'], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - FFlowModule, - HomePageBackgroundNodeComponent, - ] + imports: [FFlowModule, HomePageBackgroundNodeComponent], }) -export class HomePageBackgroundComponent implements OnInit, OnDestroy { - - private subscription$: Subscription = new Subscription(); - - public nodes: IHeroFlowNode[] = HERO_FLOW_CONFIGURATION.nodes; - - public connections: IHeroFlowConnection[] = HERO_FLOW_CONFIGURATION.connections; - - @ViewChild(FFlowComponent, { static: false }) - public fFlowComponent!: FFlowComponent; - - public canvasPosition: IPoint = PointExtensions.initialize(); +export class HomePageBackgroundComponent implements OnInit { + private readonly _browser = inject(BrowserService); + protected readonly isBrowser = inject(IS_BROWSER_PLATFORM); + private readonly _destroyRef = inject(DestroyRef); - public scale: number = 1; + private readonly _flow = viewChild(FFlowComponent); - public eMarkerType = EFMarkerType; + protected readonly nodes = HERO_FLOW_CONFIGURATION.nodes; + protected readonly connections = HERO_FLOW_CONFIGURATION.connections; + protected readonly canvasPosition = signal(PointExtensions.initialize()); + protected readonly scale = signal(1); - public eConnectionBehaviour = EFConnectionBehavior; - - public isBrowser: boolean = false; - - constructor( - private fBrowser: BrowserService, - private changeDetectorRef: ChangeDetectorRef - ) { - this.isBrowser = this.fBrowser.isBrowser(); - } + protected readonly eMarkerType = EFMarkerType; + protected readonly eConnectionBehaviour = EFConnectionBehavior; public ngOnInit(): void { - if (this.fBrowser.isBrowser()) { - this.subscription$.add(this.subscribeOnWindowResize()); + if (this.isBrowser) { + this._listenWindowResize(); } } - private subscribeOnWindowResize(): Subscription { - return fromEvent(window, 'resize').pipe(startWith(null), debounceTime(1)).subscribe(() => { - if(this.fFlowComponent) { - this.modifyPosition(); - } - }); + private _listenWindowResize(): Subscription { + return fromEvent(window, 'resize') + .pipe(startWith(null), debounceTime(1), takeUntilDestroyed(this._destroyRef)) + .subscribe(() => this._modifyPosition()); } - public onLoaded(): void { - if(this.fFlowComponent) { - this.modifyPosition(); + protected loaded(): void { + this._modifyPosition(); + } + + private _modifyPosition(): void { + if (!this._flow()) { + return; } + const { scale, position } = this._calculateNewTransform(); + this.scale.set(scale); + this.canvasPosition.set(position); } - private modifyPosition(): void { - const result = new GetNewCanvasTransformHandler(this.fBrowser).handle( - new GetNewCanvasTransformRequest(this.fFlowComponent.getNodesBoundingBox() || RectExtensions.initialize()) + private _calculateNewTransform(): ITransformModel { + return new GetNewCanvasTransformHandler(this._browser).handle( + new GetNewCanvasTransformRequest( + this._flow()?.getNodesBoundingBox() || RectExtensions.initialize(), + ), ); - this.scale = result.scale; - this.canvasPosition = result.position; - this.changeDetectorRef.markForCheck(); } - public onNodePositionChanged(point: IPoint, node: IHeroFlowNode): void { + protected changeNodePosition(point: IPoint, node: IHeroFlowNode): void { node.position = point; } - - public ngOnDestroy(): void { - this.subscription$.unsubscribe(); - } } diff --git a/src/polyfills/rect-extensions.cache.ts b/src/polyfills/rect-extensions.cache.ts deleted file mode 100644 index 458e67fb..00000000 --- a/src/polyfills/rect-extensions.cache.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { IRect, RectExtensions } from '@foblex/2d'; - -type CachedRect = { - rect: IRect; - version: number; - invalidated: boolean; -}; - -const elementCache = new WeakMap(); -const observedElements = new WeakMap(); -const windowsWithListeners = new WeakSet(); -const mutationObservers = new WeakMap(); -const resizeObservers = new WeakMap(); - -let globalVersion = 0; - -const originalFromElement = RectExtensions.fromElement.bind(RectExtensions); - -const invalidateElement = (element: Element): void => { - const cached = elementCache.get(element); - if (cached) { - cached.invalidated = true; - } -}; - -const ensureViewportListeners = (element: Element): void => { - const ownerDocument = element.ownerDocument; - const defaultView = ownerDocument?.defaultView; - - if (!defaultView || windowsWithListeners.has(defaultView)) { - return; - } - - const invalidateAll = (): void => { - globalVersion++; - }; - - defaultView.addEventListener('scroll', invalidateAll, true); - defaultView.addEventListener('resize', invalidateAll); - ownerDocument?.addEventListener?.('scroll', invalidateAll, true); - - windowsWithListeners.add(defaultView); -}; - -const ensureObservers = (element: Element): void => { - const ownerDocument = element.ownerDocument; - if (!ownerDocument || observedElements.get(element) === ownerDocument) { - return; - } - - observedElements.set(element, ownerDocument); - - if (typeof MutationObserver !== 'undefined') { - let mutationObserver = mutationObservers.get(ownerDocument); - - if (!mutationObserver) { - mutationObserver = new MutationObserver((mutations) => { - for (const mutation of mutations) { - const target = mutation.target; - if (target instanceof Element) { - invalidateElement(target); - } - } - }); - - mutationObservers.set(ownerDocument, mutationObserver); - } - - mutationObserver.observe(element, { - attributes: true, - attributeFilter: ['style', 'class'], - }); - } - - if (typeof ResizeObserver !== 'undefined') { - let resizeObserver = resizeObservers.get(ownerDocument); - - if (!resizeObserver) { - resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const target = entry.target; - if (target instanceof Element) { - invalidateElement(target); - } - } - }); - - resizeObservers.set(ownerDocument, resizeObserver); - } - - try { - resizeObserver.observe(element); - } catch { - // Some browsers throw for SVG or detached hosts; ignore to keep caching functional. - } - } -}; - -RectExtensions.fromElement = function (element: HTMLElement | SVGElement | null): IRect { - if (!element) { - return RectExtensions.initialize(); - } - - ensureViewportListeners(element); - ensureObservers(element); - - let cached = elementCache.get(element); - - if (!cached) { - cached = { - rect: RectExtensions.initialize(), - version: -1, - invalidated: true, - }; - - elementCache.set(element, cached); - } - - if (cached.invalidated || cached.version !== globalVersion) { - cached.rect = originalFromElement(element); - cached.invalidated = false; - cached.version = globalVersion; - } - - return RectExtensions.copy(cached.rect); -};