@flighthq/path 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/containsPathPoint.d.ts +13 -0
  2. package/dist/containsPathPoint.d.ts.map +1 -0
  3. package/dist/containsPathPoint.js +194 -0
  4. package/dist/containsPathPoint.js.map +1 -0
  5. package/dist/copyPath.d.ts +4 -0
  6. package/dist/copyPath.d.ts.map +1 -0
  7. package/dist/copyPath.js +28 -0
  8. package/dist/copyPath.js.map +1 -0
  9. package/dist/dashPath.d.ts +3 -0
  10. package/dist/dashPath.d.ts.map +1 -0
  11. package/dist/dashPath.js +105 -0
  12. package/dist/dashPath.js.map +1 -0
  13. package/dist/fitPathCurves.d.ts +3 -0
  14. package/dist/fitPathCurves.d.ts.map +1 -0
  15. package/dist/fitPathCurves.js +222 -0
  16. package/dist/fitPathCurves.js.map +1 -0
  17. package/dist/flattenPath.d.ts +3 -0
  18. package/dist/flattenPath.d.ts.map +1 -0
  19. package/dist/flattenPath.js +145 -0
  20. package/dist/flattenPath.js.map +1 -0
  21. package/dist/forEachPathSegment.d.ts +3 -0
  22. package/dist/forEachPathSegment.d.ts.map +1 -0
  23. package/dist/forEachPathSegment.js +66 -0
  24. package/dist/forEachPathSegment.js.map +1 -0
  25. package/dist/getPathBounds.d.ts +3 -0
  26. package/dist/getPathBounds.d.ts.map +1 -0
  27. package/dist/getPathBounds.js +167 -0
  28. package/dist/getPathBounds.js.map +1 -0
  29. package/dist/getPathContourLengths.d.ts +3 -0
  30. package/dist/getPathContourLengths.d.ts.map +1 -0
  31. package/dist/getPathContourLengths.js +21 -0
  32. package/dist/getPathContourLengths.js.map +1 -0
  33. package/dist/getPathLength.d.ts +3 -0
  34. package/dist/getPathLength.d.ts.map +1 -0
  35. package/dist/getPathLength.js +23 -0
  36. package/dist/getPathLength.js.map +1 -0
  37. package/dist/getPathNearestPoint.d.ts +3 -0
  38. package/dist/getPathNearestPoint.d.ts.map +1 -0
  39. package/dist/getPathNearestPoint.js +47 -0
  40. package/dist/getPathNearestPoint.js.map +1 -0
  41. package/dist/getPathPointAtDistance.d.ts +5 -0
  42. package/dist/getPathPointAtDistance.d.ts.map +1 -0
  43. package/dist/getPathPointAtDistance.js +100 -0
  44. package/dist/getPathPointAtDistance.js.map +1 -0
  45. package/dist/getPathSegmentAtParameter.d.ts +8 -0
  46. package/dist/getPathSegmentAtParameter.d.ts.map +1 -0
  47. package/dist/getPathSegmentAtParameter.js +163 -0
  48. package/dist/getPathSegmentAtParameter.js.map +1 -0
  49. package/dist/getPathSignedArea.d.ts +4 -0
  50. package/dist/getPathSignedArea.d.ts.map +1 -0
  51. package/dist/getPathSignedArea.js +43 -0
  52. package/dist/getPathSignedArea.js.map +1 -0
  53. package/dist/index.d.ts +23 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +23 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/offsetPath.d.ts +3 -0
  58. package/dist/offsetPath.d.ts.map +1 -0
  59. package/dist/offsetPath.js +146 -0
  60. package/dist/offsetPath.js.map +1 -0
  61. package/dist/path.d.ts +17 -0
  62. package/dist/path.d.ts.map +1 -0
  63. package/dist/path.js +293 -0
  64. package/dist/path.js.map +1 -0
  65. package/dist/pathMeshPool.d.ts +6 -0
  66. package/dist/pathMeshPool.d.ts.map +1 -0
  67. package/dist/pathMeshPool.js +58 -0
  68. package/dist/pathMeshPool.js.map +1 -0
  69. package/dist/reversePath.d.ts +3 -0
  70. package/dist/reversePath.d.ts.map +1 -0
  71. package/dist/reversePath.js +95 -0
  72. package/dist/reversePath.js.map +1 -0
  73. package/dist/simplifyPath.d.ts +3 -0
  74. package/dist/simplifyPath.d.ts.map +1 -0
  75. package/dist/simplifyPath.js +78 -0
  76. package/dist/simplifyPath.js.map +1 -0
  77. package/dist/strokePath.d.ts +4 -0
  78. package/dist/strokePath.d.ts.map +1 -0
  79. package/dist/strokePath.js +458 -0
  80. package/dist/strokePath.js.map +1 -0
  81. package/dist/tessellatePath.d.ts +3 -0
  82. package/dist/tessellatePath.d.ts.map +1 -0
  83. package/dist/tessellatePath.js +107 -0
  84. package/dist/tessellatePath.js.map +1 -0
  85. package/dist/tessellatePathTyped.d.ts +3 -0
  86. package/dist/tessellatePathTyped.d.ts.map +1 -0
  87. package/dist/tessellatePathTyped.js +12 -0
  88. package/dist/tessellatePathTyped.js.map +1 -0
  89. package/dist/transformPath.d.ts +4 -0
  90. package/dist/transformPath.d.ts.map +1 -0
  91. package/dist/transformPath.js +62 -0
  92. package/dist/transformPath.js.map +1 -0
  93. package/package.json +37 -0
  94. package/src/containsPathPoint.test.ts +125 -0
  95. package/src/copyPath.test.ts +56 -0
  96. package/src/dashPath.test.ts +76 -0
  97. package/src/fitPathCurves.test.ts +65 -0
  98. package/src/flattenPath.test.ts +87 -0
  99. package/src/forEachPathSegment.test.ts +82 -0
  100. package/src/getPathBounds.test.ts +84 -0
  101. package/src/getPathContourLengths.test.ts +51 -0
  102. package/src/getPathLength.test.ts +33 -0
  103. package/src/getPathNearestPoint.test.ts +64 -0
  104. package/src/getPathPointAtDistance.test.ts +94 -0
  105. package/src/getPathSegmentAtParameter.test.ts +178 -0
  106. package/src/getPathSignedArea.test.ts +45 -0
  107. package/src/offsetPath.test.ts +78 -0
  108. package/src/path.test.ts +376 -0
  109. package/src/pathMeshPool.test.ts +78 -0
  110. package/src/reversePath.test.ts +51 -0
  111. package/src/simplifyPath.test.ts +69 -0
  112. package/src/strokePath.test.ts +150 -0
  113. package/src/tessellatePath.test.ts +62 -0
  114. package/src/tessellatePathTyped.test.ts +26 -0
  115. package/src/transformPath.test.ts +79 -0
@@ -0,0 +1,51 @@
1
+ import { getPathContourLengths } from './getPathContourLengths';
2
+ import { getPathLength } from './getPathLength';
3
+ import { appendPathClose, appendPathLineTo, appendPathMoveTo, appendPathRectangle, createPath } from './path';
4
+
5
+ describe('getPathContourLengths', () => {
6
+ it('returns an empty array for an empty path', () => {
7
+ expect(getPathContourLengths(createPath())).toStrictEqual([]);
8
+ });
9
+
10
+ it('returns a single length matching getPathLength for a single contour', () => {
11
+ const path = createPath();
12
+ appendPathMoveTo(path, 0, 0);
13
+ appendPathLineTo(path, 100, 0);
14
+ appendPathLineTo(path, 100, 50);
15
+ const lengths = getPathContourLengths(path);
16
+ expect(lengths).toHaveLength(1);
17
+ expect(lengths[0]).toBeCloseTo(getPathLength(path));
18
+ });
19
+
20
+ it('returns per-contour lengths for a multi-contour path', () => {
21
+ const path = createPath();
22
+ appendPathMoveTo(path, 0, 0);
23
+ appendPathLineTo(path, 30, 0);
24
+ appendPathMoveTo(path, 0, 0);
25
+ appendPathLineTo(path, 0, 40);
26
+ const lengths = getPathContourLengths(path);
27
+ expect(lengths).toHaveLength(2);
28
+ expect(lengths[0]).toBeCloseTo(30);
29
+ expect(lengths[1]).toBeCloseTo(40);
30
+ });
31
+
32
+ it('accounts for closed contours', () => {
33
+ const path = createPath();
34
+ appendPathRectangle(path, 0, 0, 100, 50);
35
+ const lengths = getPathContourLengths(path);
36
+ expect(lengths).toHaveLength(1);
37
+ expect(lengths[0]).toBeCloseTo(300);
38
+ });
39
+
40
+ it('sums per-contour lengths to match total getPathLength', () => {
41
+ const path = createPath();
42
+ appendPathMoveTo(path, 0, 0);
43
+ appendPathLineTo(path, 10, 0);
44
+ appendPathMoveTo(path, 0, 0);
45
+ appendPathLineTo(path, 0, 20);
46
+ appendPathClose(path);
47
+ const lengths = getPathContourLengths(path);
48
+ const sum = lengths.reduce((s, l) => s + l, 0);
49
+ expect(sum).toBeCloseTo(getPathLength(path));
50
+ });
51
+ });
@@ -0,0 +1,33 @@
1
+ import { getPathLength } from './getPathLength';
2
+ import { appendPathLineTo, appendPathMoveTo, appendPathRectangle, createPath } from './path';
3
+
4
+ describe('getPathLength', () => {
5
+ it('returns 0 for an empty path', () => {
6
+ expect(getPathLength(createPath())).toBe(0);
7
+ });
8
+
9
+ it('returns the correct length of a straight line', () => {
10
+ const path = createPath();
11
+ appendPathMoveTo(path, 0, 0);
12
+ appendPathLineTo(path, 30, 40);
13
+ // Euclidean: sqrt(30^2 + 40^2) = 50
14
+ expect(getPathLength(path)).toBeCloseTo(50);
15
+ });
16
+
17
+ it('returns the perimeter of a rectangle (excluding the closing segment)', () => {
18
+ const path = createPath();
19
+ appendPathRectangle(path, 0, 0, 100, 50);
20
+ // Perimeter of 100x50 rectangle: 2*(100+50)=300. The flattenPath closes with a segment back
21
+ // to the start, so the total is 300.
22
+ expect(getPathLength(path)).toBeCloseTo(300);
23
+ });
24
+
25
+ it('sums lengths across multiple subpaths', () => {
26
+ const path = createPath();
27
+ appendPathMoveTo(path, 0, 0);
28
+ appendPathLineTo(path, 10, 0); // length 10
29
+ appendPathMoveTo(path, 0, 0);
30
+ appendPathLineTo(path, 0, 5); // length 5
31
+ expect(getPathLength(path)).toBeCloseTo(15);
32
+ });
33
+ });
@@ -0,0 +1,64 @@
1
+ import { getPathNearestPoint } from './getPathNearestPoint';
2
+ import { appendPathCircle, appendPathLineTo, appendPathMoveTo, createPath } from './path';
3
+
4
+ describe('getPathNearestPoint', () => {
5
+ it('returns -1 for an empty path', () => {
6
+ const out = { x: 0, y: 0 };
7
+ expect(getPathNearestPoint(createPath(), 5, 5, out)).toBe(-1);
8
+ });
9
+
10
+ it('returns 0 for a point on the path', () => {
11
+ const path = createPath();
12
+ appendPathMoveTo(path, 0, 0);
13
+ appendPathLineTo(path, 100, 0);
14
+ const out = { x: 0, y: 0 };
15
+ const dist = getPathNearestPoint(path, 50, 0, out);
16
+ expect(dist).toBeCloseTo(0);
17
+ expect(out.x).toBeCloseTo(50);
18
+ expect(out.y).toBeCloseTo(0);
19
+ });
20
+
21
+ it('returns the perpendicular distance for a point off the path', () => {
22
+ const path = createPath();
23
+ appendPathMoveTo(path, 0, 0);
24
+ appendPathLineTo(path, 100, 0);
25
+ const out = { x: 0, y: 0 };
26
+ const dist = getPathNearestPoint(path, 50, 30, out);
27
+ expect(dist).toBeCloseTo(30);
28
+ expect(out.x).toBeCloseTo(50);
29
+ expect(out.y).toBeCloseTo(0);
30
+ });
31
+
32
+ it('picks the closest contour in a multi-contour path', () => {
33
+ const path = createPath();
34
+ appendPathMoveTo(path, 0, 0);
35
+ appendPathLineTo(path, 100, 0);
36
+ appendPathMoveTo(path, 0, 10);
37
+ appendPathLineTo(path, 100, 10);
38
+ const out = { x: 0, y: 0 };
39
+ const dist = getPathNearestPoint(path, 50, 8, out);
40
+ expect(dist).toBeCloseTo(2);
41
+ expect(out.y).toBeCloseTo(10);
42
+ });
43
+
44
+ it('clamps to an endpoint when the projection falls outside the segment', () => {
45
+ const path = createPath();
46
+ appendPathMoveTo(path, 10, 0);
47
+ appendPathLineTo(path, 90, 0);
48
+ const out = { x: 0, y: 0 };
49
+ const dist = getPathNearestPoint(path, 0, 0, out);
50
+ expect(dist).toBeCloseTo(10);
51
+ expect(out.x).toBeCloseTo(10);
52
+ expect(out.y).toBeCloseTo(0);
53
+ });
54
+
55
+ it('works with curved paths', () => {
56
+ const path = createPath();
57
+ appendPathCircle(path, 0, 0, 50);
58
+ const out = { x: 0, y: 0 };
59
+ const dist = getPathNearestPoint(path, 100, 0, out);
60
+ expect(dist).toBeCloseTo(50, 0);
61
+ expect(out.x).toBeCloseTo(50, 0);
62
+ expect(out.y).toBeCloseTo(0, 0);
63
+ });
64
+ });
@@ -0,0 +1,94 @@
1
+ import { getPathPointAtDistance, getPathPositionAtDistance, getPathTangentAtDistance } from './getPathPointAtDistance';
2
+ import { appendPathLineTo, appendPathMoveTo, createPath } from './path';
3
+
4
+ describe('getPathPointAtDistance', () => {
5
+ it('returns false for an empty path', () => {
6
+ const out = { x: 0, y: 0 };
7
+ expect(getPathPointAtDistance(createPath(), 0, out)).toBe(false);
8
+ });
9
+
10
+ it('returns the start point at distance 0', () => {
11
+ const path = createPath();
12
+ appendPathMoveTo(path, 5, 10);
13
+ appendPathLineTo(path, 15, 10);
14
+ const out = { x: 0, y: 0 };
15
+ getPathPointAtDistance(path, 0, out);
16
+ expect(out.x).toBeCloseTo(5);
17
+ expect(out.y).toBeCloseTo(10);
18
+ });
19
+
20
+ it('interpolates to the midpoint of a segment', () => {
21
+ const path = createPath();
22
+ appendPathMoveTo(path, 0, 0);
23
+ appendPathLineTo(path, 100, 0);
24
+ const out = { x: 0, y: 0 };
25
+ getPathPointAtDistance(path, 50, out);
26
+ expect(out.x).toBeCloseTo(50);
27
+ expect(out.y).toBeCloseTo(0);
28
+ });
29
+
30
+ it('clamps to the end when distance exceeds total length', () => {
31
+ const path = createPath();
32
+ appendPathMoveTo(path, 0, 0);
33
+ appendPathLineTo(path, 10, 0);
34
+ const out = { x: 0, y: 0 };
35
+ getPathPointAtDistance(path, 1000, out);
36
+ expect(out.x).toBeCloseTo(10);
37
+ expect(out.y).toBeCloseTo(0);
38
+ });
39
+
40
+ it('clamps to start when distance is negative', () => {
41
+ const path = createPath();
42
+ appendPathMoveTo(path, 5, 5);
43
+ appendPathLineTo(path, 15, 5);
44
+ const out = { x: 0, y: 0 };
45
+ getPathPointAtDistance(path, -10, out);
46
+ expect(out.x).toBeCloseTo(5);
47
+ expect(out.y).toBeCloseTo(5);
48
+ });
49
+ });
50
+
51
+ describe('getPathPositionAtDistance', () => {
52
+ it('returns both point and tangent simultaneously', () => {
53
+ const path = createPath();
54
+ appendPathMoveTo(path, 0, 0);
55
+ appendPathLineTo(path, 100, 0);
56
+ const pt = { x: 0, y: 0 };
57
+ const tan = { x: 0, y: 0 };
58
+ const result = getPathPositionAtDistance(path, 50, pt, tan);
59
+ expect(result).toBe(true);
60
+ expect(pt.x).toBeCloseTo(50);
61
+ expect(pt.y).toBeCloseTo(0);
62
+ expect(tan.x).toBeCloseTo(1);
63
+ expect(tan.y).toBeCloseTo(0);
64
+ });
65
+ });
66
+
67
+ describe('getPathTangentAtDistance', () => {
68
+ it('returns (1,0) as default tangent for empty path', () => {
69
+ const out = { x: 0, y: 0 };
70
+ getPathTangentAtDistance(createPath(), 0, out);
71
+ expect(out.x).toBeCloseTo(1);
72
+ expect(out.y).toBeCloseTo(0);
73
+ });
74
+
75
+ it('returns a unit tangent along a horizontal line', () => {
76
+ const path = createPath();
77
+ appendPathMoveTo(path, 0, 0);
78
+ appendPathLineTo(path, 100, 0);
79
+ const out = { x: 0, y: 0 };
80
+ getPathTangentAtDistance(path, 50, out);
81
+ expect(out.x).toBeCloseTo(1);
82
+ expect(out.y).toBeCloseTo(0);
83
+ });
84
+
85
+ it('returns a unit tangent along a vertical line', () => {
86
+ const path = createPath();
87
+ appendPathMoveTo(path, 0, 0);
88
+ appendPathLineTo(path, 0, 100);
89
+ const out = { x: 0, y: 0 };
90
+ getPathTangentAtDistance(path, 50, out);
91
+ expect(out.x).toBeCloseTo(0);
92
+ expect(out.y).toBeCloseTo(1);
93
+ });
94
+ });
@@ -0,0 +1,178 @@
1
+ import {
2
+ getCubicBezierPoint,
3
+ getCubicBezierTangent,
4
+ getPathSegmentPointAtParameter,
5
+ getPathSegmentTangentAtParameter,
6
+ getQuadraticBezierPoint,
7
+ getQuadraticBezierTangent,
8
+ } from './getPathSegmentAtParameter';
9
+ import { appendPathCubicCurveTo, appendPathCurveTo, appendPathLineTo, appendPathMoveTo, createPath } from './path';
10
+
11
+ describe('getCubicBezierPoint', () => {
12
+ it('returns the start point at t=0', () => {
13
+ const out = { x: 0, y: 0 };
14
+ getCubicBezierPoint(1, 2, 3, 4, 5, 6, 7, 8, 0, out);
15
+ expect(out.x).toBeCloseTo(1);
16
+ expect(out.y).toBeCloseTo(2);
17
+ });
18
+
19
+ it('returns the end point at t=1', () => {
20
+ const out = { x: 0, y: 0 };
21
+ getCubicBezierPoint(1, 2, 3, 4, 5, 6, 7, 8, 1, out);
22
+ expect(out.x).toBeCloseTo(7);
23
+ expect(out.y).toBeCloseTo(8);
24
+ });
25
+
26
+ it('interpolates a straight cubic at midpoint', () => {
27
+ // Straight line P0=(0,0), C1=(1,0), C2=(2,0), P1=(3,0)
28
+ const out = { x: 0, y: 0 };
29
+ getCubicBezierPoint(0, 0, 1, 0, 2, 0, 3, 0, 0.5, out);
30
+ expect(out.x).toBeCloseTo(1.5);
31
+ expect(out.y).toBeCloseTo(0);
32
+ });
33
+ });
34
+
35
+ describe('getCubicBezierTangent', () => {
36
+ it('returns the forward tangent at t=0 (C1 - P0)', () => {
37
+ const out = { x: 0, y: 0 };
38
+ getCubicBezierTangent(0, 0, 3, 0, 6, 0, 9, 0, 0, out);
39
+ // B'(0) = 3*(C1-P0) = 3*(3,0) = (9,0)
40
+ expect(out.x).toBeCloseTo(9);
41
+ expect(out.y).toBeCloseTo(0);
42
+ });
43
+
44
+ it('returns the backward tangent at t=1 (P1 - C2)', () => {
45
+ const out = { x: 0, y: 0 };
46
+ getCubicBezierTangent(0, 0, 0, 0, 6, 0, 9, 0, 1, out);
47
+ // B'(1) = 3*(P1-C2) = 3*(9-6,0) = (9,0)
48
+ expect(out.x).toBeCloseTo(9);
49
+ expect(out.y).toBeCloseTo(0);
50
+ });
51
+ });
52
+
53
+ describe('getPathSegmentPointAtParameter', () => {
54
+ it('returns false for out-of-range segment index', () => {
55
+ const path = createPath();
56
+ appendPathMoveTo(path, 0, 0);
57
+ appendPathLineTo(path, 1, 0);
58
+ const out = { x: 0, y: 0 };
59
+ expect(getPathSegmentPointAtParameter(path, 5, 0.5, out)).toBe(false);
60
+ });
61
+
62
+ it('evaluates midpoint of a line segment', () => {
63
+ const path = createPath();
64
+ appendPathMoveTo(path, 0, 0);
65
+ appendPathLineTo(path, 10, 0);
66
+ const out = { x: 0, y: 0 };
67
+ expect(getPathSegmentPointAtParameter(path, 0, 0.5, out)).toBe(true);
68
+ expect(out.x).toBeCloseTo(5);
69
+ expect(out.y).toBeCloseTo(0);
70
+ });
71
+
72
+ it('evaluates start and end of a line segment', () => {
73
+ const path = createPath();
74
+ appendPathMoveTo(path, 2, 3);
75
+ appendPathLineTo(path, 8, 9);
76
+ const start = { x: 0, y: 0 };
77
+ const end = { x: 0, y: 0 };
78
+ getPathSegmentPointAtParameter(path, 0, 0, start);
79
+ getPathSegmentPointAtParameter(path, 0, 1, end);
80
+ expect(start.x).toBeCloseTo(2);
81
+ expect(start.y).toBeCloseTo(3);
82
+ expect(end.x).toBeCloseTo(8);
83
+ expect(end.y).toBeCloseTo(9);
84
+ });
85
+
86
+ it('selects the correct segment by index in a multi-segment path', () => {
87
+ const path = createPath();
88
+ appendPathMoveTo(path, 0, 0);
89
+ appendPathLineTo(path, 10, 0); // segment 0
90
+ appendPathLineTo(path, 10, 10); // segment 1
91
+ appendPathLineTo(path, 0, 10); // segment 2
92
+ const out = { x: 0, y: 0 };
93
+ getPathSegmentPointAtParameter(path, 1, 0.5, out);
94
+ expect(out.x).toBeCloseTo(10);
95
+ expect(out.y).toBeCloseTo(5);
96
+ });
97
+
98
+ it('evaluates midpoint of a quadratic bezier', () => {
99
+ const path = createPath();
100
+ appendPathMoveTo(path, 0, 0);
101
+ appendPathCurveTo(path, 1, 2, 2, 0); // parabola-like
102
+ const out = { x: 0, y: 0 };
103
+ expect(getPathSegmentPointAtParameter(path, 0, 0.5, out)).toBe(true);
104
+ // B(0.5) = 0.25*(0,0) + 2*0.5*0.5*(1,2) + 0.25*(2,0) = (0,0)*0.25 + (0.5,1) + (0.5,0) = (1, 1)
105
+ expect(out.x).toBeCloseTo(1);
106
+ expect(out.y).toBeCloseTo(1);
107
+ });
108
+
109
+ it('evaluates midpoint of a cubic bezier', () => {
110
+ const path = createPath();
111
+ appendPathMoveTo(path, 0, 0);
112
+ appendPathCubicCurveTo(path, 0, 1, 1, 1, 1, 0); // S-curve style
113
+ const out = { x: 0, y: 0 };
114
+ expect(getPathSegmentPointAtParameter(path, 0, 0.5, out)).toBe(true);
115
+ // B(0.5) for symmetric S-curve from (0,0) ctrl (0,1),(1,1) to (1,0):
116
+ // = 0.125*(0,0) + 3*0.25*0.5*(0,1) + 3*0.5*0.25*(1,1) + 0.125*(1,0)
117
+ // = (0,0) + (0, 0.375) + (0.375, 0.375) + (0.125, 0) = (0.5, 0.75)
118
+ expect(out.x).toBeCloseTo(0.5);
119
+ expect(out.y).toBeCloseTo(0.75);
120
+ });
121
+ });
122
+
123
+ describe('getPathSegmentTangentAtParameter', () => {
124
+ it('returns false for out-of-range segment index', () => {
125
+ const path = createPath();
126
+ const out = { x: 0, y: 0 };
127
+ expect(getPathSegmentTangentAtParameter(path, 0, 0.5, out)).toBe(false);
128
+ });
129
+
130
+ it('returns the tangent direction for a horizontal line', () => {
131
+ const path = createPath();
132
+ appendPathMoveTo(path, 0, 0);
133
+ appendPathLineTo(path, 10, 0);
134
+ const out = { x: 0, y: 0 };
135
+ expect(getPathSegmentTangentAtParameter(path, 0, 0.5, out)).toBe(true);
136
+ // Direction (10,0); not normalized.
137
+ expect(out.x).toBeGreaterThan(0);
138
+ expect(out.y).toBeCloseTo(0);
139
+ });
140
+
141
+ it('returns a non-zero tangent at t=0 for a cubic bezier', () => {
142
+ const path = createPath();
143
+ appendPathMoveTo(path, 0, 0);
144
+ appendPathCubicCurveTo(path, 1, 3, 2, 3, 3, 0);
145
+ const out = { x: 0, y: 0 };
146
+ expect(getPathSegmentTangentAtParameter(path, 0, 0, out)).toBe(true);
147
+ // At t=0: B'(0) = 3*(C1-P0) = 3*(1,3) = (3,9)
148
+ expect(out.x).toBeCloseTo(3);
149
+ expect(out.y).toBeCloseTo(9);
150
+ });
151
+ });
152
+
153
+ describe('getQuadraticBezierPoint', () => {
154
+ it('returns start at t=0', () => {
155
+ const out = { x: 0, y: 0 };
156
+ getQuadraticBezierPoint(0, 1, 5, 5, 10, 1, 0, out);
157
+ expect(out.x).toBeCloseTo(0);
158
+ expect(out.y).toBeCloseTo(1);
159
+ });
160
+
161
+ it('returns end at t=1', () => {
162
+ const out = { x: 0, y: 0 };
163
+ getQuadraticBezierPoint(0, 1, 5, 5, 10, 1, 1, out);
164
+ expect(out.x).toBeCloseTo(10);
165
+ expect(out.y).toBeCloseTo(1);
166
+ });
167
+ });
168
+
169
+ describe('getQuadraticBezierTangent', () => {
170
+ it('returns the start tangent direction at t=0', () => {
171
+ const out = { x: 0, y: 0 };
172
+ // P0=(0,0), C=(4,0), P1=(8,0) — straight horizontal
173
+ getQuadraticBezierTangent(0, 0, 4, 0, 8, 0, 0, out);
174
+ // B'(0) = 2*(C-P0) = 2*(4,0) = (8,0)
175
+ expect(out.x).toBeCloseTo(8);
176
+ expect(out.y).toBeCloseTo(0);
177
+ });
178
+ });
@@ -0,0 +1,45 @@
1
+ import { getPathContourOrientation, getPathSignedArea } from './getPathSignedArea';
2
+ import { appendPathLineTo, appendPathMoveTo, appendPathRectangle, createPath } from './path';
3
+
4
+ describe('getPathContourOrientation', () => {
5
+ it('returns degenerate for an empty path', () => {
6
+ expect(getPathContourOrientation(createPath())).toBe('degenerate');
7
+ });
8
+
9
+ it('returns degenerate for a straight line (zero area)', () => {
10
+ const path = createPath();
11
+ appendPathMoveTo(path, 0, 0);
12
+ appendPathLineTo(path, 100, 0);
13
+ expect(getPathContourOrientation(path)).toBe('degenerate');
14
+ });
15
+
16
+ it('returns cw for a clockwise rectangle in screen space (y-down)', () => {
17
+ // In screen space (y-down), moveTo top-left then go right, down, left produces CW.
18
+ const path = createPath();
19
+ appendPathRectangle(path, 0, 0, 100, 100);
20
+ // appendPathRectangle goes: top-left → top-right → bottom-right → bottom-left → close
21
+ // In y-down coordinates, this is clockwise.
22
+ const orientation = getPathContourOrientation(path);
23
+ expect(orientation === 'cw' || orientation === 'ccw').toBe(true);
24
+ });
25
+ });
26
+
27
+ describe('getPathSignedArea', () => {
28
+ it('returns 0 for an empty path', () => {
29
+ expect(getPathSignedArea(createPath())).toBe(0);
30
+ });
31
+
32
+ it('returns the correct area magnitude for a rectangle', () => {
33
+ const path = createPath();
34
+ appendPathRectangle(path, 0, 0, 100, 100);
35
+ // Area magnitude should be close to 10000 (100*100).
36
+ expect(Math.abs(getPathSignedArea(path))).toBeCloseTo(10000);
37
+ });
38
+
39
+ it('returns 0 for a straight line', () => {
40
+ const path = createPath();
41
+ appendPathMoveTo(path, 0, 0);
42
+ appendPathLineTo(path, 100, 0);
43
+ expect(getPathSignedArea(path)).toBeCloseTo(0);
44
+ });
45
+ });
@@ -0,0 +1,78 @@
1
+ import type { RectangleLike } from '@flighthq/types';
2
+
3
+ import { getPathBounds } from './getPathBounds';
4
+ import { offsetPath } from './offsetPath';
5
+ import { appendPathCircle, appendPathRectangle, createPath } from './path';
6
+
7
+ describe('offsetPath', () => {
8
+ it('outsets a rectangle so bounds grow', () => {
9
+ const source = createPath();
10
+ appendPathRectangle(source, 10, 10, 80, 60);
11
+ const out = createPath();
12
+ offsetPath(source, 5, out);
13
+
14
+ const srcBounds: RectangleLike = { x: 0, y: 0, width: 0, height: 0 };
15
+ const outBounds: RectangleLike = { x: 0, y: 0, width: 0, height: 0 };
16
+ getPathBounds(source, srcBounds);
17
+ getPathBounds(out, outBounds);
18
+
19
+ expect(outBounds.width).toBeGreaterThan(srcBounds.width);
20
+ expect(outBounds.height).toBeGreaterThan(srcBounds.height);
21
+ });
22
+
23
+ it('offsets a circle outward', () => {
24
+ const source = createPath();
25
+ appendPathCircle(source, 0, 0, 50);
26
+ const out = createPath();
27
+ offsetPath(source, 10, out);
28
+
29
+ const outBounds: RectangleLike = { x: 0, y: 0, width: 0, height: 0 };
30
+ getPathBounds(out, outBounds);
31
+ expect(outBounds.width).toBeCloseTo(120, -1);
32
+ expect(outBounds.height).toBeCloseTo(120, -1);
33
+ });
34
+
35
+ it('zero offset produces the same geometry', () => {
36
+ const source = createPath();
37
+ appendPathRectangle(source, 0, 0, 100, 50);
38
+ const out = createPath();
39
+ offsetPath(source, 0, out);
40
+
41
+ const srcBounds: RectangleLike = { x: 0, y: 0, width: 0, height: 0 };
42
+ const outBounds: RectangleLike = { x: 0, y: 0, width: 0, height: 0 };
43
+ getPathBounds(source, srcBounds);
44
+ getPathBounds(out, outBounds);
45
+
46
+ expect(outBounds.x).toBeCloseTo(srcBounds.x);
47
+ expect(outBounds.y).toBeCloseTo(srcBounds.y);
48
+ expect(outBounds.width).toBeCloseTo(srcBounds.width);
49
+ expect(outBounds.height).toBeCloseTo(srcBounds.height);
50
+ });
51
+
52
+ it('negative offset insets (shrinks) the path', () => {
53
+ const source = createPath();
54
+ appendPathRectangle(source, 0, 0, 100, 100);
55
+ const out = createPath();
56
+ offsetPath(source, -10, out);
57
+
58
+ const outBounds: RectangleLike = { x: 0, y: 0, width: 0, height: 0 };
59
+ getPathBounds(out, outBounds);
60
+
61
+ expect(outBounds.width).toBeLessThan(100);
62
+ expect(outBounds.height).toBeLessThan(100);
63
+ });
64
+
65
+ it('preserves winding rule', () => {
66
+ const source = createPath('evenOdd');
67
+ appendPathRectangle(source, 0, 0, 50, 50);
68
+ const out = createPath();
69
+ offsetPath(source, 5, out);
70
+ expect(out.winding).toBe('evenOdd');
71
+ });
72
+
73
+ it('handles an empty path', () => {
74
+ const out = createPath();
75
+ offsetPath(createPath(), 10, out);
76
+ expect(out.commands).toStrictEqual([]);
77
+ });
78
+ });