@flighthq/shape 0.2.1-next.701.409c279 → 0.2.1-next.738.07b221d
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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/shapeFill.d.ts +4 -1
- package/dist/shapeFill.d.ts.map +1 -1
- package/dist/shapeFill.js +82 -66
- package/dist/shapeFill.js.map +1 -1
- package/dist/shapeStroke.d.ts +4 -0
- package/dist/shapeStroke.d.ts.map +1 -0
- package/dist/shapeStroke.js +164 -0
- package/dist/shapeStroke.js.map +1 -0
- package/package.json +6 -5
- package/src/shapeFill.test.ts +79 -1
- package/src/shapeStroke.test.ts +136 -0
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './scale9Shape';
|
|
|
2
2
|
export * from './shape';
|
|
3
3
|
export * from './shapeCommands';
|
|
4
4
|
export * from './shapeFill';
|
|
5
|
+
export * from './shapeStroke';
|
|
5
6
|
export * from './shapeHitTestBuiltins';
|
|
6
7
|
export * from './shapeHitTestRegistry';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export * from './scale9Shape';
|
|
|
2
2
|
export * from './shape';
|
|
3
3
|
export * from './shapeCommands';
|
|
4
4
|
export * from './shapeFill';
|
|
5
|
+
export * from './shapeStroke';
|
|
5
6
|
export * from './shapeHitTestBuiltins';
|
|
6
7
|
export * from './shapeHitTestRegistry';
|
|
7
8
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC"}
|
package/dist/shapeFill.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { ShapeCommandToken, ShapeFillRegion } from '@flighthq/types';
|
|
1
|
+
import type { Path, ShapeCommandToken, ShapeFillRegion } from '@flighthq/types';
|
|
2
|
+
export declare function appendShapeGeometryCommand(path: Path, name: string, commands: readonly ShapeCommandToken[], a: number): void;
|
|
3
|
+
export declare function getPathCommandOperandCount(verb: number): number;
|
|
2
4
|
export declare function getShapeFillRegions(commands: readonly ShapeCommandToken[]): ShapeFillRegion[] | null;
|
|
3
5
|
export declare function hasNonSolidShapeFill(commands: readonly ShapeCommandToken[]): boolean;
|
|
6
|
+
export declare function hasShapeFill(commands: readonly ShapeCommandToken[]): boolean;
|
|
4
7
|
//# sourceMappingURL=shapeFill.d.ts.map
|
package/dist/shapeFill.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shapeFill.d.ts","sourceRoot":"","sources":["../src/shapeFill.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"shapeFill.d.ts","sourceRoot":"","sources":["../src/shapeFill.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAe,iBAAiB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAQ7F,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,SAAS,iBAAiB,EAAE,EACtC,CAAC,EAAE,MAAM,GACR,IAAI,CAsEN;AAMD,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAc/D;AAUD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,eAAe,EAAE,GAAG,IAAI,CAuCpG;AAGD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,OAAO,CAoBpF;AAKD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,OAAO,CAQ5E"}
|
package/dist/shapeFill.js
CHANGED
|
@@ -1,4 +1,65 @@
|
|
|
1
1
|
import { PathCommand } from '@flighthq/types';
|
|
2
|
+
// Appends one shape geometry command (moveTo/lineTo/curveTo/cubicCurveTo and the drawCircle/Ellipse/
|
|
3
|
+
// Rectangle/RoundRectangle/Path primitives) onto `path`, expanding primitives into MOVE/LINE/CURVE
|
|
4
|
+
// verbs (curves kept for the renderer to flatten). Shared by the fill-region and stroke-region walkers
|
|
5
|
+
// so both expand geometry identically. Non-geometry command names are ignored (a no-op). `a` is the
|
|
6
|
+
// index of the command's first argument in `commands`.
|
|
7
|
+
export function appendShapeGeometryCommand(path, name, commands, a) {
|
|
8
|
+
switch (name) {
|
|
9
|
+
case 'moveTo':
|
|
10
|
+
pushVerb(path, PathCommand.MOVE_TO, commands[a], commands[a + 1]);
|
|
11
|
+
break;
|
|
12
|
+
case 'lineTo':
|
|
13
|
+
pushVerb(path, PathCommand.LINE_TO, commands[a], commands[a + 1]);
|
|
14
|
+
break;
|
|
15
|
+
case 'curveTo':
|
|
16
|
+
pushQuadratic(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 3]);
|
|
17
|
+
break;
|
|
18
|
+
case 'cubicCurveTo':
|
|
19
|
+
pushCubic(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 3], commands[a + 4], commands[a + 5]);
|
|
20
|
+
break;
|
|
21
|
+
case 'drawCircle':
|
|
22
|
+
appendEllipseToPath(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 2]);
|
|
23
|
+
break;
|
|
24
|
+
case 'drawEllipse': {
|
|
25
|
+
const w = commands[a + 2];
|
|
26
|
+
const h = commands[a + 3];
|
|
27
|
+
appendEllipseToPath(path, commands[a] + w / 2, commands[a + 1] + h / 2, w / 2, h / 2);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case 'drawRectangle':
|
|
31
|
+
appendRectangleToPath(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 3]);
|
|
32
|
+
break;
|
|
33
|
+
case 'drawRoundRectangle':
|
|
34
|
+
appendRoundRectangleToPath(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 3], commands[a + 4] / 2, commands[a + 5] / 2);
|
|
35
|
+
break;
|
|
36
|
+
case 'drawPath':
|
|
37
|
+
path.winding = commands[a + 2];
|
|
38
|
+
appendRawPath(path, commands[a], commands[a + 1]);
|
|
39
|
+
break;
|
|
40
|
+
default:
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// The number of `data` numbers a PathCommand verb consumes: MOVE_TO/LINE_TO = 2, CURVE_TO (quadratic)
|
|
45
|
+
// and WIDE_MOVE_TO/WIDE_LINE_TO = 4, CUBIC_CURVE_TO = 6, CLOSE/NO_OP = 0. The one place that maps a raw
|
|
46
|
+
// Path verb to its operand width, so a verb/data stream (appendRawPath) and a closure scan
|
|
47
|
+
// (shapeStroke) walk it without desynchronizing the data cursor.
|
|
48
|
+
export function getPathCommandOperandCount(verb) {
|
|
49
|
+
switch (verb) {
|
|
50
|
+
case PathCommand.MOVE_TO:
|
|
51
|
+
case PathCommand.LINE_TO:
|
|
52
|
+
return 2;
|
|
53
|
+
case PathCommand.CURVE_TO:
|
|
54
|
+
case PathCommand.WIDE_MOVE_TO:
|
|
55
|
+
case PathCommand.WIDE_LINE_TO:
|
|
56
|
+
return 4;
|
|
57
|
+
case PathCommand.CUBIC_CURVE_TO:
|
|
58
|
+
return 6;
|
|
59
|
+
default: // CLOSE, NO_OP
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
2
63
|
// Resolves a Shape's drawing-command stream into solid-fill regions for the GPU fill path: each
|
|
3
64
|
// `beginFill … endFill` (or the next fill) span becomes one `ShapeFillRegion` whose `path` carries the
|
|
4
65
|
// geometry (primitives expanded to MOVE/LINE/CURVE verbs, curves kept for the renderer to flatten).
|
|
@@ -14,9 +75,7 @@ export function getShapeFillRegions(commands) {
|
|
|
14
75
|
let path = null;
|
|
15
76
|
let color = 0;
|
|
16
77
|
let alpha = 1;
|
|
17
|
-
|
|
18
|
-
let startX = 0;
|
|
19
|
-
let startY = 0;
|
|
78
|
+
const winding = 'nonZero';
|
|
20
79
|
let i = 0;
|
|
21
80
|
while (i < commands.length) {
|
|
22
81
|
const name = commands[i];
|
|
@@ -29,7 +88,6 @@ export function getShapeFillRegions(commands) {
|
|
|
29
88
|
regions.push({ path, color, alpha });
|
|
30
89
|
color = commands[a];
|
|
31
90
|
alpha = commands[a + 1];
|
|
32
|
-
winding = 'nonZero';
|
|
33
91
|
path = { commands: [], data: [], winding };
|
|
34
92
|
break;
|
|
35
93
|
}
|
|
@@ -39,68 +97,11 @@ export function getShapeFillRegions(commands) {
|
|
|
39
97
|
path = null;
|
|
40
98
|
break;
|
|
41
99
|
}
|
|
42
|
-
case 'moveTo': {
|
|
43
|
-
if (path === null)
|
|
44
|
-
break;
|
|
45
|
-
startX = commands[a];
|
|
46
|
-
startY = commands[a + 1];
|
|
47
|
-
pushVerb(path, PathCommand.MOVE_TO, startX, startY);
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
case 'lineTo': {
|
|
51
|
-
if (path === null)
|
|
52
|
-
break;
|
|
53
|
-
pushVerb(path, PathCommand.LINE_TO, commands[a], commands[a + 1]);
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
case 'curveTo': {
|
|
57
|
-
if (path === null)
|
|
58
|
-
break;
|
|
59
|
-
pushQuadratic(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 3]);
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
case 'cubicCurveTo': {
|
|
63
|
-
if (path === null)
|
|
64
|
-
break;
|
|
65
|
-
pushCubic(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 3], commands[a + 4], commands[a + 5]);
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
case 'drawCircle': {
|
|
69
|
-
if (path === null)
|
|
70
|
-
break;
|
|
71
|
-
appendEllipseToPath(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 2]);
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
case 'drawEllipse': {
|
|
75
|
-
if (path === null)
|
|
76
|
-
break;
|
|
77
|
-
const w = commands[a + 2];
|
|
78
|
-
const h = commands[a + 3];
|
|
79
|
-
appendEllipseToPath(path, commands[a] + w / 2, commands[a + 1] + h / 2, w / 2, h / 2);
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
case 'drawRectangle': {
|
|
83
|
-
if (path === null)
|
|
84
|
-
break;
|
|
85
|
-
appendRectangleToPath(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 3]);
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
case 'drawRoundRectangle': {
|
|
89
|
-
if (path === null)
|
|
90
|
-
break;
|
|
91
|
-
appendRoundRectangleToPath(path, commands[a], commands[a + 1], commands[a + 2], commands[a + 3], commands[a + 4] / 2, commands[a + 5] / 2);
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
case 'drawPath': {
|
|
95
|
-
if (path === null)
|
|
96
|
-
break;
|
|
97
|
-
path.winding = commands[a + 2];
|
|
98
|
-
appendRawPath(path, commands[a], commands[a + 1]);
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
// Non-geometry styling commands (lineStyle, begin*Fill variants) are handled by the
|
|
102
|
-
// hasNonSolidShapeFill guard above or are no-ops for solid fills.
|
|
103
100
|
default:
|
|
101
|
+
// Geometry verbs (moveTo/lineTo/curve/primitives) append to the active path; non-geometry
|
|
102
|
+
// styling commands (lineStyle, begin*Fill variants) are handled above / by the guard.
|
|
103
|
+
if (path !== null)
|
|
104
|
+
appendShapeGeometryCommand(path, name, commands, a);
|
|
104
105
|
break;
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -128,6 +129,19 @@ export function hasNonSolidShapeFill(commands) {
|
|
|
128
129
|
}
|
|
129
130
|
return false;
|
|
130
131
|
}
|
|
132
|
+
// True if the command stream declares any fill (solid, gradient, or bitmap). Lets the GPU stroke path
|
|
133
|
+
// tell a STROKE-ONLY shape (renderable as outline meshes) from a filled-and-stroked one (which still
|
|
134
|
+
// needs the raster fallback until the GPU fill + stroke paths compose).
|
|
135
|
+
export function hasShapeFill(commands) {
|
|
136
|
+
let i = 0;
|
|
137
|
+
while (i < commands.length) {
|
|
138
|
+
const name = commands[i];
|
|
139
|
+
if (name === 'beginFill' || name === 'beginGradientFill' || name === 'beginBitmapFill')
|
|
140
|
+
return true;
|
|
141
|
+
i += 2 + commands[i + 1];
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
131
145
|
const KAPPA = 0.5522847498307936;
|
|
132
146
|
function appendEllipseToPath(path, cx, cy, rx, ry) {
|
|
133
147
|
const kx = rx * KAPPA;
|
|
@@ -143,7 +157,9 @@ function appendRawPath(path, verbs, data) {
|
|
|
143
157
|
let d = 0;
|
|
144
158
|
for (let v = 0; v < verbs.length; v++) {
|
|
145
159
|
const verb = verbs[v];
|
|
146
|
-
|
|
160
|
+
// Per-verb operand width — CLOSE/NO_OP consume 0 and WIDE_* consume 4, so a CLOSE or wide verb
|
|
161
|
+
// followed by another subpath keeps the data cursor aligned (assuming 2 for all-but-curves did not).
|
|
162
|
+
const args = getPathCommandOperandCount(verb);
|
|
147
163
|
path.commands.push(verb);
|
|
148
164
|
for (let k = 0; k < args; k++)
|
|
149
165
|
path.data.push(data[d + k]);
|
package/dist/shapeFill.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shapeFill.js","sourceRoot":"","sources":["../src/shapeFill.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,
|
|
1
|
+
{"version":3,"file":"shapeFill.js","sourceRoot":"","sources":["../src/shapeFill.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ShapeCommandToken, ShapeFillRegion } from '@flighthq/types';
|
|
2
|
+
export declare function getShapeStrokeRegions(commands: readonly ShapeCommandToken[]): ShapeFillRegion[] | null;
|
|
3
|
+
export declare function hasNonSolidShapeStroke(commands: readonly ShapeCommandToken[]): boolean;
|
|
4
|
+
//# sourceMappingURL=shapeStroke.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shapeStroke.d.ts","sourceRoot":"","sources":["../src/shapeStroke.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAQ,iBAAiB,EAAE,eAAe,EAAe,MAAM,iBAAiB,CAAC;AAuB7F,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,eAAe,EAAE,GAAG,IAAI,CA4EtG;AAID,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,GAAG,OAAO,CAQtF"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { strokePath } from '@flighthq/path';
|
|
2
|
+
import { PathCommand } from '@flighthq/types';
|
|
3
|
+
import { appendShapeGeometryCommand, getPathCommandOperandCount } from './shapeFill';
|
|
4
|
+
// Resolves a Shape's drawing-command stream into stroke OUTLINE regions for the GPU fill path: each
|
|
5
|
+
// `lineStyle` span's centerline geometry is offset into a fillable outline via `@flighthq/path`'s
|
|
6
|
+
// `strokePath` (real miter/bevel/round joins, butt/round/square caps, and dashing), emitted as a
|
|
7
|
+
// `ShapeFillRegion` so the renderer tessellates + fills it with the same flat-color mesh path solid
|
|
8
|
+
// fills use. This replaces the offscreen Canvas-2D raster-to-texture stroke fallback with
|
|
9
|
+
// GPU-tessellated, resolution-independent geometry (sharp when scaled, no per-shape offscreen cost,
|
|
10
|
+
// and no bounds-clipped miter spikes) — and is the first consumer of the otherwise-orphaned strokePath.
|
|
11
|
+
//
|
|
12
|
+
// Returns `null` when the shape uses a stroke the direct-fill outline route cannot express, so the
|
|
13
|
+
// caller falls back to the raster path: a gradient/bitmap stroke (non-solid color), or a CLOSED stroke.
|
|
14
|
+
// A closed stroke offsets into a hollow RING, and the tessellator fills each contour solid with no hole
|
|
15
|
+
// subtraction, so a ring can't be direct-filled — only an OPEN stroke's outline is a simple fillable
|
|
16
|
+
// polygon. Closure is decided geometrically on the accumulated centerline (a CLOSE verb, or a subpath
|
|
17
|
+
// whose end returns to its start — which covers self-closing rect/ellipse/circle/round-rect primitives,
|
|
18
|
+
// `appendShapePolygon`, a manual return-to-start polyline, and a `drawPath` carrying either), so it is
|
|
19
|
+
// stroke-span-aware: only geometry drawn under an active `lineStyle` reaches a centerline, so an
|
|
20
|
+
// unstroked closed primitive never forces the fallback. Solid open strokes only; ring tessellation (a
|
|
21
|
+
// stroke-strip or stencil-cover route) is a later addition.
|
|
22
|
+
export function getShapeStrokeRegions(commands) {
|
|
23
|
+
if (hasNonSolidShapeStroke(commands))
|
|
24
|
+
return null;
|
|
25
|
+
const regions = [];
|
|
26
|
+
let centerline = null; // accumulated centerline for the active stroke span
|
|
27
|
+
let style = null;
|
|
28
|
+
let color = 0;
|
|
29
|
+
let alpha = 1;
|
|
30
|
+
let penX = 0;
|
|
31
|
+
let penY = 0;
|
|
32
|
+
let deferred = false; // a stroked span closed into a ring the direct-fill route can't express
|
|
33
|
+
const flush = () => {
|
|
34
|
+
if (style === null || centerline === null || centerline.commands.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
if (isCenterlineClosed(centerline)) {
|
|
37
|
+
deferred = true;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const outline = strokePath(centerline, style);
|
|
41
|
+
if (outline.commands.length > 0)
|
|
42
|
+
regions.push({ path: outline, color, alpha });
|
|
43
|
+
};
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < commands.length) {
|
|
46
|
+
const name = commands[i];
|
|
47
|
+
const argCount = commands[i + 1];
|
|
48
|
+
const a = i + 2;
|
|
49
|
+
i = a + argCount;
|
|
50
|
+
if (name === 'lineStyle') {
|
|
51
|
+
// A new stroke style ends the previous span's outline and opens a fresh centerline.
|
|
52
|
+
flush();
|
|
53
|
+
const thickness = commands[a];
|
|
54
|
+
if (thickness > 0) {
|
|
55
|
+
color = commands[a + 1];
|
|
56
|
+
alpha = commands[a + 2];
|
|
57
|
+
const caps = commands[a + 5];
|
|
58
|
+
const joints = commands[a + 6];
|
|
59
|
+
style = {
|
|
60
|
+
width: thickness,
|
|
61
|
+
// Shape's CapsStyle 'none' is Canvas/`strokeStyle`'s 'butt'; round/square pass through.
|
|
62
|
+
cap: caps === 'none' ? 'butt' : caps,
|
|
63
|
+
join: joints,
|
|
64
|
+
miterLimit: commands[a + 7],
|
|
65
|
+
};
|
|
66
|
+
centerline = { commands: [], data: [], winding: 'nonZero' };
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
style = null;
|
|
70
|
+
centerline = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (isShapeGeometryCommand(name)) {
|
|
74
|
+
if (centerline !== null) {
|
|
75
|
+
// A span whose first geometry is a lineTo/curve (no leading moveTo) begins from the current pen
|
|
76
|
+
// position, matching Canvas 2D stroking the running path.
|
|
77
|
+
if (centerline.commands.length === 0 && name !== 'moveTo') {
|
|
78
|
+
centerline.commands.push(PathCommand.MOVE_TO);
|
|
79
|
+
centerline.data.push(penX, penY);
|
|
80
|
+
}
|
|
81
|
+
appendShapeGeometryCommand(centerline, name, commands, a);
|
|
82
|
+
}
|
|
83
|
+
// Track the pen off the polyline verbs so the next span can seed from it (primitives self-start).
|
|
84
|
+
if (name === 'moveTo' || name === 'lineTo') {
|
|
85
|
+
penX = commands[a];
|
|
86
|
+
penY = commands[a + 1];
|
|
87
|
+
}
|
|
88
|
+
else if (name === 'curveTo') {
|
|
89
|
+
penX = commands[a + 2];
|
|
90
|
+
penY = commands[a + 3];
|
|
91
|
+
}
|
|
92
|
+
else if (name === 'cubicCurveTo') {
|
|
93
|
+
penX = commands[a + 4];
|
|
94
|
+
penY = commands[a + 5];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Fill-style + other non-geometry commands do not affect the stroke centerline.
|
|
98
|
+
}
|
|
99
|
+
flush();
|
|
100
|
+
return deferred ? null : regions;
|
|
101
|
+
}
|
|
102
|
+
// True if the stream uses a stroke the GPU outline path cannot express (a gradient or bitmap stroke),
|
|
103
|
+
// so the caller must fall back to the raster path. A solid `lineStyle` is expressible.
|
|
104
|
+
export function hasNonSolidShapeStroke(commands) {
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < commands.length) {
|
|
107
|
+
const name = commands[i];
|
|
108
|
+
if (name === 'lineGradientStyle' || name === 'lineBitmapStyle')
|
|
109
|
+
return true;
|
|
110
|
+
i += 2 + commands[i + 1];
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// True if the centerline encloses a region — a CLOSE verb, or any subpath whose end returns to its
|
|
115
|
+
// start (≥2 segments so it bounds area). Such a stroke offsets into a hollow ring the direct-fill
|
|
116
|
+
// tessellator cannot express, so the caller defers the whole shape to raster. Walks the verb/data
|
|
117
|
+
// stream with getPathCommandOperandCount so CLOSE/WIDE verbs keep the data cursor aligned.
|
|
118
|
+
function isCenterlineClosed(path) {
|
|
119
|
+
const { commands, data } = path;
|
|
120
|
+
let d = 0;
|
|
121
|
+
let subStartX = 0;
|
|
122
|
+
let subStartY = 0;
|
|
123
|
+
let lastX = 0;
|
|
124
|
+
let lastY = 0;
|
|
125
|
+
let segments = 0;
|
|
126
|
+
const subpathReturnsToStart = () => segments >= 2 && Math.abs(lastX - subStartX) < CLOSE_EPSILON && Math.abs(lastY - subStartY) < CLOSE_EPSILON;
|
|
127
|
+
for (let c = 0; c < commands.length; c++) {
|
|
128
|
+
const verb = commands[c];
|
|
129
|
+
if (verb === PathCommand.CLOSE)
|
|
130
|
+
return true;
|
|
131
|
+
const n = getPathCommandOperandCount(verb);
|
|
132
|
+
if (verb === PathCommand.MOVE_TO || verb === PathCommand.WIDE_MOVE_TO) {
|
|
133
|
+
if (subpathReturnsToStart())
|
|
134
|
+
return true;
|
|
135
|
+
// A WIDE_MOVE_TO's real endpoint is its trailing (x,y) pair; MOVE_TO's is its only pair.
|
|
136
|
+
lastX = subStartX = data[d + n - 2];
|
|
137
|
+
lastY = subStartY = data[d + n - 1];
|
|
138
|
+
segments = 0;
|
|
139
|
+
}
|
|
140
|
+
else if (n >= 2) {
|
|
141
|
+
// The endpoint of LINE/CURVE/CUBIC/WIDE_LINE is its trailing (x,y) pair.
|
|
142
|
+
lastX = data[d + n - 2];
|
|
143
|
+
lastY = data[d + n - 1];
|
|
144
|
+
segments++;
|
|
145
|
+
}
|
|
146
|
+
d += n;
|
|
147
|
+
}
|
|
148
|
+
return subpathReturnsToStart();
|
|
149
|
+
}
|
|
150
|
+
function isShapeGeometryCommand(name) {
|
|
151
|
+
return (name === 'moveTo' ||
|
|
152
|
+
name === 'lineTo' ||
|
|
153
|
+
name === 'curveTo' ||
|
|
154
|
+
name === 'cubicCurveTo' ||
|
|
155
|
+
name === 'drawCircle' ||
|
|
156
|
+
name === 'drawEllipse' ||
|
|
157
|
+
name === 'drawRectangle' ||
|
|
158
|
+
name === 'drawRoundRectangle' ||
|
|
159
|
+
name === 'drawPath');
|
|
160
|
+
}
|
|
161
|
+
// Endpoint-coincidence tolerance for return-to-start closure. Authored primitives (rect/ellipse) and
|
|
162
|
+
// polygons re-emit their start coordinate exactly, so this only absorbs trivial float noise.
|
|
163
|
+
const CLOSE_EPSILON = 1e-6;
|
|
164
|
+
//# sourceMappingURL=shapeStroke.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shapeStroke.js","sourceRoot":"","sources":["../src/shapeStroke.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/shape",
|
|
3
|
-
"version": "0.2.1-next.
|
|
3
|
+
"version": "0.2.1-next.738.07b221d",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@flighthq/displayobject": "0.2.1-next.
|
|
36
|
-
"@flighthq/geometry": "0.2.1-next.
|
|
37
|
-
"@flighthq/node": "0.2.1-next.
|
|
38
|
-
"@flighthq/
|
|
35
|
+
"@flighthq/displayobject": "0.2.1-next.738.07b221d",
|
|
36
|
+
"@flighthq/geometry": "0.2.1-next.738.07b221d",
|
|
37
|
+
"@flighthq/node": "0.2.1-next.738.07b221d",
|
|
38
|
+
"@flighthq/path": "0.2.1-next.738.07b221d",
|
|
39
|
+
"@flighthq/types": "0.2.1-next.738.07b221d"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"typescript": "^5.3.0"
|
package/src/shapeFill.test.ts
CHANGED
|
@@ -11,7 +11,70 @@ import {
|
|
|
11
11
|
appendShapeMoveTo,
|
|
12
12
|
appendShapeRectangle,
|
|
13
13
|
} from './shapeCommands';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
appendShapeGeometryCommand,
|
|
16
|
+
getPathCommandOperandCount,
|
|
17
|
+
getShapeFillRegions,
|
|
18
|
+
hasNonSolidShapeFill,
|
|
19
|
+
hasShapeFill,
|
|
20
|
+
} from './shapeFill';
|
|
21
|
+
|
|
22
|
+
describe('appendShapeGeometryCommand', () => {
|
|
23
|
+
it('appends polyline verbs and expands primitives, ignoring non-geometry names', () => {
|
|
24
|
+
const path = { commands: [] as number[], data: [] as number[], winding: 'nonZero' as const };
|
|
25
|
+
appendShapeGeometryCommand(path, 'moveTo', ['moveTo', 2, 5, 6], 2);
|
|
26
|
+
appendShapeGeometryCommand(path, 'lineTo', ['lineTo', 2, 7, 8], 2);
|
|
27
|
+
expect(path.commands).toEqual([PathCommand.MOVE_TO, PathCommand.LINE_TO]);
|
|
28
|
+
expect(path.data).toEqual([5, 6, 7, 8]);
|
|
29
|
+
|
|
30
|
+
// A rectangle primitive expands into MOVE + 4 LINE verbs.
|
|
31
|
+
const rect = { commands: [] as number[], data: [] as number[], winding: 'nonZero' as const };
|
|
32
|
+
appendShapeGeometryCommand(rect, 'drawRectangle', ['drawRectangle', 4, 0, 0, 10, 10], 2);
|
|
33
|
+
expect(rect.commands).toEqual([
|
|
34
|
+
PathCommand.MOVE_TO,
|
|
35
|
+
PathCommand.LINE_TO,
|
|
36
|
+
PathCommand.LINE_TO,
|
|
37
|
+
PathCommand.LINE_TO,
|
|
38
|
+
PathCommand.LINE_TO,
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
// A styling command name is a no-op.
|
|
42
|
+
const noop = { commands: [] as number[], data: [] as number[], winding: 'nonZero' as const };
|
|
43
|
+
appendShapeGeometryCommand(noop, 'beginFill', ['beginFill', 2, 0xff0000, 1], 2);
|
|
44
|
+
expect(noop.commands).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('keeps the data cursor aligned across a raw CLOSE between drawPath subpaths', () => {
|
|
48
|
+
// A drawPath with two subpaths separated by a CLOSE verb. CLOSE consumes 0 operands: if it were
|
|
49
|
+
// parsed as 2, the second subpath's coordinates would shift.
|
|
50
|
+
const path = { commands: [] as number[], data: [] as number[], winding: 'nonZero' as const };
|
|
51
|
+
const verbs = [
|
|
52
|
+
PathCommand.MOVE_TO,
|
|
53
|
+
PathCommand.LINE_TO,
|
|
54
|
+
PathCommand.CLOSE,
|
|
55
|
+
PathCommand.MOVE_TO,
|
|
56
|
+
PathCommand.LINE_TO,
|
|
57
|
+
];
|
|
58
|
+
const raw = [0, 0, 10, 0, 20, 20, 30, 20];
|
|
59
|
+
appendShapeGeometryCommand(path, 'drawPath', ['drawPath', 3, verbs, raw, 'nonZero'], 2);
|
|
60
|
+
expect(path.commands).toEqual(verbs);
|
|
61
|
+
// The second subpath still reads (20,20)->(30,20); CLOSE contributed no data.
|
|
62
|
+
expect(path.data).toEqual([0, 0, 10, 0, 20, 20, 30, 20]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('getPathCommandOperandCount', () => {
|
|
67
|
+
it('reports operand width per verb (MOVE/LINE 2, CURVE/WIDE 4, CUBIC 6, CLOSE/NO_OP 0)', () => {
|
|
68
|
+
expect(getPathCommandOperandCount(PathCommand.MOVE_TO)).toBe(2);
|
|
69
|
+
expect(getPathCommandOperandCount(PathCommand.LINE_TO)).toBe(2);
|
|
70
|
+
expect(getPathCommandOperandCount(PathCommand.CURVE_TO)).toBe(4);
|
|
71
|
+
expect(getPathCommandOperandCount(PathCommand.WIDE_MOVE_TO)).toBe(4);
|
|
72
|
+
expect(getPathCommandOperandCount(PathCommand.WIDE_LINE_TO)).toBe(4);
|
|
73
|
+
expect(getPathCommandOperandCount(PathCommand.CUBIC_CURVE_TO)).toBe(6);
|
|
74
|
+
expect(getPathCommandOperandCount(PathCommand.CLOSE)).toBe(0);
|
|
75
|
+
expect(getPathCommandOperandCount(PathCommand.NO_OP)).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
15
78
|
|
|
16
79
|
describe('getShapeFillRegions', () => {
|
|
17
80
|
it('resolves a solid rectangle fill into one region with a closed outline', () => {
|
|
@@ -114,3 +177,18 @@ describe('hasNonSolidShapeFill', () => {
|
|
|
114
177
|
expect(hasNonSolidShapeFill(shape.data.commands)).toBe(true);
|
|
115
178
|
});
|
|
116
179
|
});
|
|
180
|
+
|
|
181
|
+
describe('hasShapeFill', () => {
|
|
182
|
+
it('is true when a fill is declared and false for a stroke-only shape', () => {
|
|
183
|
+
const filled = createShape();
|
|
184
|
+
appendShapeBeginFill(filled, 0xff0000);
|
|
185
|
+
appendShapeRectangle(filled, 0, 0, 10, 10);
|
|
186
|
+
expect(hasShapeFill(filled.data.commands)).toBe(true);
|
|
187
|
+
|
|
188
|
+
const strokeOnly = createShape();
|
|
189
|
+
appendShapeLineStyle(strokeOnly, 2, 0);
|
|
190
|
+
appendShapeMoveTo(strokeOnly, 0, 0);
|
|
191
|
+
appendShapeLineTo(strokeOnly, 10, 0);
|
|
192
|
+
expect(hasShapeFill(strokeOnly.data.commands)).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { tessellatePath } from '@flighthq/path';
|
|
2
|
+
import type { ShapeCommandToken } from '@flighthq/types';
|
|
3
|
+
import { PathCommand } from '@flighthq/types';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { createShape } from './shape';
|
|
7
|
+
import {
|
|
8
|
+
appendShapeLineStyle,
|
|
9
|
+
appendShapeLineTo,
|
|
10
|
+
appendShapeMoveTo,
|
|
11
|
+
appendShapePath,
|
|
12
|
+
appendShapePolygon,
|
|
13
|
+
appendShapeRectangle,
|
|
14
|
+
} from './shapeCommands';
|
|
15
|
+
import { getShapeStrokeRegions, hasNonSolidShapeStroke } from './shapeStroke';
|
|
16
|
+
|
|
17
|
+
// A 90° corner stroked with the given join, as one span.
|
|
18
|
+
function strokedCorner(join: 'bevel' | 'miter' | 'round', thickness = 20) {
|
|
19
|
+
const shape = createShape();
|
|
20
|
+
appendShapeLineStyle(shape, thickness, 0x112233, 0.5, false, 'normal', 'none', join, 6);
|
|
21
|
+
appendShapeMoveTo(shape, 0, 0);
|
|
22
|
+
appendShapeLineTo(shape, 100, 0);
|
|
23
|
+
appendShapeLineTo(shape, 100, 100);
|
|
24
|
+
return getShapeStrokeRegions(shape.data.commands);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('getShapeStrokeRegions', () => {
|
|
28
|
+
it('offsets a solid stroke span into one fillable outline region carrying its color + alpha', () => {
|
|
29
|
+
const regions = strokedCorner('miter');
|
|
30
|
+
expect(regions).not.toBeNull();
|
|
31
|
+
expect(regions!.length).toBe(1);
|
|
32
|
+
expect(regions![0].color).toBe(0x112233);
|
|
33
|
+
expect(regions![0].alpha).toBe(0.5);
|
|
34
|
+
// A real outline: closed contour(s) with MOVE + fill geometry, not empty.
|
|
35
|
+
expect(regions![0].path.commands.length).toBeGreaterThan(0);
|
|
36
|
+
expect(regions![0].path.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('differentiates join styles (miter vs bevel vs round produce distinct outlines)', () => {
|
|
40
|
+
const miter = strokedCorner('miter')![0].path.data.length;
|
|
41
|
+
const bevel = strokedCorner('bevel')![0].path.data.length;
|
|
42
|
+
const round = strokedCorner('round')![0].path.data.length;
|
|
43
|
+
// The corner geometry differs per join: bevel cuts the corner, round arcs it (more points), miter
|
|
44
|
+
// extends it. If joins were ignored (the old raster-clip bug) these would be identical.
|
|
45
|
+
expect(miter).not.toBe(bevel);
|
|
46
|
+
expect(round).toBeGreaterThan(bevel);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('emits no region for a cleared stroke (lineStyle thickness 0) and for a stroke-free shape', () => {
|
|
50
|
+
const cleared = createShape();
|
|
51
|
+
appendShapeLineStyle(cleared, 0);
|
|
52
|
+
appendShapeMoveTo(cleared, 0, 0);
|
|
53
|
+
appendShapeLineTo(cleared, 50, 0);
|
|
54
|
+
expect(getShapeStrokeRegions(cleared.data.commands)).toEqual([]);
|
|
55
|
+
|
|
56
|
+
const strokeFree = createShape();
|
|
57
|
+
appendShapeMoveTo(strokeFree, 0, 0);
|
|
58
|
+
appendShapeLineTo(strokeFree, 50, 0);
|
|
59
|
+
expect(getShapeStrokeRegions(strokeFree.data.commands)).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('closes each span and re-strokes on a new lineStyle (two spans → two regions)', () => {
|
|
63
|
+
const shape = createShape();
|
|
64
|
+
appendShapeLineStyle(shape, 10, 0xff0000, 1, false, 'normal', 'none', 'miter', 6);
|
|
65
|
+
appendShapeMoveTo(shape, 0, 0);
|
|
66
|
+
appendShapeLineTo(shape, 50, 0);
|
|
67
|
+
appendShapeLineStyle(shape, 4, 0x00ff00, 1, false, 'normal', 'round', 'round', 6);
|
|
68
|
+
appendShapeMoveTo(shape, 0, 20);
|
|
69
|
+
appendShapeLineTo(shape, 50, 20);
|
|
70
|
+
const regions = getShapeStrokeRegions(shape.data.commands);
|
|
71
|
+
expect(regions!.length).toBe(2);
|
|
72
|
+
expect(regions![0].color).toBe(0xff0000);
|
|
73
|
+
expect(regions![1].color).toBe(0x00ff00);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('tessellates an OPEN stroke outline into a fillable (non-empty) mesh', () => {
|
|
77
|
+
const outline = strokedCorner('miter')![0].path;
|
|
78
|
+
// The V's outline is a simple polygon: ear-clipping fills it (indices > 0). A closed stroke's ring
|
|
79
|
+
// could not be direct-filled, which is why closed strokes defer (next test).
|
|
80
|
+
const mesh = tessellatePath(outline);
|
|
81
|
+
expect(mesh.indices.length).toBeGreaterThan(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('defers a CLOSED stroke (rectangle) to the raster path — a ring is not direct-fillable (null)', () => {
|
|
85
|
+
const shape = createShape();
|
|
86
|
+
appendShapeLineStyle(shape, 8, 0xff0000, 1, false, 'normal', 'none', 'miter', 4);
|
|
87
|
+
appendShapeRectangle(shape, 10, 10, 100, 50);
|
|
88
|
+
expect(getShapeStrokeRegions(shape.data.commands)).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('defers a stroked drawPath carrying a CLOSE verb (null)', () => {
|
|
92
|
+
const shape = createShape();
|
|
93
|
+
appendShapeLineStyle(shape, 6, 0x0000ff, 1, false, 'normal', 'none', 'miter', 4);
|
|
94
|
+
// A triangle subpath closed by an explicit CLOSE verb — a ring, not an open stroke.
|
|
95
|
+
const verbs = [PathCommand.MOVE_TO, PathCommand.LINE_TO, PathCommand.LINE_TO, PathCommand.CLOSE];
|
|
96
|
+
appendShapePath(shape, verbs, [0, 0, 40, 0, 20, 30], 'nonZero');
|
|
97
|
+
expect(getShapeStrokeRegions(shape.data.commands)).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('defers a stroked return-to-start polygon (appendShapePolygon) even without a CLOSE verb (null)', () => {
|
|
101
|
+
const shape = createShape();
|
|
102
|
+
appendShapeLineStyle(shape, 6, 0x0000ff, 1, false, 'normal', 'none', 'miter', 4);
|
|
103
|
+
// appendShapePolygon re-emits the first point as a trailing lineTo (return-to-start), no CLOSE verb.
|
|
104
|
+
appendShapePolygon(shape, [0, 0, 50, 0, 25, 40]);
|
|
105
|
+
expect(getShapeStrokeRegions(shape.data.commands)).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('ignores an UNSTROKED closed primitive and still strokes a later open line (span-aware)', () => {
|
|
109
|
+
const shape = createShape();
|
|
110
|
+
// Rectangle drawn with no active lineStyle — it never reaches a centerline, so it must NOT force the
|
|
111
|
+
// whole shape to the raster fallback. The later open stroked line still yields one region.
|
|
112
|
+
appendShapeRectangle(shape, 0, 0, 100, 50);
|
|
113
|
+
appendShapeLineStyle(shape, 6, 0xff0000, 1, false, 'normal', 'none', 'miter', 4);
|
|
114
|
+
appendShapeMoveTo(shape, 10, 80);
|
|
115
|
+
appendShapeLineTo(shape, 90, 80);
|
|
116
|
+
const regions = getShapeStrokeRegions(shape.data.commands);
|
|
117
|
+
expect(regions).not.toBeNull();
|
|
118
|
+
expect(regions!.length).toBe(1);
|
|
119
|
+
expect(regions![0].color).toBe(0xff0000);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('defers a gradient/bitmap stroke to the raster path (null)', () => {
|
|
123
|
+
const gradientStroke: ShapeCommandToken[] = ['lineGradientStyle', 1, 0, 'moveTo', 2, 0, 0, 'lineTo', 2, 50, 0];
|
|
124
|
+
expect(getShapeStrokeRegions(gradientStroke)).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('hasNonSolidShapeStroke', () => {
|
|
129
|
+
it('is true only for a gradient or bitmap stroke, not a solid lineStyle', () => {
|
|
130
|
+
const solid = createShape();
|
|
131
|
+
appendShapeLineStyle(solid, 3);
|
|
132
|
+
expect(hasNonSolidShapeStroke(solid.data.commands)).toBe(false);
|
|
133
|
+
expect(hasNonSolidShapeStroke(['lineGradientStyle', 1, 0] as ShapeCommandToken[])).toBe(true);
|
|
134
|
+
expect(hasNonSolidShapeStroke(['lineBitmapStyle', 1, 0] as ShapeCommandToken[])).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|