@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.
- package/dist/containsPathPoint.d.ts +13 -0
- package/dist/containsPathPoint.d.ts.map +1 -0
- package/dist/containsPathPoint.js +194 -0
- package/dist/containsPathPoint.js.map +1 -0
- package/dist/copyPath.d.ts +4 -0
- package/dist/copyPath.d.ts.map +1 -0
- package/dist/copyPath.js +28 -0
- package/dist/copyPath.js.map +1 -0
- package/dist/dashPath.d.ts +3 -0
- package/dist/dashPath.d.ts.map +1 -0
- package/dist/dashPath.js +105 -0
- package/dist/dashPath.js.map +1 -0
- package/dist/fitPathCurves.d.ts +3 -0
- package/dist/fitPathCurves.d.ts.map +1 -0
- package/dist/fitPathCurves.js +222 -0
- package/dist/fitPathCurves.js.map +1 -0
- package/dist/flattenPath.d.ts +3 -0
- package/dist/flattenPath.d.ts.map +1 -0
- package/dist/flattenPath.js +145 -0
- package/dist/flattenPath.js.map +1 -0
- package/dist/forEachPathSegment.d.ts +3 -0
- package/dist/forEachPathSegment.d.ts.map +1 -0
- package/dist/forEachPathSegment.js +66 -0
- package/dist/forEachPathSegment.js.map +1 -0
- package/dist/getPathBounds.d.ts +3 -0
- package/dist/getPathBounds.d.ts.map +1 -0
- package/dist/getPathBounds.js +167 -0
- package/dist/getPathBounds.js.map +1 -0
- package/dist/getPathContourLengths.d.ts +3 -0
- package/dist/getPathContourLengths.d.ts.map +1 -0
- package/dist/getPathContourLengths.js +21 -0
- package/dist/getPathContourLengths.js.map +1 -0
- package/dist/getPathLength.d.ts +3 -0
- package/dist/getPathLength.d.ts.map +1 -0
- package/dist/getPathLength.js +23 -0
- package/dist/getPathLength.js.map +1 -0
- package/dist/getPathNearestPoint.d.ts +3 -0
- package/dist/getPathNearestPoint.d.ts.map +1 -0
- package/dist/getPathNearestPoint.js +47 -0
- package/dist/getPathNearestPoint.js.map +1 -0
- package/dist/getPathPointAtDistance.d.ts +5 -0
- package/dist/getPathPointAtDistance.d.ts.map +1 -0
- package/dist/getPathPointAtDistance.js +100 -0
- package/dist/getPathPointAtDistance.js.map +1 -0
- package/dist/getPathSegmentAtParameter.d.ts +8 -0
- package/dist/getPathSegmentAtParameter.d.ts.map +1 -0
- package/dist/getPathSegmentAtParameter.js +163 -0
- package/dist/getPathSegmentAtParameter.js.map +1 -0
- package/dist/getPathSignedArea.d.ts +4 -0
- package/dist/getPathSignedArea.d.ts.map +1 -0
- package/dist/getPathSignedArea.js +43 -0
- package/dist/getPathSignedArea.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/offsetPath.d.ts +3 -0
- package/dist/offsetPath.d.ts.map +1 -0
- package/dist/offsetPath.js +146 -0
- package/dist/offsetPath.js.map +1 -0
- package/dist/path.d.ts +17 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +293 -0
- package/dist/path.js.map +1 -0
- package/dist/pathMeshPool.d.ts +6 -0
- package/dist/pathMeshPool.d.ts.map +1 -0
- package/dist/pathMeshPool.js +58 -0
- package/dist/pathMeshPool.js.map +1 -0
- package/dist/reversePath.d.ts +3 -0
- package/dist/reversePath.d.ts.map +1 -0
- package/dist/reversePath.js +95 -0
- package/dist/reversePath.js.map +1 -0
- package/dist/simplifyPath.d.ts +3 -0
- package/dist/simplifyPath.d.ts.map +1 -0
- package/dist/simplifyPath.js +78 -0
- package/dist/simplifyPath.js.map +1 -0
- package/dist/strokePath.d.ts +4 -0
- package/dist/strokePath.d.ts.map +1 -0
- package/dist/strokePath.js +458 -0
- package/dist/strokePath.js.map +1 -0
- package/dist/tessellatePath.d.ts +3 -0
- package/dist/tessellatePath.d.ts.map +1 -0
- package/dist/tessellatePath.js +107 -0
- package/dist/tessellatePath.js.map +1 -0
- package/dist/tessellatePathTyped.d.ts +3 -0
- package/dist/tessellatePathTyped.d.ts.map +1 -0
- package/dist/tessellatePathTyped.js +12 -0
- package/dist/tessellatePathTyped.js.map +1 -0
- package/dist/transformPath.d.ts +4 -0
- package/dist/transformPath.d.ts.map +1 -0
- package/dist/transformPath.js +62 -0
- package/dist/transformPath.js.map +1 -0
- package/package.json +37 -0
- package/src/containsPathPoint.test.ts +125 -0
- package/src/copyPath.test.ts +56 -0
- package/src/dashPath.test.ts +76 -0
- package/src/fitPathCurves.test.ts +65 -0
- package/src/flattenPath.test.ts +87 -0
- package/src/forEachPathSegment.test.ts +82 -0
- package/src/getPathBounds.test.ts +84 -0
- package/src/getPathContourLengths.test.ts +51 -0
- package/src/getPathLength.test.ts +33 -0
- package/src/getPathNearestPoint.test.ts +64 -0
- package/src/getPathPointAtDistance.test.ts +94 -0
- package/src/getPathSegmentAtParameter.test.ts +178 -0
- package/src/getPathSignedArea.test.ts +45 -0
- package/src/offsetPath.test.ts +78 -0
- package/src/path.test.ts +376 -0
- package/src/pathMeshPool.test.ts +78 -0
- package/src/reversePath.test.ts +51 -0
- package/src/simplifyPath.test.ts +69 -0
- package/src/strokePath.test.ts +150 -0
- package/src/tessellatePath.test.ts +62 -0
- package/src/tessellatePathTyped.test.ts +26 -0
- package/src/transformPath.test.ts +79 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types';
|
|
2
|
+
import { copyPath } from './copyPath';
|
|
3
|
+
// Applies a 2D affine matrix to all anchor and control-point coordinate pairs in `source`, writing
|
|
4
|
+
// the result into `out`. `out` may be the same object as `source` (alias-safe: inputs are read into
|
|
5
|
+
// locals before writing). Commands and winding are copied unchanged.
|
|
6
|
+
//
|
|
7
|
+
// Matrix layout: [a c tx] x' = a*x + c*y + tx
|
|
8
|
+
// [b d ty] y' = b*x + d*y + ty
|
|
9
|
+
export function transformPath(source, matrix, out) {
|
|
10
|
+
const { a, b, c, d, tx, ty } = matrix;
|
|
11
|
+
// Copy commands and winding first (handles the alias case for the data array below).
|
|
12
|
+
copyPath(source, out);
|
|
13
|
+
const data = out.data;
|
|
14
|
+
const commands = source.commands;
|
|
15
|
+
let di = 0;
|
|
16
|
+
for (let ci = 0; ci < commands.length; ci++) {
|
|
17
|
+
const command = commands[ci];
|
|
18
|
+
if (command === PathCommand.MOVE_TO || command === PathCommand.LINE_TO) {
|
|
19
|
+
// 2 values: x, y
|
|
20
|
+
const x = data[di];
|
|
21
|
+
const y = data[di + 1];
|
|
22
|
+
data[di] = a * x + c * y + tx;
|
|
23
|
+
data[di + 1] = b * x + d * y + ty;
|
|
24
|
+
di += 2;
|
|
25
|
+
}
|
|
26
|
+
else if (command === PathCommand.CURVE_TO) {
|
|
27
|
+
// 4 values: controlX, controlY, anchorX, anchorY
|
|
28
|
+
for (let k = 0; k < 4; k += 2) {
|
|
29
|
+
const x = data[di + k];
|
|
30
|
+
const y = data[di + k + 1];
|
|
31
|
+
data[di + k] = a * x + c * y + tx;
|
|
32
|
+
data[di + k + 1] = b * x + d * y + ty;
|
|
33
|
+
}
|
|
34
|
+
di += 4;
|
|
35
|
+
}
|
|
36
|
+
else if (command === PathCommand.CUBIC_CURVE_TO) {
|
|
37
|
+
// 6 values: control1X, control1Y, control2X, control2Y, anchorX, anchorY
|
|
38
|
+
for (let k = 0; k < 6; k += 2) {
|
|
39
|
+
const x = data[di + k];
|
|
40
|
+
const y = data[di + k + 1];
|
|
41
|
+
data[di + k] = a * x + c * y + tx;
|
|
42
|
+
data[di + k + 1] = b * x + d * y + ty;
|
|
43
|
+
}
|
|
44
|
+
di += 6;
|
|
45
|
+
}
|
|
46
|
+
else if (command === PathCommand.WIDE_MOVE_TO || command === PathCommand.WIDE_LINE_TO) {
|
|
47
|
+
// 4 values: dummy pair + real x, y. Only the real pair at [di+2, di+3] is transformed.
|
|
48
|
+
const x = data[di + 2];
|
|
49
|
+
const y = data[di + 3];
|
|
50
|
+
data[di + 2] = a * x + c * y + tx;
|
|
51
|
+
data[di + 3] = b * x + d * y + ty;
|
|
52
|
+
di += 4;
|
|
53
|
+
}
|
|
54
|
+
// CLOSE, NO_OP, and unrecognized verbs consume no data.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Translates all points in `source` by (dx, dy), writing the result into `out`. Alias-safe.
|
|
58
|
+
// This is a convenience wrapper around `transformPath` for the common pure-translate case.
|
|
59
|
+
export function translatePath(source, dx, dy, out) {
|
|
60
|
+
transformPath(source, { a: 1, b: 0, c: 0, d: 1, tx: dx, ty: dy }, out);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=transformPath.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformPath.js","sourceRoot":"","sources":["../src/transformPath.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/path",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src/**/*.test.ts",
|
|
16
|
+
"!dist/**/*.test.js",
|
|
17
|
+
"!dist/**/*.test.d.ts",
|
|
18
|
+
"!dist/**/*.test.js.map",
|
|
19
|
+
"!dist/**/*.test.d.ts.map"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -b",
|
|
23
|
+
"clean": "tsc -b --clean",
|
|
24
|
+
"test": "vitest run --config vitest.config.ts",
|
|
25
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
26
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
27
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@flighthq/types": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.3.0"
|
|
34
|
+
},
|
|
35
|
+
"description": "Vector path geometry: curve flattening and tessellation of GraphicsPath outlines",
|
|
36
|
+
"sideEffects": false
|
|
37
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { containsPathPoint } from './containsPathPoint';
|
|
4
|
+
import { appendPathCubicCurveTo, appendPathCurveTo, appendPathLineTo, appendPathMoveTo, createPath } from './path';
|
|
5
|
+
|
|
6
|
+
describe('containsPathPoint', () => {
|
|
7
|
+
it('returns true for a point inside a convex polygon (nonZero)', () => {
|
|
8
|
+
const path = createPath('nonZero');
|
|
9
|
+
// Unit square [0,0]→[100,100]
|
|
10
|
+
appendPathMoveTo(path, 0, 0);
|
|
11
|
+
appendPathLineTo(path, 100, 0);
|
|
12
|
+
appendPathLineTo(path, 100, 100);
|
|
13
|
+
appendPathLineTo(path, 0, 100);
|
|
14
|
+
appendPathLineTo(path, 0, 0);
|
|
15
|
+
expect(containsPathPoint(path, 50, 50)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns false for a point outside a convex polygon (nonZero)', () => {
|
|
19
|
+
const path = createPath('nonZero');
|
|
20
|
+
appendPathMoveTo(path, 0, 0);
|
|
21
|
+
appendPathLineTo(path, 100, 0);
|
|
22
|
+
appendPathLineTo(path, 100, 100);
|
|
23
|
+
appendPathLineTo(path, 0, 100);
|
|
24
|
+
appendPathLineTo(path, 0, 0);
|
|
25
|
+
expect(containsPathPoint(path, 200, 200)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns true for a point inside a convex polygon (evenOdd)', () => {
|
|
29
|
+
const path = createPath('evenOdd');
|
|
30
|
+
appendPathMoveTo(path, 0, 0);
|
|
31
|
+
appendPathLineTo(path, 100, 0);
|
|
32
|
+
appendPathLineTo(path, 100, 100);
|
|
33
|
+
appendPathLineTo(path, 0, 100);
|
|
34
|
+
appendPathLineTo(path, 0, 0);
|
|
35
|
+
expect(containsPathPoint(path, 50, 50)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns false for a point in a hole (evenOdd — concave figure-8 crossing)', () => {
|
|
39
|
+
// Overlapping two CCW squares: the overlapping center is a "hole" in evenOdd.
|
|
40
|
+
const path = createPath('evenOdd');
|
|
41
|
+
appendPathMoveTo(path, 0, 0);
|
|
42
|
+
appendPathLineTo(path, 60, 0);
|
|
43
|
+
appendPathLineTo(path, 60, 60);
|
|
44
|
+
appendPathLineTo(path, 0, 60);
|
|
45
|
+
appendPathLineTo(path, 0, 0);
|
|
46
|
+
appendPathMoveTo(path, 40, 0);
|
|
47
|
+
appendPathLineTo(path, 100, 0);
|
|
48
|
+
appendPathLineTo(path, 100, 60);
|
|
49
|
+
appendPathLineTo(path, 40, 60);
|
|
50
|
+
appendPathLineTo(path, 40, 0);
|
|
51
|
+
// Point at x=50, y=30 is inside BOTH squares → evenOdd makes it a hole
|
|
52
|
+
expect(containsPathPoint(path, 50, 30)).toBe(false);
|
|
53
|
+
// Point at x=20, y=30 is inside only the first square → inside
|
|
54
|
+
expect(containsPathPoint(path, 20, 30)).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns true for a point inside the inner ring (nonZero — same winding union)', () => {
|
|
58
|
+
// nonZero with two same-direction overlapping squares → union, center is inside
|
|
59
|
+
const path = createPath('nonZero');
|
|
60
|
+
appendPathMoveTo(path, 0, 0);
|
|
61
|
+
appendPathLineTo(path, 60, 0);
|
|
62
|
+
appendPathLineTo(path, 60, 60);
|
|
63
|
+
appendPathLineTo(path, 0, 60);
|
|
64
|
+
appendPathLineTo(path, 0, 0);
|
|
65
|
+
appendPathMoveTo(path, 40, 0);
|
|
66
|
+
appendPathLineTo(path, 100, 0);
|
|
67
|
+
appendPathLineTo(path, 100, 60);
|
|
68
|
+
appendPathLineTo(path, 40, 60);
|
|
69
|
+
appendPathLineTo(path, 40, 0);
|
|
70
|
+
expect(containsPathPoint(path, 50, 30)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles a quadratic curve contour', () => {
|
|
74
|
+
// Approximate a quarter-circle using a single quadratic — the interior should register a hit.
|
|
75
|
+
const path = createPath('nonZero');
|
|
76
|
+
appendPathMoveTo(path, 100, 0);
|
|
77
|
+
appendPathCurveTo(path, 100, 100, 0, 100);
|
|
78
|
+
appendPathLineTo(path, 0, 0);
|
|
79
|
+
appendPathLineTo(path, 100, 0);
|
|
80
|
+
expect(containsPathPoint(path, 30, 30)).toBe(true);
|
|
81
|
+
expect(containsPathPoint(path, 90, 90)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('handles a cubic curve contour', () => {
|
|
85
|
+
const path = createPath('nonZero');
|
|
86
|
+
appendPathMoveTo(path, 0, 0);
|
|
87
|
+
appendPathLineTo(path, 100, 0);
|
|
88
|
+
appendPathCubicCurveTo(path, 100, 33, 100, 66, 100, 100);
|
|
89
|
+
appendPathLineTo(path, 0, 100);
|
|
90
|
+
appendPathLineTo(path, 0, 0);
|
|
91
|
+
expect(containsPathPoint(path, 50, 50)).toBe(true);
|
|
92
|
+
expect(containsPathPoint(path, 150, 50)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns false for an empty path', () => {
|
|
96
|
+
const path = createPath('nonZero');
|
|
97
|
+
expect(containsPathPoint(path, 0, 0)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('handles a WIDE_MOVE_TO command', () => {
|
|
101
|
+
const path = createPath('nonZero');
|
|
102
|
+
path.commands.push(PathCommand.WIDE_MOVE_TO);
|
|
103
|
+
path.data.push(0, 0, 0, 0); // dummy pair + real point (0,0)
|
|
104
|
+
path.commands.push(PathCommand.LINE_TO);
|
|
105
|
+
path.data.push(100, 0);
|
|
106
|
+
path.commands.push(PathCommand.LINE_TO);
|
|
107
|
+
path.data.push(100, 100);
|
|
108
|
+
path.commands.push(PathCommand.LINE_TO);
|
|
109
|
+
path.data.push(0, 100);
|
|
110
|
+
path.commands.push(PathCommand.LINE_TO);
|
|
111
|
+
path.data.push(0, 0);
|
|
112
|
+
expect(containsPathPoint(path, 50, 50)).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('handles a WIDE_LINE_TO command', () => {
|
|
116
|
+
const path = createPath('nonZero');
|
|
117
|
+
appendPathMoveTo(path, 0, 0);
|
|
118
|
+
path.commands.push(PathCommand.WIDE_LINE_TO);
|
|
119
|
+
path.data.push(0, 0, 100, 0); // dummy pair + real point (100,0)
|
|
120
|
+
appendPathLineTo(path, 100, 100);
|
|
121
|
+
appendPathLineTo(path, 0, 100);
|
|
122
|
+
appendPathLineTo(path, 0, 0);
|
|
123
|
+
expect(containsPathPoint(path, 50, 50)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { clonePath, copyPath } from './copyPath';
|
|
2
|
+
import { appendPathLineTo, appendPathMoveTo, appendPathRectangle, createPath } from './path';
|
|
3
|
+
|
|
4
|
+
describe('clonePath', () => {
|
|
5
|
+
it('returns a new path with the same commands and data', () => {
|
|
6
|
+
const source = createPath('evenOdd');
|
|
7
|
+
appendPathRectangle(source, 0, 0, 100, 50);
|
|
8
|
+
const clone = clonePath(source);
|
|
9
|
+
expect(clone).not.toBe(source);
|
|
10
|
+
expect(clone.commands).toStrictEqual(source.commands);
|
|
11
|
+
expect(clone.data).toStrictEqual(source.data);
|
|
12
|
+
expect(clone.winding).toBe('evenOdd');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('mutations to the clone do not affect the source', () => {
|
|
16
|
+
const source = createPath();
|
|
17
|
+
appendPathMoveTo(source, 0, 0);
|
|
18
|
+
const clone = clonePath(source);
|
|
19
|
+
clone.commands.push(99);
|
|
20
|
+
expect(source.commands.length).toBe(1);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('copyPath', () => {
|
|
25
|
+
it('allocates a new path when out is omitted', () => {
|
|
26
|
+
const source = createPath();
|
|
27
|
+
appendPathMoveTo(source, 5, 10);
|
|
28
|
+
const result = copyPath(source);
|
|
29
|
+
expect(result).not.toBe(source);
|
|
30
|
+
expect(result.commands).toStrictEqual(source.commands);
|
|
31
|
+
expect(result.data).toStrictEqual(source.data);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('writes into out when provided', () => {
|
|
35
|
+
const source = createPath();
|
|
36
|
+
appendPathMoveTo(source, 1, 2);
|
|
37
|
+
appendPathLineTo(source, 3, 4);
|
|
38
|
+
const out = createPath('evenOdd');
|
|
39
|
+
const result = copyPath(source, out);
|
|
40
|
+
expect(result).toBe(out);
|
|
41
|
+
expect(out.commands).toStrictEqual(source.commands);
|
|
42
|
+
expect(out.data).toStrictEqual(source.data);
|
|
43
|
+
expect(out.winding).toBe('nonZero');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('is a no-op when out is the same object as source', () => {
|
|
47
|
+
const source = createPath();
|
|
48
|
+
appendPathMoveTo(source, 7, 8);
|
|
49
|
+
const cmdsBefore = source.commands.slice();
|
|
50
|
+
const dataBefore = source.data.slice();
|
|
51
|
+
const result = copyPath(source, source);
|
|
52
|
+
expect(result).toBe(source);
|
|
53
|
+
expect(source.commands).toStrictEqual(cmdsBefore);
|
|
54
|
+
expect(source.data).toStrictEqual(dataBefore);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { dashPath } from './dashPath';
|
|
4
|
+
import { appendPathClose, appendPathLineTo, appendPathMoveTo, createPath } from './path';
|
|
5
|
+
|
|
6
|
+
describe('dashPath', () => {
|
|
7
|
+
it('produces dashed segments from a simple line', () => {
|
|
8
|
+
const source = createPath();
|
|
9
|
+
appendPathMoveTo(source, 0, 0);
|
|
10
|
+
appendPathLineTo(source, 100, 0);
|
|
11
|
+
const out = createPath();
|
|
12
|
+
dashPath(source, [10, 5], 0, out);
|
|
13
|
+
|
|
14
|
+
// Each dash-on segment starts with MOVE_TO and has LINE_TOs.
|
|
15
|
+
const moves = out.commands.filter((c) => c === PathCommand.MOVE_TO);
|
|
16
|
+
expect(moves.length).toBeGreaterThanOrEqual(6);
|
|
17
|
+
expect(out.commands).not.toContain(PathCommand.CLOSE);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('copies the source unchanged for an empty dash array', () => {
|
|
21
|
+
const source = createPath();
|
|
22
|
+
appendPathMoveTo(source, 0, 0);
|
|
23
|
+
appendPathLineTo(source, 50, 50);
|
|
24
|
+
const out = createPath();
|
|
25
|
+
dashPath(source, [], 0, out);
|
|
26
|
+
|
|
27
|
+
expect(out.commands).toStrictEqual(source.commands);
|
|
28
|
+
expect(out.data).toStrictEqual(source.data);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('handles a closed path', () => {
|
|
32
|
+
const source = createPath();
|
|
33
|
+
appendPathMoveTo(source, 0, 0);
|
|
34
|
+
appendPathLineTo(source, 100, 0);
|
|
35
|
+
appendPathLineTo(source, 100, 100);
|
|
36
|
+
appendPathClose(source);
|
|
37
|
+
const out = createPath();
|
|
38
|
+
dashPath(source, [20, 10], 0, out);
|
|
39
|
+
|
|
40
|
+
const moves = out.commands.filter((c) => c === PathCommand.MOVE_TO);
|
|
41
|
+
expect(moves.length).toBeGreaterThanOrEqual(2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('applies dash offset correctly', () => {
|
|
45
|
+
const source = createPath();
|
|
46
|
+
appendPathMoveTo(source, 0, 0);
|
|
47
|
+
appendPathLineTo(source, 100, 0);
|
|
48
|
+
const noOffset = createPath();
|
|
49
|
+
dashPath(source, [10, 10], 0, noOffset);
|
|
50
|
+
const withOffset = createPath();
|
|
51
|
+
dashPath(source, [10, 10], 5, withOffset);
|
|
52
|
+
|
|
53
|
+
// The first segment with offset=5 starts at x=0 but the on-phase is only 5 units.
|
|
54
|
+
// So the data should differ.
|
|
55
|
+
expect(withOffset.data).not.toStrictEqual(noOffset.data);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handles zero-length segments in the dash array', () => {
|
|
59
|
+
const source = createPath();
|
|
60
|
+
appendPathMoveTo(source, 0, 0);
|
|
61
|
+
appendPathLineTo(source, 50, 0);
|
|
62
|
+
const out = createPath();
|
|
63
|
+
// Zero gap means continuous — effectively all on.
|
|
64
|
+
dashPath(source, [10, 0], 0, out);
|
|
65
|
+
expect(out.commands.length).toBeGreaterThan(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('preserves winding rule', () => {
|
|
69
|
+
const source = createPath('evenOdd');
|
|
70
|
+
appendPathMoveTo(source, 0, 0);
|
|
71
|
+
appendPathLineTo(source, 100, 0);
|
|
72
|
+
const out = createPath();
|
|
73
|
+
dashPath(source, [10, 5], 0, out);
|
|
74
|
+
expect(out.winding).toBe('evenOdd');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types';
|
|
2
|
+
|
|
3
|
+
import { fitPathCurves } from './fitPathCurves';
|
|
4
|
+
import { appendPathLineTo, appendPathMoveTo, createPath } from './path';
|
|
5
|
+
|
|
6
|
+
describe('fitPathCurves', () => {
|
|
7
|
+
it('keeps a straight line as a line', () => {
|
|
8
|
+
const source = createPath();
|
|
9
|
+
appendPathMoveTo(source, 0, 0);
|
|
10
|
+
appendPathLineTo(source, 50, 0);
|
|
11
|
+
appendPathLineTo(source, 100, 0);
|
|
12
|
+
const out = createPath();
|
|
13
|
+
fitPathCurves(source, 1, out);
|
|
14
|
+
expect(out.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
15
|
+
expect(out.commands).not.toContain(PathCommand.CLOSE);
|
|
16
|
+
const lastX = out.data[out.data.length - 2];
|
|
17
|
+
const lastY = out.data[out.data.length - 1];
|
|
18
|
+
expect(lastX).toBeCloseTo(100);
|
|
19
|
+
expect(lastY).toBeCloseTo(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('produces smooth curves from a circle-like polyline', () => {
|
|
23
|
+
const source = createPath();
|
|
24
|
+
const n = 16;
|
|
25
|
+
const r = 50;
|
|
26
|
+
for (let i = 0; i < n; i++) {
|
|
27
|
+
const angle = (i / n) * Math.PI * 2;
|
|
28
|
+
const x = Math.cos(angle) * r;
|
|
29
|
+
const y = Math.sin(angle) * r;
|
|
30
|
+
if (i === 0) appendPathMoveTo(source, x, y);
|
|
31
|
+
else appendPathLineTo(source, x, y);
|
|
32
|
+
}
|
|
33
|
+
const out = createPath();
|
|
34
|
+
fitPathCurves(source, 1, out);
|
|
35
|
+
expect(out.commands).toContain(PathCommand.CUBIC_CURVE_TO);
|
|
36
|
+
const cubicCount = out.commands.filter((c) => c === PathCommand.CUBIC_CURVE_TO).length;
|
|
37
|
+
expect(cubicCount).toBeLessThan(n);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('handles a single segment', () => {
|
|
41
|
+
const source = createPath();
|
|
42
|
+
appendPathMoveTo(source, 0, 0);
|
|
43
|
+
appendPathLineTo(source, 100, 100);
|
|
44
|
+
const out = createPath();
|
|
45
|
+
fitPathCurves(source, 1, out);
|
|
46
|
+
expect(out.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
47
|
+
expect(out.data).toContain(100);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles an empty path', () => {
|
|
51
|
+
const out = createPath();
|
|
52
|
+
fitPathCurves(createPath(), 1, out);
|
|
53
|
+
expect(out.commands).toStrictEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('preserves winding rule', () => {
|
|
57
|
+
const source = createPath('evenOdd');
|
|
58
|
+
appendPathMoveTo(source, 0, 0);
|
|
59
|
+
appendPathLineTo(source, 100, 0);
|
|
60
|
+
appendPathLineTo(source, 100, 100);
|
|
61
|
+
const out = createPath();
|
|
62
|
+
fitPathCurves(source, 1, out);
|
|
63
|
+
expect(out.winding).toBe('evenOdd');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Path } from '@flighthq/types';
|
|
2
|
+
import { PathCommand } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { flattenPath } from './flattenPath';
|
|
5
|
+
import { appendPathClose, appendPathCurveTo, appendPathLineTo, appendPathMoveTo, createPath } from './path';
|
|
6
|
+
|
|
7
|
+
describe('flattenPath', () => {
|
|
8
|
+
it('flattens a straight line to its two endpoints', () => {
|
|
9
|
+
const path = createPath();
|
|
10
|
+
appendPathMoveTo(path, 0, 0);
|
|
11
|
+
appendPathLineTo(path, 100, 0);
|
|
12
|
+
expect(flattenPath(path)).toStrictEqual([[0, 0, 100, 0]]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('produces one contour per subpath', () => {
|
|
16
|
+
const path = createPath();
|
|
17
|
+
appendPathMoveTo(path, 0, 0);
|
|
18
|
+
appendPathLineTo(path, 10, 0);
|
|
19
|
+
appendPathMoveTo(path, 0, 10);
|
|
20
|
+
appendPathLineTo(path, 10, 10);
|
|
21
|
+
const contours = flattenPath(path);
|
|
22
|
+
expect(contours).toStrictEqual([
|
|
23
|
+
[0, 0, 10, 0],
|
|
24
|
+
[0, 10, 10, 10],
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('collapses a quadratic whose control lies on the chord to just the endpoint', () => {
|
|
29
|
+
const path = createPath();
|
|
30
|
+
appendPathMoveTo(path, 0, 0);
|
|
31
|
+
appendPathCurveTo(path, 50, 0, 100, 0);
|
|
32
|
+
expect(flattenPath(path)).toStrictEqual([[0, 0, 100, 0]]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('subdivides a genuinely curved quadratic, keeping the endpoints', () => {
|
|
36
|
+
const path = createPath();
|
|
37
|
+
appendPathMoveTo(path, 0, 0);
|
|
38
|
+
appendPathCurveTo(path, 50, 50, 100, 0);
|
|
39
|
+
const contour = flattenPath(path)[0];
|
|
40
|
+
expect(contour.length).toBeGreaterThan(4); // more than just start + end
|
|
41
|
+
expect(contour[0]).toBe(0);
|
|
42
|
+
expect(contour[1]).toBe(0);
|
|
43
|
+
expect(contour[contour.length - 2]).toBe(100);
|
|
44
|
+
expect(contour[contour.length - 1]).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('reads the second coordinate pair for WIDE_ verbs', () => {
|
|
48
|
+
const path: Path = {
|
|
49
|
+
commands: [PathCommand.WIDE_MOVE_TO, PathCommand.WIDE_LINE_TO],
|
|
50
|
+
data: [0, 0, 5, 6, 0, 0, 7, 8],
|
|
51
|
+
winding: 'nonZero',
|
|
52
|
+
};
|
|
53
|
+
expect(flattenPath(path)).toStrictEqual([[5, 6, 7, 8]]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('emits fewer points at a coarser tolerance', () => {
|
|
57
|
+
const path = createPath();
|
|
58
|
+
appendPathMoveTo(path, 0, 0);
|
|
59
|
+
appendPathCurveTo(path, 50, 50, 100, 0);
|
|
60
|
+
const fine = flattenPath(path, 0.1)[0].length;
|
|
61
|
+
const coarse = flattenPath(path, 50)[0].length;
|
|
62
|
+
expect(coarse).toBeLessThan(fine);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('appends the start point when a CLOSE verb is present and position differs', () => {
|
|
66
|
+
const path = createPath();
|
|
67
|
+
appendPathMoveTo(path, 0, 0);
|
|
68
|
+
appendPathLineTo(path, 100, 0);
|
|
69
|
+
appendPathLineTo(path, 100, 100);
|
|
70
|
+
appendPathClose(path);
|
|
71
|
+
const contour = flattenPath(path)[0];
|
|
72
|
+
// Should end at the start point (0,0)
|
|
73
|
+
expect(contour[contour.length - 2]).toBe(0);
|
|
74
|
+
expect(contour[contour.length - 1]).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('does not duplicate the start point when CLOSE is at the same position', () => {
|
|
78
|
+
const path = createPath();
|
|
79
|
+
appendPathMoveTo(path, 0, 0);
|
|
80
|
+
appendPathLineTo(path, 100, 0);
|
|
81
|
+
appendPathLineTo(path, 0, 0); // already at start
|
|
82
|
+
appendPathClose(path);
|
|
83
|
+
const contour = flattenPath(path)[0];
|
|
84
|
+
// Length should be 6: (0,0), (100,0), (0,0) — no duplicate close point
|
|
85
|
+
expect(contour.length).toBe(6);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { PathSegment } from '@flighthq/types';
|
|
2
|
+
import { PathCommand } from '@flighthq/types';
|
|
3
|
+
|
|
4
|
+
import { forEachPathSegment } from './forEachPathSegment';
|
|
5
|
+
import {
|
|
6
|
+
appendPathClose,
|
|
7
|
+
appendPathCubicCurveTo,
|
|
8
|
+
appendPathCurveTo,
|
|
9
|
+
appendPathLineTo,
|
|
10
|
+
appendPathMoveTo,
|
|
11
|
+
createPath,
|
|
12
|
+
} from './path';
|
|
13
|
+
|
|
14
|
+
describe('forEachPathSegment', () => {
|
|
15
|
+
it('visits nothing for an empty path', () => {
|
|
16
|
+
const segments: PathSegment[] = [];
|
|
17
|
+
forEachPathSegment(createPath(), (s) => segments.push(s));
|
|
18
|
+
expect(segments).toStrictEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it('yields moveTo and lineTo segments in order', () => {
|
|
21
|
+
const path = createPath();
|
|
22
|
+
appendPathMoveTo(path, 1, 2);
|
|
23
|
+
appendPathLineTo(path, 3, 4);
|
|
24
|
+
const segments: PathSegment[] = [];
|
|
25
|
+
forEachPathSegment(path, (s) => segments.push(s));
|
|
26
|
+
expect(segments).toStrictEqual([
|
|
27
|
+
{ kind: 'moveTo', x: 1, y: 2 },
|
|
28
|
+
{ kind: 'lineTo', x: 3, y: 4 },
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
it('yields a curveTo segment with correct coordinates', () => {
|
|
32
|
+
const path = createPath();
|
|
33
|
+
appendPathMoveTo(path, 0, 0);
|
|
34
|
+
appendPathCurveTo(path, 10, 20, 30, 0);
|
|
35
|
+
const segments: PathSegment[] = [];
|
|
36
|
+
forEachPathSegment(path, (s) => segments.push(s));
|
|
37
|
+
expect(segments[1]).toStrictEqual({ kind: 'curveTo', controlX: 10, controlY: 20, x: 30, y: 0 });
|
|
38
|
+
});
|
|
39
|
+
it('yields a cubicCurveTo segment with correct coordinates', () => {
|
|
40
|
+
const path = createPath();
|
|
41
|
+
appendPathMoveTo(path, 0, 0);
|
|
42
|
+
appendPathCubicCurveTo(path, 10, 20, 30, 40, 50, 0);
|
|
43
|
+
const segments: PathSegment[] = [];
|
|
44
|
+
forEachPathSegment(path, (s) => segments.push(s));
|
|
45
|
+
expect(segments[1]).toStrictEqual({
|
|
46
|
+
kind: 'cubicCurveTo',
|
|
47
|
+
control1X: 10,
|
|
48
|
+
control1Y: 20,
|
|
49
|
+
control2X: 30,
|
|
50
|
+
control2Y: 40,
|
|
51
|
+
x: 50,
|
|
52
|
+
y: 0,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('yields a close segment', () => {
|
|
56
|
+
const path = createPath();
|
|
57
|
+
appendPathMoveTo(path, 0, 0);
|
|
58
|
+
appendPathLineTo(path, 10, 0);
|
|
59
|
+
appendPathClose(path);
|
|
60
|
+
const segments: PathSegment[] = [];
|
|
61
|
+
forEachPathSegment(path, (s) => segments.push(s));
|
|
62
|
+
expect(segments[2]).toStrictEqual({ kind: 'close' });
|
|
63
|
+
});
|
|
64
|
+
it('normalizes WIDE_MOVE_TO to moveTo', () => {
|
|
65
|
+
const path = createPath();
|
|
66
|
+
path.commands.push(PathCommand.WIDE_MOVE_TO);
|
|
67
|
+
path.data.push(0, 0, 5, 6);
|
|
68
|
+
const segments: PathSegment[] = [];
|
|
69
|
+
forEachPathSegment(path, (s) => segments.push(s));
|
|
70
|
+
expect(segments).toStrictEqual([{ kind: 'moveTo', x: 5, y: 6 }]);
|
|
71
|
+
});
|
|
72
|
+
it('normalizes WIDE_LINE_TO to lineTo', () => {
|
|
73
|
+
const path = createPath();
|
|
74
|
+
path.commands.push(PathCommand.WIDE_MOVE_TO);
|
|
75
|
+
path.data.push(0, 0, 0, 0);
|
|
76
|
+
path.commands.push(PathCommand.WIDE_LINE_TO);
|
|
77
|
+
path.data.push(0, 0, 7, 8);
|
|
78
|
+
const segments: PathSegment[] = [];
|
|
79
|
+
forEachPathSegment(path, (s) => segments.push(s));
|
|
80
|
+
expect(segments[1]).toStrictEqual({ kind: 'lineTo', x: 7, y: 8 });
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { getPathBounds } from './getPathBounds';
|
|
2
|
+
import {
|
|
3
|
+
appendPathCubicCurveTo,
|
|
4
|
+
appendPathCurveTo,
|
|
5
|
+
appendPathLineTo,
|
|
6
|
+
appendPathMoveTo,
|
|
7
|
+
appendPathRectangle,
|
|
8
|
+
createPath,
|
|
9
|
+
} from './path';
|
|
10
|
+
|
|
11
|
+
describe('getPathBounds', () => {
|
|
12
|
+
it('returns false and a zero rect for an empty path', () => {
|
|
13
|
+
const path = createPath();
|
|
14
|
+
const out = { x: 1, y: 2, width: 3, height: 4 };
|
|
15
|
+
const result = getPathBounds(path, out);
|
|
16
|
+
expect(result).toBe(false);
|
|
17
|
+
expect(out).toStrictEqual({ x: 0, y: 0, width: 0, height: 0 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns the exact bounds of a rectangle', () => {
|
|
21
|
+
const path = createPath();
|
|
22
|
+
appendPathRectangle(path, 10, 20, 100, 50);
|
|
23
|
+
const out = { x: 0, y: 0, width: 0, height: 0 };
|
|
24
|
+
const result = getPathBounds(path, out);
|
|
25
|
+
expect(result).toBe(true);
|
|
26
|
+
expect(out.x).toBeCloseTo(10);
|
|
27
|
+
expect(out.y).toBeCloseTo(20);
|
|
28
|
+
expect(out.width).toBeCloseTo(100);
|
|
29
|
+
expect(out.height).toBeCloseTo(50);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('includes the start point of a single MOVE_TO', () => {
|
|
33
|
+
const path = createPath();
|
|
34
|
+
appendPathMoveTo(path, 5, 7);
|
|
35
|
+
const out = { x: 0, y: 0, width: 0, height: 0 };
|
|
36
|
+
getPathBounds(path, out);
|
|
37
|
+
expect(out.x).toBeCloseTo(5);
|
|
38
|
+
expect(out.y).toBeCloseTo(7);
|
|
39
|
+
expect(out.width).toBeCloseTo(0);
|
|
40
|
+
expect(out.height).toBeCloseTo(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('expands past control points for a quadratic curve', () => {
|
|
44
|
+
// A quadratic arc: start=(0,0) control=(50,100) end=(100,0)
|
|
45
|
+
// The true Y maximum is at t=0.5: B(0.5)=(25+0.5*50, 50+…) = (50, 50) — 50 not 100.
|
|
46
|
+
const path = createPath();
|
|
47
|
+
appendPathMoveTo(path, 0, 0);
|
|
48
|
+
appendPathCurveTo(path, 50, 100, 100, 0);
|
|
49
|
+
const out = { x: 0, y: 0, width: 0, height: 0 };
|
|
50
|
+
getPathBounds(path, out);
|
|
51
|
+
expect(out.x).toBeCloseTo(0);
|
|
52
|
+
expect(out.y).toBeCloseTo(0);
|
|
53
|
+
// True Y max for this curve = 50 (at t=0.5), not 100 (the control point)
|
|
54
|
+
expect(out.height).toBeCloseTo(50);
|
|
55
|
+
expect(out.width).toBeCloseTo(100);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('expands past control points for a cubic curve', () => {
|
|
59
|
+
// A symmetric cubic: start=(0,0) c1=(0,100) c2=(100,100) end=(100,0).
|
|
60
|
+
// The true Y extremum is somewhere between 0 and 100.
|
|
61
|
+
const path = createPath();
|
|
62
|
+
appendPathMoveTo(path, 0, 0);
|
|
63
|
+
appendPathCubicCurveTo(path, 0, 100, 100, 100, 100, 0);
|
|
64
|
+
const out = { x: 0, y: 0, width: 0, height: 0 };
|
|
65
|
+
getPathBounds(path, out);
|
|
66
|
+
// The maximum y should be ~75 (the bezier peak for this configuration)
|
|
67
|
+
expect(out.y).toBeCloseTo(0);
|
|
68
|
+
expect(out.height).toBeGreaterThan(70);
|
|
69
|
+
expect(out.height).toBeLessThan(100);
|
|
70
|
+
expect(out.width).toBeCloseTo(100);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles a MOVE_TO + LINE_TO spanning multiple quadrants', () => {
|
|
74
|
+
const path = createPath();
|
|
75
|
+
appendPathMoveTo(path, -50, -30);
|
|
76
|
+
appendPathLineTo(path, 80, 60);
|
|
77
|
+
const out = { x: 0, y: 0, width: 0, height: 0 };
|
|
78
|
+
getPathBounds(path, out);
|
|
79
|
+
expect(out.x).toBeCloseTo(-50);
|
|
80
|
+
expect(out.y).toBeCloseTo(-30);
|
|
81
|
+
expect(out.width).toBeCloseTo(130);
|
|
82
|
+
expect(out.height).toBeCloseTo(90);
|
|
83
|
+
});
|
|
84
|
+
});
|