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