@flighthq/shape-formats 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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/shapeJson.d.ts +13 -0
- package/dist/shapeJson.d.ts.map +1 -0
- package/dist/shapeJson.js +157 -0
- package/dist/shapeJson.js.map +1 -0
- package/package.json +39 -0
- package/src/shapeJson.test.ts +164 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ImageResource, Shape } from '@flighthq/types';
|
|
2
|
+
export interface ShapeBitmapReference {
|
|
3
|
+
index: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ShapeJsonFormatOptions {
|
|
6
|
+
space?: number | string;
|
|
7
|
+
}
|
|
8
|
+
export interface ShapeJsonParseOptions {
|
|
9
|
+
resolveBitmap?: (reference: Readonly<ShapeBitmapReference>) => ImageResource | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function formatShapeJson(shape: Readonly<Shape>, options?: Readonly<ShapeJsonFormatOptions>): string;
|
|
12
|
+
export declare function parseShapeJson(text: string, options?: Readonly<ShapeJsonParseOptions>): Shape | null;
|
|
13
|
+
//# sourceMappingURL=shapeJson.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shapeJson.d.ts","sourceRoot":"","sources":["../src/shapeJson.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAO5D,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,sBAAsB;IAErC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB;IAIpC,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,oBAAoB,CAAC,KAAK,aAAa,GAAG,IAAI,CAAC;CACrF;AAMD,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GAAG,MAAM,CA+B1G;AAMD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,KAAK,GAAG,IAAI,CAqCpG"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
2
|
+
import { appendShapeBeginBitmapFill, appendShapeBeginFill, appendShapeBeginGradientFill, appendShapeCircle, appendShapeCubicCurveTo, appendShapeCurveTo, appendShapeDrawTriangles, appendShapeEllipse, appendShapeEndFill, appendShapeLineBitmapStyle, appendShapeLineGradientStyle, appendShapeLineStyle, appendShapeLineTo, appendShapeMoveTo, appendShapePath, appendShapeRectangle, appendShapeRoundRectangle, createShape, } from '@flighthq/shape';
|
|
3
|
+
// Serializes a shape's full drawing-command stream to a native JSON string that `parseShapeJson`
|
|
4
|
+
// restores losslessly. Every non-bitmap command round-trips exactly; `beginBitmapFill`/
|
|
5
|
+
// `lineBitmapStyle` resources serialize as an ordinal `ShapeBitmapReference` (see the type) rather
|
|
6
|
+
// than the live `ImageResource`.
|
|
7
|
+
export function formatShapeJson(shape, options) {
|
|
8
|
+
const commands = shape.data.commands;
|
|
9
|
+
const entries = [];
|
|
10
|
+
let bitmapOrdinal = 0;
|
|
11
|
+
let i = 0;
|
|
12
|
+
while (i < commands.length) {
|
|
13
|
+
const key = commands[i];
|
|
14
|
+
const argCount = commands[i + 1];
|
|
15
|
+
const base = i + 2;
|
|
16
|
+
const args = [];
|
|
17
|
+
for (let a = 0; a < argCount; a++) {
|
|
18
|
+
const value = commands[base + a];
|
|
19
|
+
if (value === null) {
|
|
20
|
+
// An omitted fill/line matrix, or absent triangle indices/uv data.
|
|
21
|
+
args.push(null);
|
|
22
|
+
}
|
|
23
|
+
else if (isMatrixValue(value)) {
|
|
24
|
+
args.push({ a: value.a, b: value.b, c: value.c, d: value.d, tx: value.tx, ty: value.ty });
|
|
25
|
+
}
|
|
26
|
+
else if (isSerializableScalarOrArray(value)) {
|
|
27
|
+
// Numbers, strings (enum keywords), booleans, and numeric arrays (colors/ratios, path
|
|
28
|
+
// data/commands, triangle vertices) serialize verbatim through JSON.
|
|
29
|
+
args.push(value);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// The only remaining object arg in the command registry is a live `ImageResource` bitmap;
|
|
33
|
+
// it has no serializable id, so it becomes an ordinal reference resolved on parse.
|
|
34
|
+
args.push({ bitmap: { index: bitmapOrdinal++ } });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
entries.push({ key, args });
|
|
38
|
+
i += argCount + 2;
|
|
39
|
+
}
|
|
40
|
+
return JSON.stringify({ shapeFormat: SHAPE_JSON_FORMAT, commands: entries }, null, options?.space);
|
|
41
|
+
}
|
|
42
|
+
// Restores a `Shape` from a `formatShapeJson` string by replaying each command through the matching
|
|
43
|
+
// `appendShape*` builder. Returns `null` for malformed JSON, a missing/mismatched `shapeFormat`
|
|
44
|
+
// version tag, a non-array `commands` field, a malformed command entry, an unknown command key, or a
|
|
45
|
+
// malformed argument. Bitmap-bearing commands whose reference cannot be resolved are dropped.
|
|
46
|
+
export function parseShapeJson(text, options) {
|
|
47
|
+
let root;
|
|
48
|
+
try {
|
|
49
|
+
root = JSON.parse(text);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (!isPlainObject(root))
|
|
55
|
+
return null;
|
|
56
|
+
if (root.shapeFormat !== SHAPE_JSON_FORMAT)
|
|
57
|
+
return null;
|
|
58
|
+
const rawCommands = root.commands;
|
|
59
|
+
if (!Array.isArray(rawCommands))
|
|
60
|
+
return null;
|
|
61
|
+
const resolveBitmap = options?.resolveBitmap;
|
|
62
|
+
const shape = createShape();
|
|
63
|
+
for (const entry of rawCommands) {
|
|
64
|
+
if (!isPlainObject(entry))
|
|
65
|
+
return null;
|
|
66
|
+
const key = entry.key;
|
|
67
|
+
const rawArgs = entry.args;
|
|
68
|
+
if (typeof key !== 'string' || !Array.isArray(rawArgs))
|
|
69
|
+
return null;
|
|
70
|
+
const appender = SHAPE_COMMAND_APPENDERS[key];
|
|
71
|
+
if (appender === undefined)
|
|
72
|
+
return null;
|
|
73
|
+
const args = [];
|
|
74
|
+
let drop = false;
|
|
75
|
+
for (const raw of rawArgs) {
|
|
76
|
+
const reconstructed = reconstructShapeCommandArg(raw, resolveBitmap);
|
|
77
|
+
if (reconstructed === MALFORMED_ARG)
|
|
78
|
+
return null;
|
|
79
|
+
if (reconstructed === DROP_COMMAND) {
|
|
80
|
+
drop = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
args.push(reconstructed);
|
|
84
|
+
}
|
|
85
|
+
if (drop)
|
|
86
|
+
continue;
|
|
87
|
+
appender(shape, ...args);
|
|
88
|
+
}
|
|
89
|
+
return shape;
|
|
90
|
+
}
|
|
91
|
+
// Rebuilds a single command argument from its serialized form. Returns `MALFORMED_ARG` for an
|
|
92
|
+
// unrecognized object shape and `DROP_COMMAND` when a bitmap reference cannot be resolved.
|
|
93
|
+
function reconstructShapeCommandArg(value, resolveBitmap) {
|
|
94
|
+
if (value === null)
|
|
95
|
+
return null;
|
|
96
|
+
const type = typeof value;
|
|
97
|
+
if (type === 'number' || type === 'string' || type === 'boolean')
|
|
98
|
+
return value;
|
|
99
|
+
if (Array.isArray(value))
|
|
100
|
+
return value;
|
|
101
|
+
if (!isPlainObject(value))
|
|
102
|
+
return MALFORMED_ARG;
|
|
103
|
+
if (isPlainObject(value.bitmap) && typeof value.bitmap.index === 'number') {
|
|
104
|
+
const resolved = resolveBitmap?.({ index: value.bitmap.index }) ?? null;
|
|
105
|
+
return resolved === null ? DROP_COMMAND : resolved;
|
|
106
|
+
}
|
|
107
|
+
if (typeof value.a === 'number' &&
|
|
108
|
+
typeof value.b === 'number' &&
|
|
109
|
+
typeof value.c === 'number' &&
|
|
110
|
+
typeof value.d === 'number' &&
|
|
111
|
+
typeof value.tx === 'number' &&
|
|
112
|
+
typeof value.ty === 'number') {
|
|
113
|
+
return createMatrix(value.a, value.b, value.c, value.d, value.tx, value.ty);
|
|
114
|
+
}
|
|
115
|
+
return MALFORMED_ARG;
|
|
116
|
+
}
|
|
117
|
+
function isMatrixValue(value) {
|
|
118
|
+
return (isPlainObject(value) &&
|
|
119
|
+
typeof value.a === 'number' &&
|
|
120
|
+
typeof value.b === 'number' &&
|
|
121
|
+
typeof value.c === 'number' &&
|
|
122
|
+
typeof value.d === 'number' &&
|
|
123
|
+
typeof value.tx === 'number' &&
|
|
124
|
+
typeof value.ty === 'number');
|
|
125
|
+
}
|
|
126
|
+
function isPlainObject(value) {
|
|
127
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
function isSerializableScalarOrArray(value) {
|
|
130
|
+
const type = typeof value;
|
|
131
|
+
return type === 'number' || type === 'string' || type === 'boolean' || Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
// Sentinel returned when a command argument is structurally invalid; parse aborts to `null`.
|
|
134
|
+
const MALFORMED_ARG = Symbol('shapeFormats.malformedArg');
|
|
135
|
+
// Sentinel returned when a bitmap reference cannot be resolved; the owning command is dropped.
|
|
136
|
+
const DROP_COMMAND = Symbol('shapeFormats.dropCommand');
|
|
137
|
+
const SHAPE_JSON_FORMAT = 1;
|
|
138
|
+
const SHAPE_COMMAND_APPENDERS = {
|
|
139
|
+
beginBitmapFill: appendShapeBeginBitmapFill,
|
|
140
|
+
beginFill: appendShapeBeginFill,
|
|
141
|
+
beginGradientFill: appendShapeBeginGradientFill,
|
|
142
|
+
cubicCurveTo: appendShapeCubicCurveTo,
|
|
143
|
+
curveTo: appendShapeCurveTo,
|
|
144
|
+
drawCircle: appendShapeCircle,
|
|
145
|
+
drawEllipse: appendShapeEllipse,
|
|
146
|
+
drawPath: appendShapePath,
|
|
147
|
+
drawRectangle: appendShapeRectangle,
|
|
148
|
+
drawRoundRectangle: appendShapeRoundRectangle,
|
|
149
|
+
drawTriangles: appendShapeDrawTriangles,
|
|
150
|
+
endFill: appendShapeEndFill,
|
|
151
|
+
lineBitmapStyle: appendShapeLineBitmapStyle,
|
|
152
|
+
lineGradientStyle: appendShapeLineGradientStyle,
|
|
153
|
+
lineStyle: appendShapeLineStyle,
|
|
154
|
+
lineTo: appendShapeLineTo,
|
|
155
|
+
moveTo: appendShapeMoveTo,
|
|
156
|
+
};
|
|
157
|
+
//# sourceMappingURL=shapeJson.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shapeJson.js","sourceRoot":"","sources":["../src/shapeJson.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/shape-formats",
|
|
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/geometry": "0.1.0",
|
|
31
|
+
"@flighthq/shape": "0.1.0",
|
|
32
|
+
"@flighthq/types": "0.1.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.3.0"
|
|
36
|
+
},
|
|
37
|
+
"description": "Tree-shakable shape serialization codecs for @flighthq/shape (native command-stream JSON)",
|
|
38
|
+
"sideEffects": false
|
|
39
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createMatrix } from '@flighthq/geometry';
|
|
2
|
+
import {
|
|
3
|
+
appendShapeBeginBitmapFill,
|
|
4
|
+
appendShapeBeginFill,
|
|
5
|
+
appendShapeBeginGradientFill,
|
|
6
|
+
appendShapeCubicCurveTo,
|
|
7
|
+
appendShapeCurveTo,
|
|
8
|
+
appendShapeEndFill,
|
|
9
|
+
appendShapeLineStyle,
|
|
10
|
+
appendShapeLineTo,
|
|
11
|
+
appendShapeMoveTo,
|
|
12
|
+
appendShapePath,
|
|
13
|
+
createShape,
|
|
14
|
+
getShapeCommandCount,
|
|
15
|
+
} from '@flighthq/shape';
|
|
16
|
+
import type { ImageResource } from '@flighthq/types';
|
|
17
|
+
|
|
18
|
+
import { formatShapeJson, parseShapeJson } from './shapeJson';
|
|
19
|
+
|
|
20
|
+
function createEveryNonBitmapCommandShape() {
|
|
21
|
+
const shape = createShape();
|
|
22
|
+
appendShapeBeginFill(shape, 0xff0000ff, 1);
|
|
23
|
+
appendShapeBeginGradientFill(
|
|
24
|
+
shape,
|
|
25
|
+
'linear',
|
|
26
|
+
[0xff0000ff, 0x0000ffff],
|
|
27
|
+
[1, 0.5],
|
|
28
|
+
[0, 255],
|
|
29
|
+
createMatrix(1, 0, 0, 1, 10, 20),
|
|
30
|
+
'pad',
|
|
31
|
+
'rgb',
|
|
32
|
+
0,
|
|
33
|
+
);
|
|
34
|
+
appendShapeLineStyle(shape, 2, 0x00ff00ff, 1, false, 'normal', 'round', 'miter', 3);
|
|
35
|
+
appendShapeMoveTo(shape, 0, 0);
|
|
36
|
+
appendShapeLineTo(shape, 100, 0);
|
|
37
|
+
appendShapeCurveTo(shape, 150, 50, 100, 100);
|
|
38
|
+
appendShapeCubicCurveTo(shape, 10, 10, 20, 20, 30, 30);
|
|
39
|
+
appendShapePath(shape, [1, 2], [0, 0, 50, 50], 'evenOdd');
|
|
40
|
+
appendShapeEndFill(shape);
|
|
41
|
+
return shape;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A minimal stand-in for a live ImageResource; the codec never inspects resource internals, only
|
|
45
|
+
// swaps the object for an ordinal reference and back.
|
|
46
|
+
function createFakeBitmap(): ImageResource {
|
|
47
|
+
return { width: 4, height: 4 } as unknown as ImageResource;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('formatShapeJson', () => {
|
|
51
|
+
it('wraps the command stream in a versioned top-level object', () => {
|
|
52
|
+
const shape = createShape();
|
|
53
|
+
appendShapeBeginFill(shape, 0x112233ff, 1);
|
|
54
|
+
const parsed = JSON.parse(formatShapeJson(shape));
|
|
55
|
+
expect(parsed.shapeFormat).toBe(1);
|
|
56
|
+
expect(Array.isArray(parsed.commands)).toBe(true);
|
|
57
|
+
expect(parsed.commands[0]).toEqual({ key: 'beginFill', args: [0x112233ff, 1] });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('serializes a matrix argument as its {a,b,c,d,tx,ty} fields', () => {
|
|
61
|
+
const shape = createShape();
|
|
62
|
+
appendShapeBeginGradientFill(shape, 'radial', [0], [1], [0], createMatrix(2, 0, 0, 3, 5, 7), 'pad', 'rgb', 0);
|
|
63
|
+
const parsed = JSON.parse(formatShapeJson(shape));
|
|
64
|
+
expect(parsed.commands[0].args[4]).toEqual({ a: 2, b: 0, c: 0, d: 3, tx: 5, ty: 7 });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('serializes a bitmap resource as an ordinal reference, never the resource', () => {
|
|
68
|
+
const shape = createShape();
|
|
69
|
+
appendShapeBeginBitmapFill(shape, createFakeBitmap(), null, true, false);
|
|
70
|
+
const parsed = JSON.parse(formatShapeJson(shape));
|
|
71
|
+
expect(parsed.commands[0].args[0]).toEqual({ bitmap: { index: 0 } });
|
|
72
|
+
expect(parsed.commands[0].args[1]).toBeNull();
|
|
73
|
+
expect(parsed.commands[0].args[2]).toBe(true);
|
|
74
|
+
expect(parsed.commands[0].args[3]).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('honors the space option for pretty-printing', () => {
|
|
78
|
+
const shape = createShape();
|
|
79
|
+
appendShapeEndFill(shape);
|
|
80
|
+
expect(formatShapeJson(shape, { space: 2 })).toContain('\n');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('parseShapeJson', () => {
|
|
85
|
+
it('round-trips every non-bitmap command losslessly', () => {
|
|
86
|
+
const shape = createEveryNonBitmapCommandShape();
|
|
87
|
+
const json = formatShapeJson(shape);
|
|
88
|
+
const restored = parseShapeJson(json);
|
|
89
|
+
expect(restored).not.toBeNull();
|
|
90
|
+
expect(getShapeCommandCount(restored!)).toBe(getShapeCommandCount(shape));
|
|
91
|
+
expect(formatShapeJson(restored!)).toBe(json);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns null for malformed JSON', () => {
|
|
95
|
+
expect(parseShapeJson('{ not json')).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns null for a missing version tag', () => {
|
|
99
|
+
expect(parseShapeJson(JSON.stringify({ commands: [] }))).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns null for a mismatched version tag', () => {
|
|
103
|
+
expect(parseShapeJson(JSON.stringify({ shapeFormat: 2, commands: [] }))).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns null when the top level is not an object', () => {
|
|
107
|
+
expect(parseShapeJson(JSON.stringify([]))).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns null for an unknown command key', () => {
|
|
111
|
+
const json = JSON.stringify({ shapeFormat: 1, commands: [{ key: 'notACommand', args: [] }] });
|
|
112
|
+
expect(parseShapeJson(json)).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('returns null for a malformed argument object', () => {
|
|
116
|
+
const json = JSON.stringify({ shapeFormat: 1, commands: [{ key: 'beginFill', args: [{ nonsense: true }, 1] }] });
|
|
117
|
+
expect(parseShapeJson(json)).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('round-trips a bitmap fill through the resolver', () => {
|
|
121
|
+
const bitmap = createFakeBitmap();
|
|
122
|
+
const shape = createShape();
|
|
123
|
+
appendShapeBeginBitmapFill(shape, bitmap, createMatrix(1, 0, 0, 1, 3, 4), true, false);
|
|
124
|
+
appendShapeMoveTo(shape, 5, 6);
|
|
125
|
+
|
|
126
|
+
const seen: number[] = [];
|
|
127
|
+
const restored = parseShapeJson(formatShapeJson(shape), {
|
|
128
|
+
resolveBitmap: (reference) => {
|
|
129
|
+
seen.push(reference.index);
|
|
130
|
+
return bitmap;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(restored).not.toBeNull();
|
|
135
|
+
expect(seen).toEqual([0]);
|
|
136
|
+
expect(getShapeCommandCount(restored!)).toBe(2);
|
|
137
|
+
expect(restored!.data.commands[0]).toBe('beginBitmapFill');
|
|
138
|
+
expect(restored!.data.commands[2]).toBe(bitmap);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('drops a bitmap fill when no resolver is supplied and keeps the rest intact', () => {
|
|
142
|
+
const shape = createShape();
|
|
143
|
+
appendShapeBeginFill(shape, 0xffffffff, 1);
|
|
144
|
+
appendShapeBeginBitmapFill(shape, createFakeBitmap(), null, true, false);
|
|
145
|
+
appendShapeLineTo(shape, 9, 9);
|
|
146
|
+
|
|
147
|
+
const restored = parseShapeJson(formatShapeJson(shape));
|
|
148
|
+
expect(restored).not.toBeNull();
|
|
149
|
+
expect(getShapeCommandCount(restored!)).toBe(2);
|
|
150
|
+
expect(restored!.data.commands[0]).toBe('beginFill');
|
|
151
|
+
expect(restored!.data.commands[4]).toBe('lineTo');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('drops a bitmap fill when the resolver returns null', () => {
|
|
155
|
+
const shape = createShape();
|
|
156
|
+
appendShapeBeginBitmapFill(shape, createFakeBitmap(), null, true, false);
|
|
157
|
+
appendShapeEndFill(shape);
|
|
158
|
+
|
|
159
|
+
const restored = parseShapeJson(formatShapeJson(shape), { resolveBitmap: () => null });
|
|
160
|
+
expect(restored).not.toBeNull();
|
|
161
|
+
expect(getShapeCommandCount(restored!)).toBe(1);
|
|
162
|
+
expect(restored!.data.commands[0]).toBe('endFill');
|
|
163
|
+
});
|
|
164
|
+
});
|