@flighthq/path 0.3.0-next.1220.6982c21 → 0.3.0-next.1248.f867198

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/path",
3
- "version": "0.3.0-next.1220.6982c21",
3
+ "version": "0.3.0-next.1248.f867198",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/flighthq/flight.git",
@@ -36,7 +36,7 @@
36
36
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
37
  },
38
38
  "dependencies": {
39
- "@flighthq/types": "0.3.0-next.1220.6982c21"
39
+ "@flighthq/types": "0.3.0-next.1248.f867198"
40
40
  },
41
41
  "devDependencies": {
42
42
  "typescript": "^5.3.0"
@@ -0,0 +1,39 @@
1
+ import { explainStrokePathTessellation } from './explainStrokePathTessellation';
2
+ import { appendPathClose, appendPathLineTo, appendPathMoveTo, appendPathRectangle, createPath } from './path';
3
+
4
+ describe('explainStrokePathTessellation', () => {
5
+ it('reports a supported simple closed ring', () => {
6
+ const path = createPath();
7
+ appendPathRectangle(path, 0, 0, 100, 50);
8
+ expect(explainStrokePathTessellation(path, { width: 10 })).toEqual({
9
+ reason: 'ok',
10
+ subpath: null,
11
+ supported: true,
12
+ });
13
+ });
14
+
15
+ it('identifies the self-intersecting subpath behind a null mesh', () => {
16
+ const path = createPath();
17
+ appendPathMoveTo(path, 0, 0);
18
+ appendPathLineTo(path, 40, 40);
19
+ appendPathLineTo(path, 0, 40);
20
+ appendPathLineTo(path, 40, 0);
21
+ appendPathClose(path);
22
+ expect(explainStrokePathTessellation(path, { width: 8 })).toEqual({
23
+ reason: 'self-intersecting-centerline',
24
+ subpath: 0,
25
+ supported: false,
26
+ });
27
+ });
28
+
29
+ it('reports invalid style input without throwing', () => {
30
+ const path = createPath();
31
+ appendPathMoveTo(path, 0, 0);
32
+ appendPathLineTo(path, 10, 0);
33
+ expect(explainStrokePathTessellation(path, { width: 0 })).toEqual({
34
+ reason: 'invalid-style',
35
+ subpath: null,
36
+ supported: false,
37
+ });
38
+ });
39
+ });
@@ -0,0 +1,27 @@
1
+ import { appendPathLineTo, appendPathMoveTo, appendPathRectangle, createPath } from './path';
2
+ import {
3
+ buildStrokePathGeometry,
4
+ StrokePathTessellationIssueNone,
5
+ StrokePathTessellationIssueSelfIntersectingCenterline,
6
+ } from './strokePathGeometry';
7
+
8
+ describe('buildStrokePathGeometry', () => {
9
+ it('builds paired closed-ring sections and reports pathological centerlines', () => {
10
+ const rectangle = createPath();
11
+ appendPathRectangle(rectangle, 0, 0, 100, 50);
12
+ const valid = buildStrokePathGeometry(rectangle, { width: 10 }, 0.25);
13
+ expect(valid.issue).toBe(StrokePathTessellationIssueNone);
14
+ expect(valid.pieces).toHaveLength(1);
15
+ expect(valid.pieces[0].closed).toBe(true);
16
+ expect(valid.pieces[0].left.length).toBe(valid.pieces[0].right.length);
17
+
18
+ const crossing = createPath();
19
+ appendPathMoveTo(crossing, 0, 0);
20
+ appendPathLineTo(crossing, 40, 40);
21
+ appendPathLineTo(crossing, 0, 40);
22
+ appendPathLineTo(crossing, 40, 0);
23
+ const invalid = buildStrokePathGeometry(crossing, { width: 10 }, 0.25);
24
+ expect(invalid.issue).toBe(StrokePathTessellationIssueSelfIntersectingCenterline);
25
+ expect(invalid.issueSubpath).toBe(0);
26
+ });
27
+ });
@@ -0,0 +1,91 @@
1
+ import { appendPathClose, appendPathLineTo, appendPathMoveTo, appendPathRectangle, createPath } from './path';
2
+ import { tessellateStrokePath } from './tessellateStrokePath';
3
+
4
+ describe('tessellateStrokePath', () => {
5
+ it('tessellates an open butt-capped line without overlapping triangles', () => {
6
+ const path = createPath();
7
+ appendPathMoveTo(path, 0, 0);
8
+ appendPathLineTo(path, 100, 0);
9
+ const mesh = tessellateStrokePath(path, { cap: 'butt', width: 10 });
10
+ expect(mesh).not.toBeNull();
11
+ expect(mesh!.indices.length).toBe(6);
12
+ expect(getMeshArea(mesh!)).toBeCloseTo(1000, 6);
13
+ });
14
+
15
+ it('tessellates a closed rectangle as a hollow ring', () => {
16
+ const path = createPath();
17
+ appendPathRectangle(path, 0, 0, 100, 50);
18
+ const mesh = tessellateStrokePath(path, { join: 'miter', width: 10 });
19
+ expect(mesh).not.toBeNull();
20
+ // Outer 110×60 minus inner 90×40 = 3000. Filling both contours would be 10200 instead.
21
+ expect(getMeshArea(mesh!)).toBeCloseTo(3000, 6);
22
+ });
23
+
24
+ it('normalizes an explicit CLOSE and a return-to-start centerline to the same ring', () => {
25
+ const closed = createPath();
26
+ appendPathRectangle(closed, 0, 0, 100, 50);
27
+ const returned = createPath();
28
+ appendPathMoveTo(returned, 0, 0);
29
+ appendPathLineTo(returned, 100, 0);
30
+ appendPathLineTo(returned, 100, 50);
31
+ appendPathLineTo(returned, 0, 50);
32
+ appendPathLineTo(returned, 0, 0);
33
+ expect(tessellateStrokePath(closed, { width: 10 })).toEqual(tessellateStrokePath(returned, { width: 10 }));
34
+ });
35
+
36
+ it('uses the same round-join sampling tolerance as stroke outlines', () => {
37
+ const path = createPath();
38
+ appendPathRectangle(path, 0, 0, 100, 50);
39
+ const fine = tessellateStrokePath(path, { join: 'round', width: 10 }, 0.1);
40
+ const coarse = tessellateStrokePath(path, { join: 'round', width: 10 }, 5);
41
+ expect(fine).not.toBeNull();
42
+ expect(coarse).not.toBeNull();
43
+ expect(fine!.vertices.length).toBeGreaterThan(coarse!.vertices.length);
44
+ });
45
+
46
+ it('turns a dashed closed path into independently capped open stroke pieces', () => {
47
+ const path = createPath();
48
+ appendPathRectangle(path, 0, 0, 100, 50);
49
+ const mesh = tessellateStrokePath(path, { cap: 'round', dash: [20, 10], width: 6 });
50
+ expect(mesh).not.toBeNull();
51
+ expect(mesh!.indices.length).toBeGreaterThan(6);
52
+ });
53
+
54
+ it('returns null for a self-intersecting centerline', () => {
55
+ const path = bowTie();
56
+ expect(tessellateStrokePath(path, { width: 8 })).toBeNull();
57
+ });
58
+
59
+ it('returns null for invalid stroke dimensions instead of emitting non-finite geometry', () => {
60
+ const path = createPath();
61
+ appendPathMoveTo(path, 0, 0);
62
+ appendPathLineTo(path, 10, 0);
63
+ expect(tessellateStrokePath(path, { width: 0 })).toBeNull();
64
+ expect(tessellateStrokePath(path, { width: Number.NaN })).toBeNull();
65
+ });
66
+ });
67
+
68
+ function bowTie() {
69
+ const path = createPath();
70
+ appendPathMoveTo(path, 0, 0);
71
+ appendPathLineTo(path, 40, 40);
72
+ appendPathLineTo(path, 0, 40);
73
+ appendPathLineTo(path, 40, 0);
74
+ appendPathClose(path);
75
+ return path;
76
+ }
77
+
78
+ function getMeshArea(mesh: { indices: readonly number[]; vertices: readonly number[] }): number {
79
+ let area = 0;
80
+ for (let i = 0; i < mesh.indices.length; i += 3) {
81
+ const a = mesh.indices[i] * 2;
82
+ const b = mesh.indices[i + 1] * 2;
83
+ const c = mesh.indices[i + 2] * 2;
84
+ area +=
85
+ Math.abs(
86
+ (mesh.vertices[b] - mesh.vertices[a]) * (mesh.vertices[c + 1] - mesh.vertices[a + 1]) -
87
+ (mesh.vertices[b + 1] - mesh.vertices[a + 1]) * (mesh.vertices[c] - mesh.vertices[a]),
88
+ ) / 2;
89
+ }
90
+ return area;
91
+ }