@flighthq/shape 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@flighthq/shape",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/displayobject": "0.1.0",
31
+ "@flighthq/geometry": "0.1.0",
32
+ "@flighthq/node": "0.1.0",
33
+ "@flighthq/path": "0.1.0",
34
+ "@flighthq/types": "0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "typescript": "^5.3.0"
38
+ },
39
+ "description": "Vector Shape display node: drawing commands, scale-9 shapes, and shape hit testing",
40
+ "sideEffects": false
41
+ }
@@ -0,0 +1,64 @@
1
+ import { Scale9ShapeKind } from '@flighthq/types';
2
+
3
+ import {
4
+ createScale9Shape,
5
+ createScale9ShapeData,
6
+ createScale9ShapeRuntime,
7
+ getScale9ShapeRuntime,
8
+ } from './scale9Shape';
9
+
10
+ const grid = { x: 10, y: 10, width: 80, height: 80 };
11
+
12
+ describe('createScale9Shape', () => {
13
+ it('returns a shape with the given scale9Grid on its data', () => {
14
+ const shape = createScale9Shape(grid);
15
+ expect(shape.data.scale9Grid).toBe(grid);
16
+ });
17
+
18
+ it('initializes with an empty commands array', () => {
19
+ const shape = createScale9Shape(grid);
20
+ expect(shape.data.commands).toHaveLength(0);
21
+ });
22
+
23
+ it('has Scale9ShapeKind', () => {
24
+ const shape = createScale9Shape(grid);
25
+ expect(shape.kind).toStrictEqual(Scale9ShapeKind);
26
+ });
27
+
28
+ it('returns a new object each call', () => {
29
+ expect(createScale9Shape(grid)).not.toBe(createScale9Shape(grid));
30
+ });
31
+ });
32
+
33
+ describe('createScale9ShapeData', () => {
34
+ it('stores the scale9Grid reference', () => {
35
+ const data = createScale9ShapeData(grid);
36
+ expect(data.scale9Grid).toBe(grid);
37
+ });
38
+
39
+ it('returns an empty commands array', () => {
40
+ const data = createScale9ShapeData(grid);
41
+ expect(data.commands).toHaveLength(0);
42
+ });
43
+
44
+ it('uses provided commands when given', () => {
45
+ const commands = [{ key: 'endFill' as const, args: [] as const }];
46
+ const data = createScale9ShapeData(grid, { commands });
47
+ expect(data.commands).toBe(commands);
48
+ });
49
+ });
50
+
51
+ describe('createScale9ShapeRuntime', () => {
52
+ it('returns a non-null runtime', () => {
53
+ const runtime = createScale9ShapeRuntime();
54
+ expect(runtime).not.toBeNull();
55
+ });
56
+ });
57
+
58
+ describe('getScale9ShapeRuntime', () => {
59
+ it('returns the runtime for a Scale9Shape', () => {
60
+ const shape = createScale9Shape(grid);
61
+ const runtime = getScale9ShapeRuntime(shape);
62
+ expect(runtime).not.toBeNull();
63
+ });
64
+ });
@@ -0,0 +1,207 @@
1
+ import { createRectangle } from '@flighthq/geometry';
2
+ import { getNodeLocalBoundsRevision, getNodeLocalContentRevision } from '@flighthq/node';
3
+ import { ShapeKind } from '@flighthq/types';
4
+
5
+ import {
6
+ clearShapeCommands,
7
+ computeShapeLocalBoundsRectangle,
8
+ copyShapeCommands,
9
+ createShape,
10
+ createShapeData,
11
+ createShapeRuntime,
12
+ getShapeBounds,
13
+ getShapeCommandCount,
14
+ getShapeRuntime,
15
+ invalidateShapeGeometry,
16
+ isShapeEmpty,
17
+ } from './shape';
18
+
19
+ describe('clearShapeCommands', () => {
20
+ it('empties the commands array and bumps the content revision', () => {
21
+ const shape = createShape();
22
+ shape.data.commands.push('endFill', 0);
23
+ const content = getNodeLocalContentRevision(shape);
24
+ clearShapeCommands(shape);
25
+ expect(shape.data.commands).toHaveLength(0);
26
+ expect(getNodeLocalContentRevision(shape)).toBe(content + 1);
27
+ });
28
+ });
29
+
30
+ describe('computeShapeLocalBoundsRectangle', () => {
31
+ it('sets out to zero for an empty shape with no commands', () => {
32
+ const shape = createShape();
33
+ const out = createRectangle(1, 2, 3, 4);
34
+ computeShapeLocalBoundsRectangle(out, shape as any);
35
+ expect(out.x).toBe(0);
36
+ expect(out.y).toBe(0);
37
+ expect(out.width).toBe(0);
38
+ expect(out.height).toBe(0);
39
+ });
40
+
41
+ it('computes bounds from drawRectangle commands', () => {
42
+ const shape = createShape();
43
+ shape.data.commands.push('drawRectangle', 4, 10, 20, 100, 50);
44
+ const out = createRectangle();
45
+ computeShapeLocalBoundsRectangle(out, shape as any);
46
+ expect(out.x).toBe(10);
47
+ expect(out.y).toBe(20);
48
+ expect(out.width).toBe(100);
49
+ expect(out.height).toBe(50);
50
+ });
51
+
52
+ it('computes bounds from moveTo and lineTo commands', () => {
53
+ const shape = createShape();
54
+ shape.data.commands.push('moveTo', 2, 0, 0, 'lineTo', 2, 80, 60);
55
+ const out = createRectangle();
56
+ computeShapeLocalBoundsRectangle(out, shape as any);
57
+ expect(out.x).toBe(0);
58
+ expect(out.y).toBe(0);
59
+ expect(out.width).toBe(80);
60
+ expect(out.height).toBe(60);
61
+ });
62
+ });
63
+
64
+ describe('copyShapeCommands', () => {
65
+ it('copies commands from source to target', () => {
66
+ const source = createShape();
67
+ source.data.commands.push({ key: 'endFill', args: [] });
68
+ const target = createShape();
69
+ copyShapeCommands(target, source);
70
+ expect(target.data.commands).toHaveLength(1);
71
+ expect(target.data.commands[0]).toEqual({ key: 'endFill', args: [] });
72
+ });
73
+
74
+ it('replaces existing target commands and bumps the content revision', () => {
75
+ const source = createShape();
76
+ source.data.commands.push({ key: 'endFill', args: [] });
77
+ const target = createShape();
78
+ target.data.commands.push({ key: 'endFill', args: [] });
79
+ target.data.commands.push({ key: 'endFill', args: [] });
80
+ const content = getNodeLocalContentRevision(target);
81
+ copyShapeCommands(target, source);
82
+ expect(target.data.commands).toHaveLength(1);
83
+ expect(getNodeLocalContentRevision(target)).toBe(content + 1);
84
+ });
85
+
86
+ it('does not share the same array reference', () => {
87
+ const source = createShape();
88
+ const target = createShape();
89
+ copyShapeCommands(target, source);
90
+ expect(target.data.commands).not.toBe(source.data.commands);
91
+ });
92
+ });
93
+
94
+ describe('createShape', () => {
95
+ it('initializes with an empty commands array', () => {
96
+ const shape = createShape();
97
+ expect(shape.data.commands).toHaveLength(0);
98
+ expect(shape.kind).toStrictEqual(ShapeKind);
99
+ });
100
+
101
+ it('allows pre-defined commands', () => {
102
+ const commands = [{ key: 'endFill' as const, args: [] as const }];
103
+ const shape = createShape({ data: { commands } });
104
+ expect(shape.data.commands).toBe(commands);
105
+ });
106
+
107
+ it('returns a new object for better hidden-class performance', () => {
108
+ expect(createShape()).not.toBe(createShape());
109
+ });
110
+ });
111
+
112
+ describe('createShapeData', () => {
113
+ it('returns a ShapeData object with an empty commands array', () => {
114
+ const data = createShapeData();
115
+ expect(data.commands).toHaveLength(0);
116
+ });
117
+
118
+ it('returns a new object each call', () => {
119
+ expect(createShapeData()).not.toBe(createShapeData());
120
+ });
121
+ });
122
+
123
+ describe('createShapeRuntime', () => {
124
+ it('returns a non-null runtime', () => {
125
+ const runtime = createShapeRuntime();
126
+ expect(runtime).not.toBeNull();
127
+ });
128
+ });
129
+
130
+ describe('getShapeBounds', () => {
131
+ it('returns the same bounds as computeShapeLocalBoundsRectangle', () => {
132
+ const shape = createShape();
133
+ shape.data.commands.push('drawRectangle', 4, 5, 10, 200, 80);
134
+ const out1 = createRectangle();
135
+ const out2 = createRectangle();
136
+ computeShapeLocalBoundsRectangle(out1, shape);
137
+ getShapeBounds(out2, shape);
138
+ expect(out2.x).toBe(out1.x);
139
+ expect(out2.y).toBe(out1.y);
140
+ expect(out2.width).toBe(out1.width);
141
+ expect(out2.height).toBe(out1.height);
142
+ });
143
+
144
+ it('returns zero bounds for an empty shape', () => {
145
+ const shape = createShape();
146
+ const out = createRectangle(1, 2, 3, 4);
147
+ getShapeBounds(out, shape);
148
+ expect(out.x).toBe(0);
149
+ expect(out.y).toBe(0);
150
+ expect(out.width).toBe(0);
151
+ expect(out.height).toBe(0);
152
+ });
153
+ });
154
+
155
+ describe('getShapeCommandCount', () => {
156
+ it('returns 0 for an empty shape', () => {
157
+ const shape = createShape();
158
+ expect(getShapeCommandCount(shape)).toBe(0);
159
+ });
160
+
161
+ it('counts each command entry (not each flat array element)', () => {
162
+ const shape = createShape();
163
+ shape.data.commands.push('beginFill', 2, 0xff0000, 1);
164
+ shape.data.commands.push('drawRectangle', 4, 0, 0, 100, 100);
165
+ shape.data.commands.push('endFill', 0);
166
+ expect(getShapeCommandCount(shape)).toBe(3);
167
+ });
168
+ });
169
+
170
+ describe('getShapeRuntime', () => {
171
+ it('returns the runtime for a Shape', () => {
172
+ const shape = createShape();
173
+ const runtime = getShapeRuntime(shape);
174
+ expect(runtime).not.toBeNull();
175
+ });
176
+ });
177
+
178
+ describe('invalidateShapeGeometry', () => {
179
+ it('bumps both the content and local-bounds revisions', () => {
180
+ const shape = createShape();
181
+ const content = getNodeLocalContentRevision(shape);
182
+ const bounds = getNodeLocalBoundsRevision(shape);
183
+ invalidateShapeGeometry(shape);
184
+ expect(getNodeLocalContentRevision(shape)).toBe(content + 1);
185
+ expect(getNodeLocalBoundsRevision(shape)).toBe(bounds + 1);
186
+ });
187
+ });
188
+
189
+ describe('isShapeEmpty', () => {
190
+ it('returns true for a shape with no commands', () => {
191
+ const shape = createShape();
192
+ expect(isShapeEmpty(shape)).toBe(true);
193
+ });
194
+
195
+ it('returns false after any command is appended', () => {
196
+ const shape = createShape();
197
+ shape.data.commands.push('endFill', 0);
198
+ expect(isShapeEmpty(shape)).toBe(false);
199
+ });
200
+
201
+ it('returns true again after clearShapeCommands', () => {
202
+ const shape = createShape();
203
+ shape.data.commands.push('endFill', 0);
204
+ clearShapeCommands(shape);
205
+ expect(isShapeEmpty(shape)).toBe(true);
206
+ });
207
+ });