@flighthq/shape 0.3.0-next.906.07cea63 → 0.3.1-next.209.43ef1bc
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/compactStrokePath.d.ts +3 -0
- package/dist/compactStrokePath.d.ts.map +1 -0
- package/dist/compactStrokePath.js +462 -0
- package/dist/compactStrokePath.js.map +1 -0
- package/dist/contract.d.ts +7 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +7 -2
- package/dist/contract.js.map +1 -1
- package/dist/explainMorphShapeGradientEndpoints.d.ts +5 -0
- package/dist/explainMorphShapeGradientEndpoints.d.ts.map +1 -0
- package/dist/explainMorphShapeGradientEndpoints.js +43 -0
- package/dist/explainMorphShapeGradientEndpoints.js.map +1 -0
- package/dist/explainShapeTessellation.d.ts +3 -0
- package/dist/explainShapeTessellation.d.ts.map +1 -0
- package/dist/explainShapeTessellation.js +20 -0
- package/dist/explainShapeTessellation.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/morphShape.d.ts +8 -0
- package/dist/morphShape.d.ts.map +1 -0
- package/dist/morphShape.js +74 -0
- package/dist/morphShape.js.map +1 -0
- package/dist/morphShapeAnimation.d.ts +5 -0
- package/dist/morphShapeAnimation.d.ts.map +1 -0
- package/dist/morphShapeAnimation.js +27 -0
- package/dist/morphShapeAnimation.js.map +1 -0
- package/dist/morphShapePaint.d.ts +9 -0
- package/dist/morphShapePaint.d.ts.map +1 -0
- package/dist/morphShapePaint.js +183 -0
- package/dist/morphShapePaint.js.map +1 -0
- package/dist/scale9ShapeCommands.d.ts +3 -0
- package/dist/scale9ShapeCommands.d.ts.map +1 -0
- package/dist/scale9ShapeCommands.js +119 -0
- package/dist/scale9ShapeCommands.js.map +1 -0
- package/dist/shape.d.ts.map +1 -1
- package/dist/shape.js +16 -3
- package/dist/shape.js.map +1 -1
- package/dist/shapeCommands.d.ts +3 -3
- package/dist/shapeCommands.d.ts.map +1 -1
- package/dist/shapeCommands.js +8 -8
- package/dist/shapeCommands.js.map +1 -1
- package/dist/shapeFill.d.ts.map +1 -1
- package/dist/shapeFill.js +8 -14
- package/dist/shapeFill.js.map +1 -1
- package/dist/shapeStroke.d.ts +2 -2
- package/dist/shapeStroke.d.ts.map +1 -1
- package/dist/shapeStroke.js +13 -77
- package/dist/shapeStroke.js.map +1 -1
- package/dist/shapeStrokeOutline.d.ts +3 -0
- package/dist/shapeStrokeOutline.d.ts.map +1 -0
- package/dist/shapeStrokeOutline.js +164 -0
- package/dist/shapeStrokeOutline.js.map +1 -0
- package/package.json +10 -7
- package/src/compactStrokePath.test.ts +31 -0
- package/src/explainMorphShapeGradientEndpoints.test.ts +47 -0
- package/src/explainShapeTessellation.test.ts +65 -0
- package/src/morphShape.test.ts +207 -0
- package/src/morphShapeAnimation.test.ts +147 -0
- package/src/morphShapePaint.test.ts +236 -0
- package/src/scale9ShapeCommands.test.ts +64 -0
- package/src/shape.test.ts +35 -0
- package/src/shapeCommands.test.ts +31 -31
- package/src/shapeFill.test.ts +9 -3
- package/src/shapeStroke.test.ts +35 -24
- package/src/shapeStrokeOutline.test.ts +31 -0
- package/dist/shapeHitTestBuiltins.d.ts +0 -2
- package/dist/shapeHitTestBuiltins.d.ts.map +0 -1
- package/dist/shapeHitTestBuiltins.js +0 -85
- package/dist/shapeHitTestBuiltins.js.map +0 -1
- package/dist/shapeHitTestRegistry.d.ts +0 -4
- package/dist/shapeHitTestRegistry.d.ts.map +0 -1
- package/dist/shapeHitTestRegistry.js +0 -13
- package/dist/shapeHitTestRegistry.js.map +0 -1
- package/src/shapeHitTestBuiltins.test.ts +0 -77
- package/src/shapeHitTestRegistry.test.ts +0 -45
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { PathCommand } from '@flighthq/types/contract';
|
|
2
|
+
import { compactStrokePath } from './compactStrokePath';
|
|
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 by the compact open-stroke kernel (real
|
|
6
|
+
// 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) without retaining the larger closed-ring validation kernel.
|
|
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; the backend's
|
|
21
|
+
// explicit stroke-path-tessellation feature owns the closed-ring strip route.
|
|
22
|
+
export function getShapeStrokeOutlineRegions(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 = compactStrokePath(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
|
+
function hasNonSolidShapeStroke(commands) {
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < commands.length) {
|
|
107
|
+
const name = commands[i];
|
|
108
|
+
if (name === 'lineGradientStyle' || name === 'lineTextureStyle')
|
|
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=shapeStrokeOutline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shapeStrokeOutline.js","sourceRoot":"","sources":["../src/shapeStrokeOutline.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/shape",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1-next.209.43ef1bc",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -36,15 +38,16 @@
|
|
|
36
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@flighthq/
|
|
40
|
-
"@flighthq/
|
|
41
|
-
"@flighthq/
|
|
42
|
-
"@flighthq/
|
|
43
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/animation": "0.3.1-next.209.43ef1bc",
|
|
42
|
+
"@flighthq/scene2d": "0.3.1-next.209.43ef1bc",
|
|
43
|
+
"@flighthq/geometry": "0.3.1-next.209.43ef1bc",
|
|
44
|
+
"@flighthq/node": "0.3.1-next.209.43ef1bc",
|
|
45
|
+
"@flighthq/path": "0.3.1-next.209.43ef1bc",
|
|
46
|
+
"@flighthq/types": "0.3.1-next.209.43ef1bc"
|
|
44
47
|
},
|
|
45
48
|
"devDependencies": {
|
|
46
49
|
"typescript": "^5.3.0"
|
|
47
50
|
},
|
|
48
|
-
"description": "Vector Shape display
|
|
51
|
+
"description": "Vector Shape and MorphShape display nodes: retained commands, compound geometry and paint morphing, scale-9, and hit testing",
|
|
49
52
|
"sideEffects": false
|
|
50
53
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Path } from '@flighthq/types/contract';
|
|
2
|
+
import { PathCommand } from '@flighthq/types/contract';
|
|
3
|
+
|
|
4
|
+
import { compactStrokePath } from './compactStrokePath';
|
|
5
|
+
|
|
6
|
+
describe('compactStrokePath', () => {
|
|
7
|
+
it('converts an open centerline into a closed fill outline', () => {
|
|
8
|
+
const centerline: Path = {
|
|
9
|
+
commands: [PathCommand.MOVE_TO, PathCommand.LINE_TO],
|
|
10
|
+
data: [0, 0, 20, 0],
|
|
11
|
+
winding: 'nonZero',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const outline = compactStrokePath(centerline, { cap: 'square', width: 4 });
|
|
15
|
+
|
|
16
|
+
expect(outline.commands[0]).toBe(PathCommand.MOVE_TO);
|
|
17
|
+
expect(outline.commands.at(-1)).toBe(PathCommand.CLOSE);
|
|
18
|
+
expect(outline.data).toContain(-2);
|
|
19
|
+
expect(outline.data).toContain(22);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns an empty path for a centerline without a segment', () => {
|
|
23
|
+
const centerline: Path = {
|
|
24
|
+
commands: [PathCommand.MOVE_TO],
|
|
25
|
+
data: [0, 0],
|
|
26
|
+
winding: 'nonZero',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
expect(compactStrokePath(centerline, { width: 4 }).commands).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
explainMorphShapeGradientEndpoints,
|
|
3
|
+
getMorphShapeGradientEndpointIssue,
|
|
4
|
+
} from './explainMorphShapeGradientEndpoints';
|
|
5
|
+
|
|
6
|
+
describe('explainMorphShapeGradientEndpoints', () => {
|
|
7
|
+
it('reports compatible equal-length gradient stops', () => {
|
|
8
|
+
expect(
|
|
9
|
+
explainMorphShapeGradientEndpoints(
|
|
10
|
+
{ alphas: [1], colors: [0], ratios: [0] },
|
|
11
|
+
{ alphas: [1], colors: [0xffffff], ratios: [255] },
|
|
12
|
+
),
|
|
13
|
+
).toStrictEqual({ endStopCount: 1, reason: 'ok', startStopCount: 1, supported: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it.each([
|
|
17
|
+
[{ alphas: [], colors: [], ratios: [] }, { alphas: [], colors: [], ratios: [] }, 'empty-gradient'],
|
|
18
|
+
[
|
|
19
|
+
{ alphas: [], colors: [0], ratios: [0] },
|
|
20
|
+
{ alphas: [1], colors: [0], ratios: [0] },
|
|
21
|
+
'start-stop-component-count-mismatch',
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
{ alphas: [1], colors: [0], ratios: [0] },
|
|
25
|
+
{ alphas: [], colors: [0], ratios: [0] },
|
|
26
|
+
'end-stop-component-count-mismatch',
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
{ alphas: [1], colors: [0], ratios: [0] },
|
|
30
|
+
{ alphas: [1, 1], colors: [0, 1], ratios: [0, 255] },
|
|
31
|
+
'stop-count-mismatch',
|
|
32
|
+
],
|
|
33
|
+
])('reports incompatible topology as %s', (start, end, reason) => {
|
|
34
|
+
expect(explainMorphShapeGradientEndpoints(start, end)).toMatchObject({ reason, supported: false });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('getMorphShapeGradientEndpointIssue', () => {
|
|
39
|
+
it('provides a string-free compatibility seam for paint construction', () => {
|
|
40
|
+
expect(
|
|
41
|
+
getMorphShapeGradientEndpointIssue(
|
|
42
|
+
{ alphas: [1], colors: [0], ratios: [0] },
|
|
43
|
+
{ alphas: [1], colors: [0xffffff], ratios: [255] },
|
|
44
|
+
),
|
|
45
|
+
).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { explainShapeTessellation } from './explainShapeTessellation';
|
|
2
|
+
import { createShape } from './shape';
|
|
3
|
+
import {
|
|
4
|
+
appendShapeBeginFill,
|
|
5
|
+
appendShapeBeginGradientFill,
|
|
6
|
+
appendShapeEndFill,
|
|
7
|
+
appendShapeLineStyle,
|
|
8
|
+
appendShapeLineTo,
|
|
9
|
+
appendShapeMoveTo,
|
|
10
|
+
appendShapeRectangle,
|
|
11
|
+
} from './shapeCommands';
|
|
12
|
+
|
|
13
|
+
describe('explainShapeTessellation', () => {
|
|
14
|
+
it('reports a solid fill as tessellating at any alpha, opaque or not', () => {
|
|
15
|
+
// Alpha is carried on the region, so it never moves a shape off the mesh lane — worth pinning,
|
|
16
|
+
// because a translucent shape that fails to draw invites blaming the alpha.
|
|
17
|
+
for (const alpha of [1, 0.25, 0]) {
|
|
18
|
+
const shape = createShape();
|
|
19
|
+
appendShapeBeginFill(shape, 0xff0000, alpha);
|
|
20
|
+
appendShapeRectangle(shape, 0, 0, 50, 50);
|
|
21
|
+
appendShapeEndFill(shape);
|
|
22
|
+
|
|
23
|
+
expect(explainShapeTessellation(shape.data.commands)).toEqual({ blockedBy: 'none', status: 'tessellates' });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('reports an open stroke as tessellating', () => {
|
|
28
|
+
const shape = createShape();
|
|
29
|
+
appendShapeLineStyle(shape, 2, 0x000000);
|
|
30
|
+
appendShapeMoveTo(shape, 0, 0);
|
|
31
|
+
appendShapeLineTo(shape, 50, 0);
|
|
32
|
+
|
|
33
|
+
expect(explainShapeTessellation(shape.data.commands)).toEqual({ blockedBy: 'none', status: 'tessellates' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('names the closed stroke that the default lane declines, and clears it for the opt-in lane', () => {
|
|
37
|
+
const shape = createShape();
|
|
38
|
+
appendShapeBeginFill(shape, 0xff0000, 1);
|
|
39
|
+
appendShapeLineStyle(shape, 2, 0x000000);
|
|
40
|
+
appendShapeRectangle(shape, 0, 0, 50, 50);
|
|
41
|
+
appendShapeEndFill(shape);
|
|
42
|
+
|
|
43
|
+
expect(explainShapeTessellation(shape.data.commands)).toEqual({
|
|
44
|
+
blockedBy: 'stroke-outline',
|
|
45
|
+
status: 'needs-rasterizer',
|
|
46
|
+
});
|
|
47
|
+
expect(explainShapeTessellation(shape.data.commands, true)).toEqual({
|
|
48
|
+
blockedBy: 'none',
|
|
49
|
+
status: 'tessellates',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('names a non-solid fill, which no stroke lane setting can rescue', () => {
|
|
54
|
+
const shape = createShape();
|
|
55
|
+
appendShapeBeginGradientFill(shape, 'linear', [0xff0000, 0x0000ff], [1, 1], [0, 255]);
|
|
56
|
+
appendShapeRectangle(shape, 0, 0, 50, 50);
|
|
57
|
+
appendShapeEndFill(shape);
|
|
58
|
+
|
|
59
|
+
expect(explainShapeTessellation(shape.data.commands)).toEqual({
|
|
60
|
+
blockedBy: 'non-solid-fill',
|
|
61
|
+
status: 'needs-rasterizer',
|
|
62
|
+
});
|
|
63
|
+
expect(explainShapeTessellation(shape.data.commands, true).status).toBe('needs-rasterizer');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { createRectangle } from '@flighthq/geometry/contract';
|
|
2
|
+
import { getNodeLocalContentRevision } from '@flighthq/node/contract';
|
|
3
|
+
import { appendPathLineTo, appendPathMoveTo, createPath, createPathMorph } from '@flighthq/path/contract';
|
|
4
|
+
import type {
|
|
5
|
+
MorphShapePaintBinding,
|
|
6
|
+
MorphShapePathBinding,
|
|
7
|
+
PathMorph,
|
|
8
|
+
ShapeCommandToken,
|
|
9
|
+
} from '@flighthq/types/contract';
|
|
10
|
+
import { MorphShapeKind } from '@flighthq/types/contract';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
appendMorphShapePath,
|
|
14
|
+
createMorphShape,
|
|
15
|
+
createMorphShapeData,
|
|
16
|
+
createMorphShapeRuntime,
|
|
17
|
+
getMorphShapeRuntime,
|
|
18
|
+
setMorphShapeProgress,
|
|
19
|
+
} from './morphShape';
|
|
20
|
+
import { getShapeBounds } from './shape';
|
|
21
|
+
import { appendShapeBeginFill, appendShapeEndFill } from './shapeCommands';
|
|
22
|
+
|
|
23
|
+
describe('appendMorphShapePath', () => {
|
|
24
|
+
it('inserts the live path between ordinary Shape styling commands', () => {
|
|
25
|
+
const shape = createMorphShape(createTestMorph());
|
|
26
|
+
appendShapeBeginFill(shape, 0x336699, 0.75);
|
|
27
|
+
|
|
28
|
+
appendMorphShapePath(shape);
|
|
29
|
+
appendShapeEndFill(shape);
|
|
30
|
+
|
|
31
|
+
expect(shape.data.commands).toStrictEqual([
|
|
32
|
+
'beginFill',
|
|
33
|
+
2,
|
|
34
|
+
0x336699,
|
|
35
|
+
0.75,
|
|
36
|
+
'drawPath',
|
|
37
|
+
3,
|
|
38
|
+
shape.data.path.commands,
|
|
39
|
+
shape.data.path.data,
|
|
40
|
+
'nonZero',
|
|
41
|
+
'endFill',
|
|
42
|
+
0,
|
|
43
|
+
]);
|
|
44
|
+
expect(shape.data.commands[6]).toBe(shape.data.path.commands);
|
|
45
|
+
expect(shape.data.commands[7]).toBe(shape.data.path.data);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('retains and reuses independent path morphs in one command stream', () => {
|
|
49
|
+
const shape = createMorphShape(createTestMorph());
|
|
50
|
+
const secondMorph = createTestMorph(100);
|
|
51
|
+
const primaryPath = appendMorphShapePath(shape);
|
|
52
|
+
const secondPath = appendMorphShapePath(shape, secondMorph);
|
|
53
|
+
const repeatedSecondPath = appendMorphShapePath(shape, secondMorph);
|
|
54
|
+
const primaryCommands = primaryPath.commands;
|
|
55
|
+
const primaryData = primaryPath.data;
|
|
56
|
+
const secondCommands = secondPath.commands;
|
|
57
|
+
const secondData = secondPath.data;
|
|
58
|
+
const revision = getNodeLocalContentRevision(shape);
|
|
59
|
+
|
|
60
|
+
setMorphShapeProgress(shape, 0.5);
|
|
61
|
+
|
|
62
|
+
expect(shape.data.pathBindings).toHaveLength(2);
|
|
63
|
+
expect(repeatedSecondPath).toBe(secondPath);
|
|
64
|
+
expect(primaryPath.commands).toBe(primaryCommands);
|
|
65
|
+
expect(primaryPath.data).toBe(primaryData);
|
|
66
|
+
expect(secondPath.commands).toBe(secondCommands);
|
|
67
|
+
expect(secondPath.data).toBe(secondData);
|
|
68
|
+
expect(primaryPath.data.slice(0, 2)).toStrictEqual([5, 10]);
|
|
69
|
+
expect(secondPath.data.slice(0, 2)).toStrictEqual([105, 110]);
|
|
70
|
+
expect(getNodeLocalContentRevision(shape)).toBe(revision + 1);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('createMorphShape', () => {
|
|
75
|
+
it('creates a distinct MorphShape over the prepared start sample', () => {
|
|
76
|
+
const morph = createTestMorph();
|
|
77
|
+
|
|
78
|
+
const shape = createMorphShape(morph);
|
|
79
|
+
|
|
80
|
+
expect(shape.kind).toBe(MorphShapeKind);
|
|
81
|
+
expect(shape.data.commands).toStrictEqual([]);
|
|
82
|
+
expect(shape.data.morph).toBe(morph);
|
|
83
|
+
expect(shape.data.pathBindings).toStrictEqual([{ morph, path: shape.data.path }]);
|
|
84
|
+
expect(shape.data.paintBindings).toStrictEqual([]);
|
|
85
|
+
expect(shape.data.path.commands).toStrictEqual(morph.commands);
|
|
86
|
+
expect(shape.data.path.data).toStrictEqual(morph.startData);
|
|
87
|
+
expect(shape.data.progress).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('uses initial progress and a provided command buffer', () => {
|
|
91
|
+
const commands: ShapeCommandToken[] = ['beginFill', 2, 0xff0000, 1];
|
|
92
|
+
|
|
93
|
+
const shape = createMorphShape(createTestMorph(), { data: { commands, progress: 0.5 } });
|
|
94
|
+
|
|
95
|
+
expect(shape.data.commands).toBe(commands);
|
|
96
|
+
expect(shape.data.path.data).toStrictEqual([5, 10, 10, 10, 15, 10, 20, 10]);
|
|
97
|
+
expect(shape.data.progress).toBe(0.5);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('owns a distinct sampled path for each retained shape', () => {
|
|
101
|
+
const morph = createTestMorph();
|
|
102
|
+
|
|
103
|
+
const a = createMorphShape(morph);
|
|
104
|
+
const b = createMorphShape(morph);
|
|
105
|
+
|
|
106
|
+
expect(a).not.toBe(b);
|
|
107
|
+
expect(a.data.path).not.toBe(b.data.path);
|
|
108
|
+
expect(a.data.path.commands).not.toBe(b.data.path.commands);
|
|
109
|
+
expect(a.data.path.data).not.toBe(b.data.path.data);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('createMorphShapeData', () => {
|
|
114
|
+
it('owns its mutable binding registries without replacing retained values', () => {
|
|
115
|
+
const morph = createTestMorph();
|
|
116
|
+
const paintBindings: MorphShapePaintBinding[] = [];
|
|
117
|
+
const pathBindings: MorphShapePathBinding[] = [];
|
|
118
|
+
|
|
119
|
+
const data = createMorphShapeData(morph, { paintBindings, pathBindings });
|
|
120
|
+
|
|
121
|
+
expect(data.paintBindings).not.toBe(paintBindings);
|
|
122
|
+
expect(data.pathBindings).not.toBe(pathBindings);
|
|
123
|
+
expect(paintBindings).toStrictEqual([]);
|
|
124
|
+
expect(pathBindings).toStrictEqual([]);
|
|
125
|
+
expect(data.pathBindings).toStrictEqual([{ morph, path: data.path }]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('samples the requested initial endpoint exactly', () => {
|
|
129
|
+
const morph = createTestMorph();
|
|
130
|
+
|
|
131
|
+
const data = createMorphShapeData(morph, { progress: 1 });
|
|
132
|
+
|
|
133
|
+
expect(data.path.data).toStrictEqual(morph.endData);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('createMorphShapeRuntime', () => {
|
|
138
|
+
it('creates the standard Shape bounds runtime', () => {
|
|
139
|
+
expect(createMorphShapeRuntime().computeLocalBoundsRectangle).toBeTypeOf('function');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('getMorphShapeRuntime', () => {
|
|
144
|
+
it('returns the runtime attached to the retained shape', () => {
|
|
145
|
+
expect(getMorphShapeRuntime(createMorphShape(createTestMorph()))).not.toBeNull();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('setMorphShapeProgress', () => {
|
|
150
|
+
it('does not invalidate or resample unchanged progress', () => {
|
|
151
|
+
const shape = createMorphShape(createTestMorph());
|
|
152
|
+
const revision = getNodeLocalContentRevision(shape);
|
|
153
|
+
const data = shape.data.path.data.slice();
|
|
154
|
+
|
|
155
|
+
setMorphShapeProgress(shape, 0);
|
|
156
|
+
|
|
157
|
+
expect(shape.data.path.data).toStrictEqual(data);
|
|
158
|
+
expect(getNodeLocalContentRevision(shape)).toBe(revision);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('resamples stable buffers, stores progress, and invalidates content', () => {
|
|
162
|
+
const shape = createMorphShape(createTestMorph());
|
|
163
|
+
appendMorphShapePath(shape);
|
|
164
|
+
const commands = shape.data.path.commands;
|
|
165
|
+
const data = shape.data.path.data;
|
|
166
|
+
const revision = getNodeLocalContentRevision(shape);
|
|
167
|
+
|
|
168
|
+
setMorphShapeProgress(shape, 0.5);
|
|
169
|
+
|
|
170
|
+
expect(shape.data.progress).toBe(0.5);
|
|
171
|
+
expect(shape.data.path.commands).toBe(commands);
|
|
172
|
+
expect(shape.data.path.data).toBe(data);
|
|
173
|
+
expect(shape.data.path.data).toStrictEqual([5, 10, 10, 10, 15, 10, 20, 10]);
|
|
174
|
+
expect(getNodeLocalContentRevision(shape)).toBe(revision + 1);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('updates the bounds observed through the ordinary Shape runtime', () => {
|
|
178
|
+
const shape = createMorphShape(createTestMorph());
|
|
179
|
+
appendMorphShapePath(shape);
|
|
180
|
+
const bounds = createRectangle();
|
|
181
|
+
getShapeBounds(bounds, shape);
|
|
182
|
+
expect(bounds).toMatchObject({ x: 0, y: 0, width: 10, height: 0 });
|
|
183
|
+
|
|
184
|
+
setMorphShapeProgress(shape, 1);
|
|
185
|
+
getShapeBounds(bounds, shape);
|
|
186
|
+
|
|
187
|
+
expect(bounds).toMatchObject({ x: 10, y: 20, width: 20, height: 0 });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('preserves unclamped easing overshoot', () => {
|
|
191
|
+
const shape = createMorphShape(createTestMorph());
|
|
192
|
+
|
|
193
|
+
setMorphShapeProgress(shape, 1.5);
|
|
194
|
+
|
|
195
|
+
expect(shape.data.path.data.slice(0, 2)).toStrictEqual([15, 30]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
function createTestMorph(offset = 0): PathMorph {
|
|
200
|
+
const start = createPath();
|
|
201
|
+
appendPathMoveTo(start, offset, offset);
|
|
202
|
+
appendPathLineTo(start, offset + 10, offset);
|
|
203
|
+
const end = createPath();
|
|
204
|
+
appendPathMoveTo(end, offset + 10, offset + 20);
|
|
205
|
+
appendPathLineTo(end, offset + 30, offset + 20);
|
|
206
|
+
return createPathMorph(start, end)!;
|
|
207
|
+
}
|