@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,150 @@
1
+ import { PathCommand } from '@flighthq/types';
2
+
3
+ import { appendPathClose, appendPathLineTo, appendPathMoveTo, createPath } from './path';
4
+ import { strokePath } from './strokePath';
5
+
6
+ describe('strokePath', () => {
7
+ it('returns an empty path for an empty input', () => {
8
+ const result = strokePath(createPath(), {});
9
+ expect(result.commands.length).toBe(0);
10
+ });
11
+
12
+ it('returns a closed outline for a horizontal line (butt caps)', () => {
13
+ // A horizontal line from (0,0) to (100,0) with width 10 and butt caps should produce
14
+ // a closed rectangular outline.
15
+ const path = createPath();
16
+ appendPathMoveTo(path, 0, 0);
17
+ appendPathLineTo(path, 100, 0);
18
+ const result = strokePath(path, { width: 10, cap: 'butt' });
19
+ // The result must contain at least one CLOSE
20
+ expect(result.commands).toContain(PathCommand.CLOSE);
21
+ // And at least one MOVE_TO
22
+ expect(result.commands).toContain(PathCommand.MOVE_TO);
23
+ // Width=10, so offset = 5. y values should be near ±5.
24
+ const yValues = result.data.filter((_, i) => i % 2 === 1);
25
+ expect(Math.max(...yValues)).toBeCloseTo(5);
26
+ expect(Math.min(...yValues)).toBeCloseTo(-5);
27
+ });
28
+
29
+ it('extends endpoints for square caps', () => {
30
+ const path = createPath();
31
+ appendPathMoveTo(path, 0, 0);
32
+ appendPathLineTo(path, 100, 0);
33
+ const result = strokePath(path, { width: 10, cap: 'square' });
34
+ // Square caps extend by halfWidth (5) on each end. x range should be [-5, 105].
35
+ const xValues = result.data.filter((_, i) => i % 2 === 0);
36
+ expect(Math.min(...xValues)).toBeCloseTo(-5);
37
+ expect(Math.max(...xValues)).toBeCloseTo(105);
38
+ });
39
+
40
+ it('produces round caps with extra points for a curved endcap', () => {
41
+ const path = createPath();
42
+ appendPathMoveTo(path, 0, 0);
43
+ appendPathLineTo(path, 100, 0);
44
+ const buttResult = strokePath(path, { width: 10, cap: 'butt' });
45
+ const roundResult = strokePath(path, { width: 10, cap: 'round' });
46
+ // Round caps produce more outline points than butt caps.
47
+ expect(roundResult.commands.length).toBeGreaterThan(buttResult.commands.length);
48
+ });
49
+
50
+ it('handles a closed path without adding caps', () => {
51
+ const path = createPath();
52
+ appendPathMoveTo(path, 0, 0);
53
+ appendPathLineTo(path, 100, 0);
54
+ appendPathLineTo(path, 100, 100);
55
+ appendPathClose(path);
56
+ const result = strokePath(path, { width: 4 });
57
+ // Closed path produces at least one closed contour.
58
+ expect(result.commands).toContain(PathCommand.CLOSE);
59
+ });
60
+
61
+ it('produces fewer outline points with a coarser tolerance', () => {
62
+ const path = createPath();
63
+ appendPathMoveTo(path, 0, 0);
64
+ appendPathLineTo(path, 100, 0);
65
+ const fine = strokePath(path, { width: 10, cap: 'round' }, 0.1).commands.length;
66
+ const coarse = strokePath(path, { width: 10, cap: 'round' }, 10).commands.length;
67
+ expect(coarse).toBeLessThanOrEqual(fine);
68
+ });
69
+
70
+ // Dashing edge cases.
71
+ it('dashes: produces multiple segments for a simple on/off pattern', () => {
72
+ const path = createPath();
73
+ appendPathMoveTo(path, 0, 0);
74
+ appendPathLineTo(path, 100, 0);
75
+ // dash=[10,10]: alternating 10-unit on, 10-unit off
76
+ const result = strokePath(path, { width: 2, dash: [10, 10] });
77
+ // Should produce at least 2 closed outlines (multiple MOVE_TO).
78
+ const moveTos = result.commands.filter((c) => c === PathCommand.MOVE_TO).length;
79
+ expect(moveTos).toBeGreaterThanOrEqual(2);
80
+ });
81
+
82
+ it('dashes: dashOffset shifts the pattern start position', () => {
83
+ // Path of 25 units. dash=[20, 5] = on for 20, off for 5.
84
+ // dashOffset=0: on 0-20, off 20-25 → 1 dash.
85
+ // dashOffset=15: we are 15 units into the dash at start. remaining on = 5.
86
+ // on 0-5, off 5-10, on 10-25 → 2 dashes.
87
+ const path = createPath();
88
+ appendPathMoveTo(path, 0, 0);
89
+ appendPathLineTo(path, 25, 0);
90
+ const result0 = strokePath(path, { width: 2, dash: [20, 5], dashOffset: 0 });
91
+ const result15 = strokePath(path, { width: 2, dash: [20, 5], dashOffset: 15 });
92
+ const moveTos0 = result0.commands.filter((c) => c === PathCommand.MOVE_TO).length;
93
+ const moveTos15 = result15.commands.filter((c) => c === PathCommand.MOVE_TO).length;
94
+ expect(moveTos0).toBe(1);
95
+ expect(moveTos15).toBe(2);
96
+ });
97
+
98
+ it('dashes: zero-length dash entry is ignored (no infinite loop)', () => {
99
+ const path = createPath();
100
+ appendPathMoveTo(path, 0, 0);
101
+ appendPathLineTo(path, 100, 0);
102
+ // A zero-length dash entry should not cause an infinite loop or throw.
103
+ expect(() => strokePath(path, { width: 2, dash: [0, 10, 10, 10] })).not.toThrow();
104
+ });
105
+
106
+ it('dashes: very short segments produce the correct number of dashes', () => {
107
+ const path = createPath();
108
+ appendPathMoveTo(path, 0, 0);
109
+ // Total path length = 5 (very short relative to the 10-unit dash)
110
+ appendPathLineTo(path, 5, 0);
111
+ const result = strokePath(path, { width: 1, dash: [10, 10] });
112
+ // Only one dash segment should appear since the whole path < first dash length.
113
+ const moveTos = result.commands.filter((c) => c === PathCommand.MOVE_TO).length;
114
+ expect(moveTos).toBe(1);
115
+ });
116
+
117
+ it('dashes: pattern wraps correctly across multiple path segments', () => {
118
+ // Two segments of 25 each = 50 total, with dash=[15,5]: wraps at 20/40 so has 2+ dashes.
119
+ const path = createPath();
120
+ appendPathMoveTo(path, 0, 0);
121
+ appendPathLineTo(path, 25, 0);
122
+ appendPathLineTo(path, 50, 0);
123
+ const result = strokePath(path, { width: 1, dash: [15, 5] });
124
+ const moveTos = result.commands.filter((c) => c === PathCommand.MOVE_TO).length;
125
+ // dash=[15,5]: on 0-15, off 15-20, on 20-35, off 35-40, on 40-50 → 3 dashes
126
+ expect(moveTos).toBeGreaterThanOrEqual(2);
127
+ });
128
+
129
+ it('dashes: solid stroke (empty dash array) produces a single outline', () => {
130
+ const path = createPath();
131
+ appendPathMoveTo(path, 0, 0);
132
+ appendPathLineTo(path, 100, 0);
133
+ const solid = strokePath(path, { width: 4, dash: [] });
134
+ const moveTos = solid.commands.filter((c) => c === PathCommand.MOVE_TO).length;
135
+ expect(moveTos).toBe(1);
136
+ });
137
+
138
+ it('dashes: multiple subpaths each get the dash pattern applied independently', () => {
139
+ // Two separate subpaths each of length 100; dash=[30,10] gives 3 dashes per subpath.
140
+ const path = createPath();
141
+ appendPathMoveTo(path, 0, 0);
142
+ appendPathLineTo(path, 100, 0);
143
+ appendPathMoveTo(path, 0, 20);
144
+ appendPathLineTo(path, 100, 20);
145
+ const result = strokePath(path, { width: 2, dash: [30, 10] });
146
+ const moveTos = result.commands.filter((c) => c === PathCommand.MOVE_TO).length;
147
+ // At least 2 subpaths × at least 1 dash each.
148
+ expect(moveTos).toBeGreaterThanOrEqual(2);
149
+ });
150
+ });
@@ -0,0 +1,62 @@
1
+ import type { PathMesh } from '@flighthq/types';
2
+
3
+ import { appendPathLineTo, appendPathMoveTo, createPath } from './path';
4
+ import { tessellatePath } from './tessellatePath';
5
+
6
+ function meshArea(mesh: PathMesh): number {
7
+ let area = 0;
8
+ for (let i = 0; i < mesh.indices.length; i += 3) {
9
+ const i0 = mesh.indices[i];
10
+ const i1 = mesh.indices[i + 1];
11
+ const i2 = mesh.indices[i + 2];
12
+ const ax = mesh.vertices[i0 * 2];
13
+ const ay = mesh.vertices[i0 * 2 + 1];
14
+ const bx = mesh.vertices[i1 * 2];
15
+ const by = mesh.vertices[i1 * 2 + 1];
16
+ const cx = mesh.vertices[i2 * 2];
17
+ const cy = mesh.vertices[i2 * 2 + 1];
18
+ area += Math.abs((bx - ax) * (cy - ay) - (cx - ax) * (by - ay)) / 2;
19
+ }
20
+ return area;
21
+ }
22
+
23
+ function polygon(...points: number[]): ReturnType<typeof createPath> {
24
+ const path = createPath();
25
+ appendPathMoveTo(path, points[0], points[1]);
26
+ for (let i = 2; i < points.length; i += 2) {
27
+ appendPathLineTo(path, points[i], points[i + 1]);
28
+ }
29
+ return path;
30
+ }
31
+
32
+ describe('tessellatePath', () => {
33
+ it('triangulates a triangle into one triangle', () => {
34
+ const mesh = tessellatePath(polygon(0, 0, 10, 0, 0, 10));
35
+ expect(mesh.indices).toHaveLength(3);
36
+ expect(meshArea(mesh)).toBeCloseTo(50);
37
+ });
38
+
39
+ it('triangulates a square into two triangles covering its area', () => {
40
+ const mesh = tessellatePath(polygon(0, 0, 10, 0, 10, 10, 0, 10));
41
+ expect(mesh.indices).toHaveLength(6);
42
+ expect(meshArea(mesh)).toBeCloseTo(100);
43
+ });
44
+
45
+ it('triangulates a concave L-shape conserving area (n-2 triangles)', () => {
46
+ const mesh = tessellatePath(polygon(0, 0, 20, 0, 20, 10, 10, 10, 10, 20, 0, 20));
47
+ expect(mesh.indices).toHaveLength(12); // 6 vertices → 4 triangles
48
+ expect(meshArea(mesh)).toBeCloseTo(300);
49
+ });
50
+
51
+ it('drops a trailing point that closes the contour explicitly', () => {
52
+ const mesh = tessellatePath(polygon(0, 0, 10, 0, 10, 10, 0, 10, 0, 0));
53
+ expect(mesh.vertices).toHaveLength(8); // 4 unique vertices, not 5
54
+ expect(meshArea(mesh)).toBeCloseTo(100);
55
+ });
56
+
57
+ it('produces an empty mesh for a degenerate contour', () => {
58
+ const mesh = tessellatePath(polygon(0, 0, 10, 0));
59
+ expect(mesh.indices).toHaveLength(0);
60
+ expect(mesh.vertices).toHaveLength(0);
61
+ });
62
+ });
@@ -0,0 +1,26 @@
1
+ import { appendPathRectangle, createPath } from './path';
2
+ import { tessellatePath } from './tessellatePath';
3
+ import { tessellatePathTyped } from './tessellatePathTyped';
4
+
5
+ describe('tessellatePathTyped', () => {
6
+ it('returns Float32Array vertices and Uint32Array indices', () => {
7
+ const path = createPath();
8
+ appendPathRectangle(path, 0, 0, 100, 100);
9
+ const mesh = tessellatePathTyped(path);
10
+ expect(mesh.vertices).toBeInstanceOf(Float32Array);
11
+ expect(mesh.indices).toBeInstanceOf(Uint32Array);
12
+ });
13
+ it('produces the same values as tessellatePath', () => {
14
+ const path = createPath();
15
+ appendPathRectangle(path, 10, 20, 80, 60);
16
+ const typed = tessellatePathTyped(path);
17
+ const plain = tessellatePath(path);
18
+ expect(Array.from(typed.vertices)).toStrictEqual(plain.vertices);
19
+ expect(Array.from(typed.indices)).toStrictEqual(plain.indices);
20
+ });
21
+ it('returns empty typed arrays for an empty path', () => {
22
+ const mesh = tessellatePathTyped(createPath());
23
+ expect(mesh.vertices.length).toBe(0);
24
+ expect(mesh.indices.length).toBe(0);
25
+ });
26
+ });
@@ -0,0 +1,79 @@
1
+ import { appendPathCubicCurveTo, appendPathCurveTo, appendPathLineTo, appendPathMoveTo, createPath } from './path';
2
+ import { transformPath, translatePath } from './transformPath';
3
+
4
+ const IDENTITY = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
5
+
6
+ describe('transformPath', () => {
7
+ it('identity matrix leaves all coordinates unchanged', () => {
8
+ const source = createPath();
9
+ appendPathMoveTo(source, 10, 20);
10
+ appendPathLineTo(source, 30, 40);
11
+ const out = createPath();
12
+ transformPath(source, IDENTITY, out);
13
+ expect(out.data).toStrictEqual([10, 20, 30, 40]);
14
+ });
15
+
16
+ it('applies a translation to MOVE_TO and LINE_TO points', () => {
17
+ const source = createPath();
18
+ appendPathMoveTo(source, 0, 0);
19
+ appendPathLineTo(source, 10, 0);
20
+ const out = createPath();
21
+ transformPath(source, { a: 1, b: 0, c: 0, d: 1, tx: 5, ty: 3 }, out);
22
+ expect(out.data).toStrictEqual([5, 3, 15, 3]);
23
+ });
24
+
25
+ it('applies a scale to CURVE_TO control and anchor points', () => {
26
+ const source = createPath();
27
+ appendPathMoveTo(source, 0, 0);
28
+ appendPathCurveTo(source, 10, 20, 30, 0);
29
+ const out = createPath();
30
+ transformPath(source, { a: 2, b: 0, c: 0, d: 2, tx: 0, ty: 0 }, out);
31
+ // MOVE_TO (0,0) scaled to (0,0); CURVE_TO (10,20,30,0) → (20,40,60,0)
32
+ expect(out.data).toStrictEqual([0, 0, 20, 40, 60, 0]);
33
+ });
34
+
35
+ it('applies a scale to CUBIC_CURVE_TO points', () => {
36
+ const source = createPath();
37
+ appendPathMoveTo(source, 0, 0);
38
+ appendPathCubicCurveTo(source, 10, 0, 20, 0, 30, 0);
39
+ const out = createPath();
40
+ transformPath(source, { a: 3, b: 0, c: 0, d: 3, tx: 0, ty: 0 }, out);
41
+ expect(out.data).toStrictEqual([0, 0, 30, 0, 60, 0, 90, 0]);
42
+ });
43
+
44
+ it('is alias-safe: out may be the same object as source', () => {
45
+ const path = createPath();
46
+ appendPathMoveTo(path, 5, 10);
47
+ appendPathLineTo(path, 15, 20);
48
+ transformPath(path, { a: 1, b: 0, c: 0, d: 1, tx: 100, ty: 200 }, path);
49
+ expect(path.data).toStrictEqual([105, 210, 115, 220]);
50
+ });
51
+
52
+ it('copies winding and commands from source to out', () => {
53
+ const source = createPath('evenOdd');
54
+ appendPathMoveTo(source, 0, 0);
55
+ appendPathLineTo(source, 1, 1);
56
+ const out = createPath('nonZero');
57
+ transformPath(source, IDENTITY, out);
58
+ expect(out.winding).toBe('evenOdd');
59
+ expect(out.commands).toStrictEqual(source.commands);
60
+ });
61
+ });
62
+
63
+ describe('translatePath', () => {
64
+ it('shifts all points by (dx, dy)', () => {
65
+ const source = createPath();
66
+ appendPathMoveTo(source, 0, 0);
67
+ appendPathLineTo(source, 10, 10);
68
+ const out = createPath();
69
+ translatePath(source, 5, -3, out);
70
+ expect(out.data).toStrictEqual([5, -3, 15, 7]);
71
+ });
72
+
73
+ it('is alias-safe', () => {
74
+ const path = createPath();
75
+ appendPathMoveTo(path, 1, 2);
76
+ translatePath(path, 10, 20, path);
77
+ expect(path.data).toStrictEqual([11, 22]);
78
+ });
79
+ });