@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.
Files changed (77) hide show
  1. package/dist/compactStrokePath.d.ts +3 -0
  2. package/dist/compactStrokePath.d.ts.map +1 -0
  3. package/dist/compactStrokePath.js +462 -0
  4. package/dist/compactStrokePath.js.map +1 -0
  5. package/dist/contract.d.ts +7 -2
  6. package/dist/contract.d.ts.map +1 -1
  7. package/dist/contract.js +7 -2
  8. package/dist/contract.js.map +1 -1
  9. package/dist/explainMorphShapeGradientEndpoints.d.ts +5 -0
  10. package/dist/explainMorphShapeGradientEndpoints.d.ts.map +1 -0
  11. package/dist/explainMorphShapeGradientEndpoints.js +43 -0
  12. package/dist/explainMorphShapeGradientEndpoints.js.map +1 -0
  13. package/dist/explainShapeTessellation.d.ts +3 -0
  14. package/dist/explainShapeTessellation.d.ts.map +1 -0
  15. package/dist/explainShapeTessellation.js +20 -0
  16. package/dist/explainShapeTessellation.js.map +1 -0
  17. package/dist/index.d.ts +3 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/morphShape.d.ts +8 -0
  22. package/dist/morphShape.d.ts.map +1 -0
  23. package/dist/morphShape.js +74 -0
  24. package/dist/morphShape.js.map +1 -0
  25. package/dist/morphShapeAnimation.d.ts +5 -0
  26. package/dist/morphShapeAnimation.d.ts.map +1 -0
  27. package/dist/morphShapeAnimation.js +27 -0
  28. package/dist/morphShapeAnimation.js.map +1 -0
  29. package/dist/morphShapePaint.d.ts +9 -0
  30. package/dist/morphShapePaint.d.ts.map +1 -0
  31. package/dist/morphShapePaint.js +183 -0
  32. package/dist/morphShapePaint.js.map +1 -0
  33. package/dist/scale9ShapeCommands.d.ts +3 -0
  34. package/dist/scale9ShapeCommands.d.ts.map +1 -0
  35. package/dist/scale9ShapeCommands.js +119 -0
  36. package/dist/scale9ShapeCommands.js.map +1 -0
  37. package/dist/shape.d.ts.map +1 -1
  38. package/dist/shape.js +16 -3
  39. package/dist/shape.js.map +1 -1
  40. package/dist/shapeCommands.d.ts +3 -3
  41. package/dist/shapeCommands.d.ts.map +1 -1
  42. package/dist/shapeCommands.js +8 -8
  43. package/dist/shapeCommands.js.map +1 -1
  44. package/dist/shapeFill.d.ts.map +1 -1
  45. package/dist/shapeFill.js +8 -14
  46. package/dist/shapeFill.js.map +1 -1
  47. package/dist/shapeStroke.d.ts +2 -2
  48. package/dist/shapeStroke.d.ts.map +1 -1
  49. package/dist/shapeStroke.js +13 -77
  50. package/dist/shapeStroke.js.map +1 -1
  51. package/dist/shapeStrokeOutline.d.ts +3 -0
  52. package/dist/shapeStrokeOutline.d.ts.map +1 -0
  53. package/dist/shapeStrokeOutline.js +164 -0
  54. package/dist/shapeStrokeOutline.js.map +1 -0
  55. package/package.json +10 -7
  56. package/src/compactStrokePath.test.ts +31 -0
  57. package/src/explainMorphShapeGradientEndpoints.test.ts +47 -0
  58. package/src/explainShapeTessellation.test.ts +65 -0
  59. package/src/morphShape.test.ts +207 -0
  60. package/src/morphShapeAnimation.test.ts +147 -0
  61. package/src/morphShapePaint.test.ts +236 -0
  62. package/src/scale9ShapeCommands.test.ts +64 -0
  63. package/src/shape.test.ts +35 -0
  64. package/src/shapeCommands.test.ts +31 -31
  65. package/src/shapeFill.test.ts +9 -3
  66. package/src/shapeStroke.test.ts +35 -24
  67. package/src/shapeStrokeOutline.test.ts +31 -0
  68. package/dist/shapeHitTestBuiltins.d.ts +0 -2
  69. package/dist/shapeHitTestBuiltins.d.ts.map +0 -1
  70. package/dist/shapeHitTestBuiltins.js +0 -85
  71. package/dist/shapeHitTestBuiltins.js.map +0 -1
  72. package/dist/shapeHitTestRegistry.d.ts +0 -4
  73. package/dist/shapeHitTestRegistry.d.ts.map +0 -1
  74. package/dist/shapeHitTestRegistry.js +0 -13
  75. package/dist/shapeHitTestRegistry.js.map +0 -1
  76. package/src/shapeHitTestBuiltins.test.ts +0 -77
  77. package/src/shapeHitTestRegistry.test.ts +0 -45
@@ -0,0 +1,147 @@
1
+ import {
2
+ createAnimationBlendTree,
3
+ createAnimationBlendTreeInput,
4
+ createAnimationChannel,
5
+ createAnimationClip,
6
+ createAnimationPlayer,
7
+ createAnimationTrack,
8
+ sampleAnimationBlendTree,
9
+ } from '@flighthq/animation/contract';
10
+ import { getNodeLocalContentRevision } from '@flighthq/node/contract';
11
+ import { appendPathLineTo, appendPathMoveTo, createPath, createPathMorph } from '@flighthq/path/contract';
12
+ import type { MorphShape, MorphShapeAnimationTarget } from '@flighthq/types/contract';
13
+
14
+ import { appendMorphShapePath, createMorphShape } from './morphShape';
15
+ import {
16
+ applyAnimationClipToMorphShape,
17
+ applyMorphShapeAnimationSample,
18
+ createMorphShapeAnimationTarget,
19
+ } from './morphShapeAnimation';
20
+ import { appendMorphShapeBeginFill } from './morphShapePaint';
21
+
22
+ describe('applyAnimationClipToMorphShape', () => {
23
+ it('samples scalar progress while preserving easing overshoot', () => {
24
+ const shape = createTestMorphShape();
25
+ const target = createMorphShapeAnimationTarget(shape);
26
+ const clip = createAnimationClip([
27
+ createAnimationChannel(
28
+ createAnimationTrack({ easing: (progress) => progress * 2, times: [0, 1], values: [0, 1] }),
29
+ target,
30
+ ),
31
+ ]);
32
+
33
+ applyAnimationClipToMorphShape(clip, 0.75);
34
+
35
+ expect(shape.data.progress).toBe(1.5);
36
+ expect(shape.data.path.data.slice(0, 2)).toStrictEqual([30, 45]);
37
+ });
38
+
39
+ it('applies one clip sample atomically to compound geometry and paint', () => {
40
+ const shape = createTestMorphShape();
41
+ const secondMorph = createTestMorph(100);
42
+ appendMorphShapeBeginFill(shape, { color: 0xff0000 }, { color: 0x0000ff });
43
+ appendMorphShapePath(shape);
44
+ const secondPath = appendMorphShapePath(shape, secondMorph);
45
+ const revision = getNodeLocalContentRevision(shape);
46
+ const clip = createAnimationClip([
47
+ createAnimationChannel(
48
+ createAnimationTrack({ times: [0, 1], values: [0, 1] }),
49
+ createMorphShapeAnimationTarget(shape),
50
+ ),
51
+ ]);
52
+
53
+ applyAnimationClipToMorphShape(clip, 0.5);
54
+
55
+ expect(shape.data.path.data.slice(0, 2)).toStrictEqual([10, 15]);
56
+ expect(secondPath.data.slice(0, 2)).toStrictEqual([110, 115]);
57
+ expect(shape.data.commands[2]).toBe(0x800080);
58
+ expect(getNodeLocalContentRevision(shape)).toBe(revision + 1);
59
+ });
60
+
61
+ it('ignores foreign channels in a composed clip', () => {
62
+ const shape = createTestMorphShape();
63
+ const clip = createAnimationClip([
64
+ createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 100] }), { node: shape, path: 'X' }),
65
+ createAnimationChannel(
66
+ createAnimationTrack({ times: [0, 1], values: [0, 1] }),
67
+ createMorphShapeAnimationTarget(shape),
68
+ ),
69
+ ]);
70
+
71
+ applyAnimationClipToMorphShape(clip, 0.25);
72
+
73
+ expect(shape.x).toBe(0);
74
+ expect(shape.data.progress).toBe(0.25);
75
+ });
76
+ });
77
+
78
+ describe('applyMorphShapeAnimationSample', () => {
79
+ it('mutates stable path buffers and invalidates retained content', () => {
80
+ const shape = createTestMorphShape();
81
+ const commands = shape.data.path.commands;
82
+ const data = shape.data.path.data;
83
+ const revision = getNodeLocalContentRevision(shape);
84
+ const channel = createAnimationChannel(
85
+ createAnimationTrack({ times: [0, 1], values: [0, 1] }),
86
+ createMorphShapeAnimationTarget(shape),
87
+ );
88
+
89
+ expect(applyMorphShapeAnimationSample([0.5], channel)).toBe(true);
90
+ expect(shape.data.progress).toBe(0.5);
91
+ expect(shape.data.path.commands).toBe(commands);
92
+ expect(shape.data.path.data).toBe(data);
93
+ expect(getNodeLocalContentRevision(shape)).toBe(revision + 1);
94
+ });
95
+
96
+ it('serves as the sink for composed animation samples', () => {
97
+ const shape = createTestMorphShape();
98
+ const target = createMorphShapeAnimationTarget(shape);
99
+ const a = createAnimationPlayer(
100
+ createAnimationClip([createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 1] }), target)]),
101
+ { time: 0.5 },
102
+ );
103
+ const b = createAnimationPlayer(
104
+ createAnimationClip([createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 0.2] }), target)]),
105
+ { time: 0.5 },
106
+ );
107
+ const tree = createAnimationBlendTree([createAnimationBlendTreeInput(a, 1), createAnimationBlendTreeInput(b, 1)]);
108
+
109
+ sampleAnimationBlendTree([0], tree, applyMorphShapeAnimationSample);
110
+
111
+ expect(shape.data.progress).toBeCloseTo(0.3);
112
+ });
113
+
114
+ it('returns false for foreign targets without changing the shape', () => {
115
+ const shape = createTestMorphShape();
116
+ const channel = createAnimationChannel(createAnimationTrack({ times: [0, 1], values: [0, 1] }), {
117
+ shape: { kind: 'acme.NotMorphShape' },
118
+ });
119
+
120
+ expect(applyMorphShapeAnimationSample([1], channel)).toBe(false);
121
+ expect(shape.data.progress).toBe(0);
122
+ });
123
+ });
124
+
125
+ describe('createMorphShapeAnimationTarget', () => {
126
+ it('allocates a reusable target descriptor for one MorphShape', () => {
127
+ const shape = createTestMorphShape();
128
+
129
+ const target: MorphShapeAnimationTarget = createMorphShapeAnimationTarget(shape);
130
+
131
+ expect(target).toStrictEqual({ shape });
132
+ });
133
+ });
134
+
135
+ function createTestMorphShape(): MorphShape {
136
+ return createMorphShape(createTestMorph());
137
+ }
138
+
139
+ function createTestMorph(offset = 0) {
140
+ const start = createPath();
141
+ appendPathMoveTo(start, offset, offset);
142
+ appendPathLineTo(start, offset + 10, offset);
143
+ const end = createPath();
144
+ appendPathMoveTo(end, offset + 20, offset + 30);
145
+ appendPathLineTo(end, offset + 40, offset + 30);
146
+ return createPathMorph(start, end)!;
147
+ }
@@ -0,0 +1,236 @@
1
+ import { createMatrix } from '@flighthq/geometry/contract';
2
+ import { getNodeLocalContentRevision } from '@flighthq/node/contract';
3
+ import { appendPathLineTo, appendPathMoveTo, createPath, createPathMorph } from '@flighthq/path/contract';
4
+
5
+ import { createMorphShape, setMorphShapeProgress } from './morphShape';
6
+ import {
7
+ appendMorphShapeBeginFill,
8
+ appendMorphShapeBeginGradientFill,
9
+ appendMorphShapeBeginTextureFill,
10
+ appendMorphShapeLineGradientStyle,
11
+ appendMorphShapeLineStyle,
12
+ appendMorphShapeLineTextureStyle,
13
+ sampleMorphShapePaintBindings,
14
+ } from './morphShapePaint';
15
+ import { clearShapeCommands } from './shape';
16
+
17
+ const fakeTexture = { id: 1 } as never;
18
+
19
+ describe('appendMorphShapeBeginFill', () => {
20
+ it('retains one command while sampling packed RGB and alpha at the current progress', () => {
21
+ const shape = createTestMorphShape(0.5);
22
+
23
+ appendMorphShapeBeginFill(shape, { alpha: 0.25, color: 0xff0000 }, { alpha: 0.75, color: 0x0000ff });
24
+
25
+ expect(shape.data.commands).toStrictEqual(['beginFill', 2, 0x800080, 0.5]);
26
+ expect(shape.data.paintBindings).toHaveLength(1);
27
+ const commands = shape.data.commands;
28
+ setMorphShapeProgress(shape, 1);
29
+ expect(shape.data.commands).toBe(commands);
30
+ expect(shape.data.commands).toStrictEqual(['beginFill', 2, 0x0000ff, 0.75]);
31
+ });
32
+ });
33
+
34
+ describe('appendMorphShapeBeginGradientFill', () => {
35
+ it('samples stops, matrices, and focal ratio into stable retained values', () => {
36
+ const shape = createTestMorphShape();
37
+ const startMatrix = createMatrix(1, 2, 3, 4, 5, 6);
38
+ const endMatrix = createMatrix(3, 4, 5, 6, 7, 8);
39
+
40
+ expect(
41
+ appendMorphShapeBeginGradientFill(
42
+ shape,
43
+ 'radial',
44
+ {
45
+ alphas: [0.25, 0.5],
46
+ colors: [0x000000, 0xff0000],
47
+ focalPointRatio: -0.5,
48
+ matrix: startMatrix,
49
+ ratios: [0, 100],
50
+ },
51
+ {
52
+ alphas: [0.75, 1],
53
+ colors: [0xffffff, 0x0000ff],
54
+ focalPointRatio: 0.5,
55
+ matrix: endMatrix,
56
+ ratios: [100, 200],
57
+ },
58
+ ),
59
+ ).toBe(true);
60
+ const colors = shape.data.commands[3];
61
+ const alphas = shape.data.commands[4];
62
+ const ratios = shape.data.commands[5];
63
+ const matrix = shape.data.commands[6];
64
+
65
+ setMorphShapeProgress(shape, 0.5);
66
+
67
+ expect(shape.data.commands[3]).toBe(colors);
68
+ expect(shape.data.commands[4]).toBe(alphas);
69
+ expect(shape.data.commands[5]).toBe(ratios);
70
+ expect(shape.data.commands[6]).toBe(matrix);
71
+ expect(colors).toStrictEqual([0x808080, 0x800080]);
72
+ expect(alphas).toStrictEqual([0.5, 0.75]);
73
+ expect(ratios).toStrictEqual([50, 150]);
74
+ expect(matrix).toMatchObject({ a: 2, b: 3, c: 4, d: 5, tx: 6, ty: 7 });
75
+ expect(shape.data.commands[9]).toBe(0);
76
+ });
77
+
78
+ it('rejects incompatible gradient stop topology without mutating the shape', () => {
79
+ const shape = createTestMorphShape();
80
+ const revision = getNodeLocalContentRevision(shape);
81
+
82
+ expect(
83
+ appendMorphShapeBeginGradientFill(
84
+ shape,
85
+ 'linear',
86
+ { alphas: [1], colors: [0], ratios: [0] },
87
+ { alphas: [1, 1], colors: [0, 1], ratios: [0, 255] },
88
+ ),
89
+ ).toBe(false);
90
+ expect(shape.data.commands).toStrictEqual([]);
91
+ expect(shape.data.paintBindings).toStrictEqual([]);
92
+ expect(getNodeLocalContentRevision(shape)).toBe(revision);
93
+ });
94
+
95
+ it('treats a missing endpoint matrix as identity while retaining one live matrix', () => {
96
+ const shape = createTestMorphShape();
97
+ expect(
98
+ appendMorphShapeBeginGradientFill(
99
+ shape,
100
+ 'linear',
101
+ { alphas: [1], colors: [0], ratios: [0] },
102
+ { alphas: [1], colors: [0xffffff], matrix: createMatrix(3, 0, 0, 5, 20, 40), ratios: [255] },
103
+ ),
104
+ ).toBe(true);
105
+ const matrix = shape.data.commands[6];
106
+
107
+ setMorphShapeProgress(shape, 0.5);
108
+
109
+ expect(shape.data.commands[6]).toBe(matrix);
110
+ expect(matrix).toMatchObject({ a: 2, b: 0, c: 0, d: 3, tx: 10, ty: 20 });
111
+ });
112
+ });
113
+
114
+ describe('appendMorphShapeBeginTextureFill', () => {
115
+ it('preserves an ordinary null matrix when neither endpoint needs placement morphing', () => {
116
+ const shape = createTestMorphShape();
117
+
118
+ appendMorphShapeBeginTextureFill(shape, fakeTexture);
119
+
120
+ expect(shape.data.commands).toStrictEqual(['beginTextureFill', 2, fakeTexture, null]);
121
+ expect(shape.data.paintBindings).toStrictEqual([]);
122
+ });
123
+
124
+ it('morphs the placement matrix while retaining the texture and matrix identities', () => {
125
+ const shape = createTestMorphShape();
126
+ appendMorphShapeBeginTextureFill(shape, fakeTexture, null, createMatrix(2, 0, 0, 4, 10, 20));
127
+ const matrix = shape.data.commands[3];
128
+
129
+ setMorphShapeProgress(shape, 0.5);
130
+
131
+ expect(shape.data.commands[2]).toBe(fakeTexture);
132
+ expect(shape.data.commands[3]).toBe(matrix);
133
+ expect(matrix).toMatchObject({ a: 1.5, b: 0, c: 0, d: 2.5, tx: 5, ty: 10 });
134
+ });
135
+ });
136
+
137
+ describe('appendMorphShapeLineGradientStyle', () => {
138
+ it('uses the same stable gradient sampler for a stroke paint', () => {
139
+ const shape = createTestMorphShape();
140
+
141
+ expect(
142
+ appendMorphShapeLineGradientStyle(
143
+ shape,
144
+ 'linear',
145
+ { alphas: [1], colors: [0xff0000], ratios: [0] },
146
+ { alphas: [0.5], colors: [0x0000ff], ratios: [255] },
147
+ ),
148
+ ).toBe(true);
149
+ setMorphShapeProgress(shape, 1);
150
+
151
+ expect(shape.data.commands[0]).toBe('lineGradientStyle');
152
+ expect(shape.data.commands[3]).toStrictEqual([0x0000ff]);
153
+ expect(shape.data.commands[4]).toStrictEqual([0.5]);
154
+ expect(shape.data.commands[5]).toStrictEqual([255]);
155
+ });
156
+ });
157
+
158
+ describe('appendMorphShapeLineStyle', () => {
159
+ it('samples stroke width and color while retaining structural stroke options', () => {
160
+ const shape = createTestMorphShape(0.5);
161
+
162
+ appendMorphShapeLineStyle(
163
+ shape,
164
+ { alpha: 0.25, color: 0xff0000, thickness: 2 },
165
+ { alpha: 0.75, color: 0x0000ff, thickness: 10 },
166
+ true,
167
+ 'horizontal',
168
+ 'round',
169
+ 'bevel',
170
+ 5,
171
+ );
172
+
173
+ expect(shape.data.commands).toStrictEqual([
174
+ 'lineStyle',
175
+ 8,
176
+ 6,
177
+ 0x800080,
178
+ 0.5,
179
+ true,
180
+ 'horizontal',
181
+ 'round',
182
+ 'bevel',
183
+ 5,
184
+ ]);
185
+ });
186
+ });
187
+
188
+ describe('appendMorphShapeLineTextureStyle', () => {
189
+ it('uses the same stable matrix sampler for a stroke texture', () => {
190
+ const shape = createTestMorphShape();
191
+ appendMorphShapeLineTextureStyle(shape, fakeTexture, createMatrix(), createMatrix(2, 0, 0, 2, 10, 20));
192
+ const matrix = shape.data.commands[3];
193
+
194
+ setMorphShapeProgress(shape, 1);
195
+
196
+ expect(shape.data.commands[0]).toBe('lineTextureStyle');
197
+ expect(shape.data.commands[3]).toBe(matrix);
198
+ expect(matrix).toMatchObject({ a: 2, d: 2, tx: 10, ty: 20 });
199
+ });
200
+ });
201
+
202
+ describe('clearShapeCommands with MorphShape paint', () => {
203
+ it('removes stale paint bindings with the ordinary Shape command stream', () => {
204
+ const shape = createTestMorphShape();
205
+ appendMorphShapeBeginFill(shape, { color: 0 }, { color: 0xffffff });
206
+
207
+ clearShapeCommands(shape);
208
+ setMorphShapeProgress(shape, 1);
209
+
210
+ expect(shape.data.commands).toStrictEqual([]);
211
+ expect(shape.data.paintBindings).toStrictEqual([]);
212
+ });
213
+ });
214
+
215
+ describe('sampleMorphShapePaintBindings', () => {
216
+ it('can resample prepared paint bindings without reallocating their command values', () => {
217
+ const shape = createTestMorphShape();
218
+ appendMorphShapeBeginFill(shape, { color: 0 }, { color: 0xffffff });
219
+ const commands = shape.data.commands;
220
+
221
+ sampleMorphShapePaintBindings(shape.data, 0.25);
222
+
223
+ expect(shape.data.commands).toBe(commands);
224
+ expect(shape.data.commands[2]).toBe(0x404040);
225
+ });
226
+ });
227
+
228
+ function createTestMorphShape(progress = 0) {
229
+ const start = createPath();
230
+ appendPathMoveTo(start, 0, 0);
231
+ appendPathLineTo(start, 10, 0);
232
+ const end = createPath();
233
+ appendPathMoveTo(end, 20, 30);
234
+ appendPathLineTo(end, 40, 30);
235
+ return createMorphShape(createPathMorph(start, end)!, { data: { progress } });
236
+ }
@@ -0,0 +1,64 @@
1
+ import type { ShapeCommandToken } from '@flighthq/types/contract';
2
+
3
+ import { createScale9Shape } from './scale9Shape';
4
+ import { mapScale9ShapeCommands } from './scale9ShapeCommands';
5
+ import { appendShapeBeginFill, appendShapeRectangle } from './shapeCommands';
6
+
7
+ const grid = { x: 10, y: 10, width: 80, height: 80 };
8
+
9
+ describe('mapScale9ShapeCommands', () => {
10
+ const out: ShapeCommandToken[] = [];
11
+
12
+ it('passes style commands through unchanged', () => {
13
+ const mapper = { mapX: (x: number) => x, mapY: (y: number) => y };
14
+ mapScale9ShapeCommands(out, ['beginFill', 2, 0xff0000, 1], mapper);
15
+ expect(out).toEqual(['beginFill', 2, 0xff0000, 1]);
16
+ });
17
+
18
+ it('remaps moveTo coordinates', () => {
19
+ const mapper = { mapX: (x: number) => x * 2, mapY: (y: number) => y * 3 };
20
+ mapScale9ShapeCommands(out, ['moveTo', 2, 10, 20], mapper);
21
+ expect(out).toEqual(['moveTo', 2, 20, 60]);
22
+ });
23
+
24
+ it('remaps lineTo coordinates', () => {
25
+ const mapper = { mapX: (x: number) => x + 5, mapY: (y: number) => y + 10 };
26
+ mapScale9ShapeCommands(out, ['lineTo', 2, 100, 50], mapper);
27
+ expect(out).toEqual(['lineTo', 2, 105, 60]);
28
+ });
29
+
30
+ it('remaps drawRectangle corners and recomputes size', () => {
31
+ const mapper = { mapX: (x: number) => x * 2, mapY: (y: number) => y * 2 };
32
+ mapScale9ShapeCommands(out, ['drawRectangle', 4, 10, 20, 50, 30], mapper);
33
+ // x=10→20, y=20→40, x+w=60→120 (w=100), y+h=50→100 (h=60)
34
+ expect(out).toEqual(['drawRectangle', 4, 20, 40, 100, 60]);
35
+ });
36
+
37
+ it('remaps drawRoundRectangle corners but leaves ellipse radii unchanged', () => {
38
+ const mapper = { mapX: (x: number) => x * 2, mapY: (y: number) => y * 2 };
39
+ mapScale9ShapeCommands(out, ['drawRoundRectangle', 6, 10, 20, 50, 30, 8, 8], mapper);
40
+ expect(out).toEqual(['drawRoundRectangle', 6, 20, 40, 100, 60, 8, 8]);
41
+ });
42
+
43
+ it('remaps drawCircle center but leaves radius unchanged', () => {
44
+ const mapper = { mapX: (x: number) => x + 5, mapY: (y: number) => y + 10 };
45
+ mapScale9ShapeCommands(out, ['drawCircle', 3, 50, 50, 25], mapper);
46
+ expect(out).toEqual(['drawCircle', 3, 55, 60, 25]);
47
+ });
48
+
49
+ it('remaps drawEllipse corners and recomputes size', () => {
50
+ const mapper = { mapX: (x: number) => x * 2, mapY: (y: number) => y * 2 };
51
+ mapScale9ShapeCommands(out, ['drawEllipse', 4, 0, 0, 100, 50], mapper);
52
+ expect(out).toEqual(['drawEllipse', 4, 0, 0, 200, 100]);
53
+ });
54
+
55
+ it('returns a buffer with the same element count as the input', () => {
56
+ const shape = createScale9Shape(grid);
57
+ appendShapeBeginFill(shape, 0xff0000);
58
+ appendShapeRectangle(shape, 0, 0, 100, 100);
59
+ // A mapper is any pair of coordinate functions; building one from a grid is a renderer's job, and
60
+ // the rewrite under test does not care where the mapping came from.
61
+ mapScale9ShapeCommands(out, shape.data.commands, { mapX: (x) => x * 2, mapY: (y) => y * 2 });
62
+ expect(out).toHaveLength(shape.data.commands.length);
63
+ });
64
+ });
package/src/shape.test.ts CHANGED
@@ -4,9 +4,12 @@ import {
4
4
  getNodeLocalContentRevision,
5
5
  getNodeLocalTransformRevision,
6
6
  } from '@flighthq/node/contract';
7
+ import { createPath, createPathMorph } from '@flighthq/path/contract';
7
8
  import type { ShapeCommandToken } from '@flighthq/types/contract';
8
9
  import { ShapeKind } from '@flighthq/types/contract';
9
10
 
11
+ import { createMorphShape, setMorphShapeProgress } from './morphShape';
12
+ import { appendMorphShapeBeginFill } from './morphShapePaint';
10
13
  import {
11
14
  clearShapeCommands,
12
15
  computeShapeLocalBoundsRectangle,
@@ -169,6 +172,19 @@ describe('computeShapeLocalBoundsRectangle', () => {
169
172
  });
170
173
 
171
174
  describe('copyShapeCommands', () => {
175
+ it('discards target paint bindings when replacing a MorphShape command stream', () => {
176
+ const path = createPath();
177
+ const target = createMorphShape(createPathMorph(path, path)!);
178
+ appendMorphShapeBeginFill(target, { color: 0 }, { color: 0xffffff });
179
+ const source = createShape({ data: { commands: ['beginFill', 2, 0x123456, 1] } });
180
+
181
+ copyShapeCommands(target, source);
182
+ setMorphShapeProgress(target, 1);
183
+
184
+ expect(target.data.commands).toStrictEqual(['beginFill', 2, 0x123456, 1]);
185
+ expect(target.data.paintBindings).toStrictEqual([]);
186
+ });
187
+
172
188
  it('copies commands from source to target', () => {
173
189
  const source = createShape();
174
190
  source.data.commands.push('endFill', 0);
@@ -178,6 +194,25 @@ describe('copyShapeCommands', () => {
178
194
  expect(target.data.commands).toEqual(['endFill', 0]);
179
195
  });
180
196
 
197
+ it('copies a command stream longer than the engine argument limit', () => {
198
+ // Imported vector artwork reaches this length routinely, and a spread into push would pass one
199
+ // argument per command — so this is the size at which copying, not drawing, is what fails.
200
+ const source = createShape();
201
+ for (let i = 0; i < 400_000; i++) source.data.commands.push('lineTo', 2, i, i);
202
+ const target = createShape();
203
+
204
+ expect(() => copyShapeCommands(target, source)).not.toThrow();
205
+ expect(target.data.commands).toHaveLength(source.data.commands.length);
206
+ expect(target.data.commands[1_599_998]).toBe(399_999);
207
+ });
208
+
209
+ it('leaves the command stream intact when target and source are the same shape', () => {
210
+ const shape = createShape();
211
+ shape.data.commands.push('endFill', 0);
212
+ copyShapeCommands(shape, shape);
213
+ expect(shape.data.commands).toEqual(['endFill', 0]);
214
+ });
215
+
181
216
  it('replaces existing target commands and bumps the content revision', () => {
182
217
  const source = createShape();
183
218
  source.data.commands.push('endFill', 0);
@@ -4,7 +4,7 @@ import { createShape } from './shape';
4
4
  import {
5
5
  appendShapeArc,
6
6
  appendShapeArcTo,
7
- appendShapeBeginBitmapFill,
7
+ appendShapeBeginTextureFill,
8
8
  appendShapeBeginFill,
9
9
  appendShapeBeginGradientFill,
10
10
  appendShapeCircle,
@@ -13,7 +13,7 @@ import {
13
13
  appendShapeDrawTriangles,
14
14
  appendShapeEllipse,
15
15
  appendShapeEndFill,
16
- appendShapeLineBitmapStyle,
16
+ appendShapeLineTextureStyle,
17
17
  appendShapeLineGradientStyle,
18
18
  appendShapeLineStyle,
19
19
  appendShapeLineTo,
@@ -27,7 +27,7 @@ import {
27
27
  PathCommand,
28
28
  } from './shapeCommands';
29
29
 
30
- const fakeImageSource = { id: 1, height: 10, source: null, width: 10 } as never;
30
+ const fakeTexture = { id: 1 } as never;
31
31
  const fakeMatrix = { id: 2, a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 } as never;
32
32
 
33
33
  describe('appendShapeArc', () => {
@@ -124,20 +124,6 @@ describe('appendShapeArcTo', () => {
124
124
  });
125
125
  });
126
126
 
127
- describe('appendShapeBeginBitmapFill', () => {
128
- it('pushes a beginBitmapFill command with bitmap, matrix, repeat, smooth', () => {
129
- const shape = createShape();
130
- appendShapeBeginBitmapFill(shape, fakeImageSource, fakeMatrix, false, true);
131
- expect(shape.data.commands).toEqual(['beginBitmapFill', 4, fakeImageSource, fakeMatrix, false, true]);
132
- });
133
-
134
- it('defaults matrix to null, repeat to true, smooth to false', () => {
135
- const shape = createShape();
136
- appendShapeBeginBitmapFill(shape, fakeImageSource);
137
- expect(shape.data.commands).toEqual(['beginBitmapFill', 4, fakeImageSource, null, true, false]);
138
- });
139
- });
140
-
141
127
  describe('appendShapeBeginFill', () => {
142
128
  it('pushes a beginFill command with color and alpha', () => {
143
129
  const shape = createShape();
@@ -198,6 +184,20 @@ describe('appendShapeBeginGradientFill', () => {
198
184
  });
199
185
  });
200
186
 
187
+ describe('appendShapeBeginTextureFill', () => {
188
+ it('pushes a beginTextureFill command with texture and matrix', () => {
189
+ const shape = createShape();
190
+ appendShapeBeginTextureFill(shape, fakeTexture, fakeMatrix);
191
+ expect(shape.data.commands).toEqual(['beginTextureFill', 2, fakeTexture, fakeMatrix]);
192
+ });
193
+
194
+ it('defaults matrix to null', () => {
195
+ const shape = createShape();
196
+ appendShapeBeginTextureFill(shape, fakeTexture);
197
+ expect(shape.data.commands).toEqual(['beginTextureFill', 2, fakeTexture, null]);
198
+ });
199
+ });
200
+
201
201
  describe('appendShapeCircle', () => {
202
202
  it('pushes a drawCircle command with position and radius', () => {
203
203
  const shape = createShape();
@@ -293,20 +293,6 @@ describe('appendShapeEndFill', () => {
293
293
  });
294
294
  });
295
295
 
296
- describe('appendShapeLineBitmapStyle', () => {
297
- it('pushes a lineBitmapStyle command with bitmap, matrix, repeat, smooth', () => {
298
- const shape = createShape();
299
- appendShapeLineBitmapStyle(shape, fakeImageSource, fakeMatrix, false, true);
300
- expect(shape.data.commands).toEqual(['lineBitmapStyle', 4, fakeImageSource, fakeMatrix, false, true]);
301
- });
302
-
303
- it('defaults matrix to null, repeat to true, smooth to false', () => {
304
- const shape = createShape();
305
- appendShapeLineBitmapStyle(shape, fakeImageSource);
306
- expect(shape.data.commands).toEqual(['lineBitmapStyle', 4, fakeImageSource, null, true, false]);
307
- });
308
- });
309
-
310
296
  describe('appendShapeLineGradientStyle', () => {
311
297
  it('pushes a lineGradientStyle command with all fields', () => {
312
298
  const shape = createShape();
@@ -340,6 +326,20 @@ describe('appendShapeLineStyle', () => {
340
326
  });
341
327
  });
342
328
 
329
+ describe('appendShapeLineTextureStyle', () => {
330
+ it('pushes a lineTextureStyle command with texture and matrix', () => {
331
+ const shape = createShape();
332
+ appendShapeLineTextureStyle(shape, fakeTexture, fakeMatrix);
333
+ expect(shape.data.commands).toEqual(['lineTextureStyle', 2, fakeTexture, fakeMatrix]);
334
+ });
335
+
336
+ it('defaults matrix to null', () => {
337
+ const shape = createShape();
338
+ appendShapeLineTextureStyle(shape, fakeTexture);
339
+ expect(shape.data.commands).toEqual(['lineTextureStyle', 2, fakeTexture, null]);
340
+ });
341
+ });
342
+
343
343
  describe('appendShapeLineTo', () => {
344
344
  it('pushes a lineTo command with position', () => {
345
345
  const shape = createShape();
@@ -151,14 +151,17 @@ describe('getShapeFillRegions', () => {
151
151
  expect(getShapeFillRegions(shape.data.commands)).toBeNull();
152
152
  });
153
153
 
154
- it('returns null when a stroke is present', () => {
154
+ it('resolves solid fills independently of a solid stroke', () => {
155
155
  const shape = createShape();
156
156
  appendShapeLineStyle(shape, 2, 0x000000);
157
157
  appendShapeBeginFill(shape, 0xff0000);
158
158
  appendShapeRectangle(shape, 0, 0, 10, 10);
159
159
  appendShapeEndFill(shape);
160
160
 
161
- expect(getShapeFillRegions(shape.data.commands)).toBeNull();
161
+ const regions = getShapeFillRegions(shape.data.commands);
162
+ expect(regions).not.toBeNull();
163
+ expect(regions).toHaveLength(1);
164
+ expect(regions![0].color).toBe(0xff0000);
162
165
  });
163
166
  });
164
167
 
@@ -171,9 +174,12 @@ describe('hasNonSolidShapeFill', () => {
171
174
  expect(hasNonSolidShapeFill(shape.data.commands)).toBe(false);
172
175
  });
173
176
 
174
- it('is true when a bitmap or gradient fill or stroke is present', () => {
177
+ it('is false for a solid stroke and true for a bitmap or gradient style', () => {
175
178
  const shape = createShape();
176
179
  appendShapeLineStyle(shape, 1, 0);
180
+ expect(hasNonSolidShapeFill(shape.data.commands)).toBe(false);
181
+
182
+ appendShapeBeginGradientFill(shape, 'linear', [0xff0000, 0x0000ff], [1, 1], [0, 255]);
177
183
  expect(hasNonSolidShapeFill(shape.data.commands)).toBe(true);
178
184
  });
179
185
  });