sketchmark 2.0.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/README.md +188 -0
- package/bin/sketchmark.cjs +2008 -0
- package/dist/src/builders/index.d.ts +74 -0
- package/dist/src/builders/index.js +230 -0
- package/dist/src/compounds.d.ts +13 -0
- package/dist/src/compounds.js +118 -0
- package/dist/src/deck.d.ts +4 -0
- package/dist/src/deck.js +91 -0
- package/dist/src/diagnostics.d.ts +5 -0
- package/dist/src/diagnostics.js +113 -0
- package/dist/src/export/index.d.ts +8 -0
- package/dist/src/export/index.js +15 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +35 -0
- package/dist/src/kernel.d.ts +8 -0
- package/dist/src/kernel.js +68 -0
- package/dist/src/normalize.d.ts +6 -0
- package/dist/src/normalize.js +191 -0
- package/dist/src/patch.d.ts +5 -0
- package/dist/src/patch.js +72 -0
- package/dist/src/path-sampling.d.ts +3 -0
- package/dist/src/path-sampling.js +275 -0
- package/dist/src/player/index.d.ts +68 -0
- package/dist/src/player/index.js +600 -0
- package/dist/src/project.d.ts +11 -0
- package/dist/src/project.js +107 -0
- package/dist/src/render/html.d.ts +2 -0
- package/dist/src/render/html.js +13 -0
- package/dist/src/render/raw-three.d.ts +7 -0
- package/dist/src/render/raw-three.js +17 -0
- package/dist/src/render/svg.d.ts +3 -0
- package/dist/src/render/svg.js +277 -0
- package/dist/src/render/three-html.d.ts +2 -0
- package/dist/src/render/three-html.js +303 -0
- package/dist/src/render/three-preview-svg.d.ts +3 -0
- package/dist/src/render/three-preview-svg.js +102 -0
- package/dist/src/scenes.d.ts +4 -0
- package/dist/src/scenes.js +25 -0
- package/dist/src/schema.d.ts +2 -0
- package/dist/src/schema.js +403 -0
- package/dist/src/sequences.d.ts +43 -0
- package/dist/src/sequences.js +109 -0
- package/dist/src/shapes/builtins.d.ts +2 -0
- package/dist/src/shapes/builtins.js +429 -0
- package/dist/src/shapes/common.d.ts +9 -0
- package/dist/src/shapes/common.js +75 -0
- package/dist/src/shapes/geometry.d.ts +22 -0
- package/dist/src/shapes/geometry.js +166 -0
- package/dist/src/shapes/index.d.ts +2 -0
- package/dist/src/shapes/index.js +18 -0
- package/dist/src/shapes/registry.d.ts +9 -0
- package/dist/src/shapes/registry.js +35 -0
- package/dist/src/shapes/types.d.ts +34 -0
- package/dist/src/shapes/types.js +2 -0
- package/dist/src/types.d.ts +439 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils.d.ts +25 -0
- package/dist/src/utils.js +157 -0
- package/dist/src/validate.d.ts +2 -0
- package/dist/src/validate.js +434 -0
- package/dist/tests/run.d.ts +1 -0
- package/dist/tests/run.js +651 -0
- package/package.json +52 -0
- package/schema/visual.schema.json +930 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateVisualDocument = validateVisualDocument;
|
|
4
|
+
const shapes_1 = require("./shapes");
|
|
5
|
+
const utils_1 = require("./utils");
|
|
6
|
+
const POSITION_ANIMATION_PROPERTIES = new Set(["positionX", "positionY", "positionZ"]);
|
|
7
|
+
function validateVisualDocument(document) {
|
|
8
|
+
const issues = [];
|
|
9
|
+
const warnings = [];
|
|
10
|
+
if (!document || typeof document !== "object") {
|
|
11
|
+
issues.push(issue("/", "invalid_document", "Document must be an object."));
|
|
12
|
+
return { ok: false, issues, warnings };
|
|
13
|
+
}
|
|
14
|
+
if (document.version !== 1)
|
|
15
|
+
issues.push(issue("/version", "invalid_version", "Document version must be 1."));
|
|
16
|
+
if (!document.canvas || typeof document.canvas !== "object") {
|
|
17
|
+
issues.push(issue("/canvas", "missing_canvas", "Document must define canvas."));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
if (!(0, utils_1.isFiniteNumber)(document.canvas.width))
|
|
21
|
+
issues.push(issue("/canvas/width", "missing_canvas_width", "Canvas width must be a number."));
|
|
22
|
+
if (!(0, utils_1.isFiniteNumber)(document.canvas.height))
|
|
23
|
+
issues.push(issue("/canvas/height", "missing_canvas_height", "Canvas height must be a number."));
|
|
24
|
+
if (document.canvas.background !== undefined && typeof document.canvas.background !== "string") {
|
|
25
|
+
issues.push(issue("/canvas/background", "invalid_canvas_background", "Canvas background must be a color string.", "For gradient, pattern, or image backgrounds, keep canvas.background as a fallback color string and add a full-canvas rect/image as the first element."));
|
|
26
|
+
}
|
|
27
|
+
if (document.canvas.space === "3d" && document.canvas.renderer !== "three") {
|
|
28
|
+
issues.push(issue("/canvas/renderer", "3d_requires_three", "3D documents must set canvas.renderer to 'three'."));
|
|
29
|
+
}
|
|
30
|
+
if (document.canvas.renderer === "three" && document.canvas.space !== "3d") {
|
|
31
|
+
issues.push(issue("/canvas/space", "three_requires_3d", "Three renderer requires canvas.space to be '3d'."));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const elements = document.elements ?? [];
|
|
35
|
+
if (document.elements !== undefined && !Array.isArray(elements))
|
|
36
|
+
issues.push(issue("/elements", "invalid_elements", "Document elements must be an array."));
|
|
37
|
+
const allElements = Array.isArray(elements) ? (0, utils_1.flattenElements)(elements) : [];
|
|
38
|
+
const ids = new Map();
|
|
39
|
+
for (const [index, element] of allElements.entries()) {
|
|
40
|
+
const path = `/elements/${index}`;
|
|
41
|
+
if (typeof element.id === "string") {
|
|
42
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(element.id)) {
|
|
43
|
+
issues.push(issue(`${path}/id`, "invalid_id", `Invalid id '${element.id}'. Use letters, numbers, '_' or '-'.`));
|
|
44
|
+
}
|
|
45
|
+
else if (ids.has(element.id)) {
|
|
46
|
+
issues.push(issue(`${path}/id`, "duplicate_id", `Duplicate element id '${element.id}'.`));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
ids.set(element.id, element);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const [index, element] of allElements.entries()) {
|
|
54
|
+
validateElement(element, `/elements/${index}`, document, ids, issues, warnings);
|
|
55
|
+
}
|
|
56
|
+
for (const [sceneId, scene] of Object.entries(document.scenes ?? {})) {
|
|
57
|
+
if (!Array.isArray(scene.elements))
|
|
58
|
+
issues.push(issue(`/scenes/${sceneId}/elements`, "invalid_scene_elements", `Scene '${sceneId}' elements must be an array.`));
|
|
59
|
+
const sceneDoc = { ...document, canvas: { ...document.canvas, ...(scene.canvas ?? {}) }, elements: scene.elements, scenes: undefined, sequences: undefined };
|
|
60
|
+
const sceneResult = validateVisualDocument(sceneDoc);
|
|
61
|
+
issues.push(...sceneResult.issues.map((item) => ({ ...item, path: `/scenes/${sceneId}${item.path}` })));
|
|
62
|
+
warnings.push(...sceneResult.warnings.map((item) => ({ ...item, path: `/scenes/${sceneId}${item.path}` })));
|
|
63
|
+
}
|
|
64
|
+
for (const [sequenceId, sequence] of Object.entries(document.sequences ?? {})) {
|
|
65
|
+
if (!Array.isArray(sequence.clips))
|
|
66
|
+
issues.push(issue(`/sequences/${sequenceId}/clips`, "invalid_sequence_clips", `Sequence '${sequenceId}' clips must be an array.`));
|
|
67
|
+
for (const [index, clip] of (sequence.clips ?? []).entries()) {
|
|
68
|
+
if (!document.scenes?.[clip.scene])
|
|
69
|
+
issues.push(issue(`/sequences/${sequenceId}/clips/${index}/scene`, "unknown_sequence_scene", `Unknown scene '${clip.scene}' in sequence '${sequenceId}'.`));
|
|
70
|
+
if (!(0, utils_1.isFiniteNumber)(clip.duration) || clip.duration <= 0)
|
|
71
|
+
issues.push(issue(`/sequences/${sequenceId}/clips/${index}/duration`, "invalid_clip_duration", "Clip duration must be a positive number."));
|
|
72
|
+
validateTransition(clip.transition, `/sequences/${sequenceId}/clips/${index}/transition`, issues);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { ok: issues.length === 0, issues, warnings };
|
|
76
|
+
}
|
|
77
|
+
function validateTransition(value, path, issues) {
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
return;
|
|
80
|
+
if (value === "cut" || value === "fade")
|
|
81
|
+
return;
|
|
82
|
+
if (!value || typeof value !== "object") {
|
|
83
|
+
issues.push(issue(path, "invalid_transition", "Transition must be 'cut', 'fade', or an object with type/duration."));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const transition = value;
|
|
87
|
+
if (transition.type !== "cut" && transition.type !== "fade") {
|
|
88
|
+
issues.push(issue(`${path}/type`, "invalid_transition_type", "Transition type must be 'cut' or 'fade'."));
|
|
89
|
+
}
|
|
90
|
+
if (transition.duration !== undefined && (!(0, utils_1.isFiniteNumber)(transition.duration) || transition.duration < 0)) {
|
|
91
|
+
issues.push(issue(`${path}/duration`, "invalid_transition_duration", "Transition duration must be a non-negative number."));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function validateElement(element, path, document, ids, issues, warnings) {
|
|
95
|
+
if (!element || typeof element !== "object") {
|
|
96
|
+
issues.push(issue(path, "invalid_element", "Element must be an object."));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const type = String(element.type || "");
|
|
100
|
+
if (utils_1.COMPOUND_TYPES.has(type)) {
|
|
101
|
+
issues.push(issue(`${path}/type`, "compound_type_not_allowed", `Compound type '${type}' is not valid in canonical JSON. Use builders to expand it into primitives first.`));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const definition = (0, shapes_1.getInternalShapeDefinition)(type);
|
|
105
|
+
if (!definition) {
|
|
106
|
+
issues.push(issue(`${path}/type`, "unsupported_type", `Unsupported primitive type '${type}'.`));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (definition.kind === "3d" && (document.canvas.space !== "3d" || document.canvas.renderer !== "three")) {
|
|
110
|
+
issues.push(issue(path, "three_primitive_requires_three_canvas", `Primitive '${type}' requires canvas.space '3d' and canvas.renderer 'three'.`));
|
|
111
|
+
}
|
|
112
|
+
const context = createShapeValidationContext(document, path, ids, issues, warnings);
|
|
113
|
+
definition.validateGeometry(element, context);
|
|
114
|
+
validateStyle(element, path, issues);
|
|
115
|
+
validateAnimation(element, path, definition, issues);
|
|
116
|
+
definition.validateReferences?.(element, context);
|
|
117
|
+
definition.validateWarnings?.(element, context);
|
|
118
|
+
}
|
|
119
|
+
function createShapeValidationContext(document, path, ids, issues, warnings) {
|
|
120
|
+
return {
|
|
121
|
+
document,
|
|
122
|
+
path,
|
|
123
|
+
ids,
|
|
124
|
+
issues,
|
|
125
|
+
warnings,
|
|
126
|
+
addIssue(issuePath, code, message, suggestion) {
|
|
127
|
+
issues.push(issue(issuePath, code, message, suggestion));
|
|
128
|
+
},
|
|
129
|
+
addWarning(warningPath, code, message, suggestion) {
|
|
130
|
+
warnings.push(warning(warningPath, code, message, suggestion));
|
|
131
|
+
},
|
|
132
|
+
requireNumber(value, numberPath) {
|
|
133
|
+
requireNumber(value, numberPath, issues);
|
|
134
|
+
},
|
|
135
|
+
requirePoint2(value, pointPath, code, message) {
|
|
136
|
+
if (!(0, utils_1.isPoint2)(value))
|
|
137
|
+
issues.push(issue(pointPath, code, message));
|
|
138
|
+
},
|
|
139
|
+
requirePoint2Array(value, minLength, pointPath, code, message) {
|
|
140
|
+
if (!(0, utils_1.isPoint2Array)(value, minLength))
|
|
141
|
+
issues.push(issue(pointPath, code, message));
|
|
142
|
+
},
|
|
143
|
+
requirePoint3(value, pointPath, code, message) {
|
|
144
|
+
if (!isPoint3(value))
|
|
145
|
+
issues.push(issue(pointPath, code, message));
|
|
146
|
+
},
|
|
147
|
+
validateEndpoint(value, endpointPath) {
|
|
148
|
+
validateEndpoint(value, endpointPath, ids, issues);
|
|
149
|
+
},
|
|
150
|
+
validateImageOptions(element) {
|
|
151
|
+
validateImageOptions(element, path, issues);
|
|
152
|
+
},
|
|
153
|
+
isFollowable(type) {
|
|
154
|
+
return (0, shapes_1.isFollowableAuthoringShape)(type);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function validateStyle(element, path, issues) {
|
|
159
|
+
validatePaint(element.fill, `${path}/fill`, issues);
|
|
160
|
+
validatePaint(element.stroke, `${path}/stroke`, issues);
|
|
161
|
+
if (element.strokeCap !== undefined && !["butt", "round", "square"].includes(String(element.strokeCap))) {
|
|
162
|
+
issues.push(issue(`${path}/strokeCap`, "invalid_stroke_cap", "strokeCap must be 'butt', 'round', or 'square'."));
|
|
163
|
+
}
|
|
164
|
+
if (element.strokeJoin !== undefined && !["miter", "round", "bevel"].includes(String(element.strokeJoin))) {
|
|
165
|
+
issues.push(issue(`${path}/strokeJoin`, "invalid_stroke_join", "strokeJoin must be 'miter', 'round', or 'bevel'."));
|
|
166
|
+
}
|
|
167
|
+
if (element.miterLimit !== undefined && !(0, utils_1.isFiniteNumber)(element.miterLimit))
|
|
168
|
+
issues.push(issue(`${path}/miterLimit`, "invalid_miter_limit", "miterLimit must be a finite number."));
|
|
169
|
+
if (element.dashOffset !== undefined && !(0, utils_1.isFiniteNumber)(element.dashOffset))
|
|
170
|
+
issues.push(issue(`${path}/dashOffset`, "invalid_dash_offset", "dashOffset must be a finite number."));
|
|
171
|
+
if (element.drawStart !== undefined && (!(0, utils_1.isFiniteNumber)(element.drawStart) || element.drawStart < 0 || element.drawStart > 1)) {
|
|
172
|
+
issues.push(issue(`${path}/drawStart`, "invalid_draw_start", "drawStart must be a number between 0 and 1."));
|
|
173
|
+
}
|
|
174
|
+
if (element.drawEnd !== undefined && (!(0, utils_1.isFiniteNumber)(element.drawEnd) || element.drawEnd < 0 || element.drawEnd > 1)) {
|
|
175
|
+
issues.push(issue(`${path}/drawEnd`, "invalid_draw_end", "drawEnd must be a number between 0 and 1."));
|
|
176
|
+
}
|
|
177
|
+
if ((0, utils_1.isFiniteNumber)(element.drawStart) && (0, utils_1.isFiniteNumber)(element.drawEnd) && element.drawStart > element.drawEnd) {
|
|
178
|
+
issues.push(issue(`${path}/drawStart`, "invalid_draw_range", "drawStart must be less than or equal to drawEnd."));
|
|
179
|
+
}
|
|
180
|
+
if (element.rotation !== undefined && !(0, utils_1.isFiniteNumber)(element.rotation))
|
|
181
|
+
issues.push(issue(`${path}/rotation`, "invalid_rotation", "rotation must be a finite number in degrees."));
|
|
182
|
+
if (element.scale !== undefined && !(0, utils_1.isFiniteNumber)(element.scale))
|
|
183
|
+
issues.push(issue(`${path}/scale`, "invalid_scale", "scale must be a finite number."));
|
|
184
|
+
if (element.scaleX !== undefined && !(0, utils_1.isFiniteNumber)(element.scaleX))
|
|
185
|
+
issues.push(issue(`${path}/scaleX`, "invalid_scale_x", "scaleX must be a finite number."));
|
|
186
|
+
if (element.scaleY !== undefined && !(0, utils_1.isFiniteNumber)(element.scaleY))
|
|
187
|
+
issues.push(issue(`${path}/scaleY`, "invalid_scale_y", "scaleY must be a finite number."));
|
|
188
|
+
if (element.origin !== undefined && !(typeof element.origin === "string" && utils_1.ANCHORS.has(element.origin)) && !(0, utils_1.isPoint2)(element.origin)) {
|
|
189
|
+
issues.push(issue(`${path}/origin`, "invalid_origin", "origin must be an anchor name or [x,y]."));
|
|
190
|
+
}
|
|
191
|
+
validateEffects(element.effects, `${path}/effects`, issues);
|
|
192
|
+
validateClip(element.clip, `${path}/clip`, issues);
|
|
193
|
+
validateMask(element.mask, `${path}/mask`, issues);
|
|
194
|
+
}
|
|
195
|
+
function validatePaint(value, path, issues) {
|
|
196
|
+
if (value === undefined || typeof value === "string")
|
|
197
|
+
return;
|
|
198
|
+
if (!value || typeof value !== "object") {
|
|
199
|
+
issues.push(issue(path, "invalid_paint", "Paint must be a color string or a structured gradient object."));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (value.type === "linearGradient") {
|
|
203
|
+
if (!(0, utils_1.isPoint2)(value.from))
|
|
204
|
+
issues.push(issue(`${path}/from`, "invalid_gradient_from", "Linear gradient from must be [x,y]."));
|
|
205
|
+
if (!(0, utils_1.isPoint2)(value.to))
|
|
206
|
+
issues.push(issue(`${path}/to`, "invalid_gradient_to", "Linear gradient to must be [x,y]."));
|
|
207
|
+
validateGradientStops(value.stops, `${path}/stops`, issues);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (value.type === "radialGradient") {
|
|
211
|
+
if (!(0, utils_1.isPoint2)(value.center))
|
|
212
|
+
issues.push(issue(`${path}/center`, "invalid_gradient_center", "Radial gradient center must be [x,y]."));
|
|
213
|
+
if (!(0, utils_1.isFiniteNumber)(value.radius) || value.radius < 0)
|
|
214
|
+
issues.push(issue(`${path}/radius`, "invalid_gradient_radius", "Radial gradient radius must be a non-negative number."));
|
|
215
|
+
if (value.focus !== undefined && !(0, utils_1.isPoint2)(value.focus))
|
|
216
|
+
issues.push(issue(`${path}/focus`, "invalid_gradient_focus", "Radial gradient focus must be [x,y]."));
|
|
217
|
+
validateGradientStops(value.stops, `${path}/stops`, issues);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (value.type === "pattern") {
|
|
221
|
+
if (typeof value.src !== "string" || !value.src)
|
|
222
|
+
issues.push(issue(`${path}/src`, "invalid_pattern_src", "Pattern src must be a string."));
|
|
223
|
+
if (!(0, utils_1.isFiniteNumber)(value.width) || value.width <= 0)
|
|
224
|
+
issues.push(issue(`${path}/width`, "invalid_pattern_width", "Pattern width must be a positive number."));
|
|
225
|
+
if (!(0, utils_1.isFiniteNumber)(value.height) || value.height <= 0)
|
|
226
|
+
issues.push(issue(`${path}/height`, "invalid_pattern_height", "Pattern height must be a positive number."));
|
|
227
|
+
if (value.x !== undefined && !(0, utils_1.isFiniteNumber)(value.x))
|
|
228
|
+
issues.push(issue(`${path}/x`, "invalid_pattern_x", "Pattern x must be a finite number."));
|
|
229
|
+
if (value.y !== undefined && !(0, utils_1.isFiniteNumber)(value.y))
|
|
230
|
+
issues.push(issue(`${path}/y`, "invalid_pattern_y", "Pattern y must be a finite number."));
|
|
231
|
+
validateImageFit(value.fit, `${path}/fit`, issues);
|
|
232
|
+
if (value.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(value.opacity) || value.opacity < 0 || value.opacity > 1)) {
|
|
233
|
+
issues.push(issue(`${path}/opacity`, "invalid_pattern_opacity", "Pattern opacity must be between 0 and 1."));
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
issues.push(issue(`${path}/type`, "invalid_paint_type", "Paint object type must be 'linearGradient', 'radialGradient', or 'pattern'."));
|
|
238
|
+
}
|
|
239
|
+
function validateGradientStops(value, path, issues) {
|
|
240
|
+
if (!Array.isArray(value) || value.length < 2) {
|
|
241
|
+
issues.push(issue(path, "invalid_gradient_stops", "Gradient stops must include at least two stops."));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
for (const [index, stop] of value.entries()) {
|
|
245
|
+
const offset = Array.isArray(stop) ? stop[0] : typeof stop === "object" && stop ? stop.offset : undefined;
|
|
246
|
+
const color = Array.isArray(stop) ? stop[1] : typeof stop === "object" && stop ? stop.color : undefined;
|
|
247
|
+
if (!(0, utils_1.isFiniteNumber)(offset) || offset < 0 || offset > 1)
|
|
248
|
+
issues.push(issue(`${path}/${index}/offset`, "invalid_gradient_stop_offset", "Gradient stop offset must be between 0 and 1."));
|
|
249
|
+
if (typeof color !== "string")
|
|
250
|
+
issues.push(issue(`${path}/${index}/color`, "invalid_gradient_stop_color", "Gradient stop color must be a string."));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function validateEffects(value, path, issues) {
|
|
254
|
+
if (value === undefined)
|
|
255
|
+
return;
|
|
256
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
257
|
+
issues.push(issue(path, "invalid_effects", "effects must be an object."));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
for (const key of ["blur", "brightness", "contrast", "saturate", "hueRotate"]) {
|
|
261
|
+
const effectValue = value[key];
|
|
262
|
+
if (effectValue !== undefined && !(0, utils_1.isFiniteNumber)(effectValue))
|
|
263
|
+
issues.push(issue(`${path}/${key}`, "invalid_effect_value", `${key} must be a finite number.`));
|
|
264
|
+
}
|
|
265
|
+
if (value.shadow !== undefined) {
|
|
266
|
+
const shadow = value.shadow;
|
|
267
|
+
if (!shadow || typeof shadow !== "object" || Array.isArray(shadow)) {
|
|
268
|
+
issues.push(issue(`${path}/shadow`, "invalid_shadow", "shadow must be an object."));
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
requireNumber(shadow.dx, `${path}/shadow/dx`, issues);
|
|
272
|
+
requireNumber(shadow.dy, `${path}/shadow/dy`, issues);
|
|
273
|
+
requireNumber(shadow.blur, `${path}/shadow/blur`, issues);
|
|
274
|
+
if (typeof shadow.color !== "string")
|
|
275
|
+
issues.push(issue(`${path}/shadow/color`, "invalid_shadow_color", "shadow color must be a string."));
|
|
276
|
+
if (shadow.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(shadow.opacity) || shadow.opacity < 0 || shadow.opacity > 1)) {
|
|
277
|
+
issues.push(issue(`${path}/shadow/opacity`, "invalid_shadow_opacity", "shadow opacity must be between 0 and 1."));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function validateClip(value, path, issues) {
|
|
283
|
+
validateShape(value, path, "clip", issues);
|
|
284
|
+
}
|
|
285
|
+
function validateMask(value, path, issues) {
|
|
286
|
+
validateShape(value, path, "mask", issues);
|
|
287
|
+
if (value && typeof value === "object" && value.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(value.opacity) || value.opacity < 0 || value.opacity > 1)) {
|
|
288
|
+
issues.push(issue(`${path}/opacity`, "invalid_mask_opacity", "mask opacity must be between 0 and 1."));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function validateShape(value, path, label, issues) {
|
|
292
|
+
if (value === undefined)
|
|
293
|
+
return;
|
|
294
|
+
if (!value || typeof value !== "object") {
|
|
295
|
+
issues.push(issue(path, `invalid_${label}`, `${label} must be a rect, circle, or path object.`));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (value.type === "rect") {
|
|
299
|
+
requireNumber(value.x, `${path}/x`, issues);
|
|
300
|
+
requireNumber(value.y, `${path}/y`, issues);
|
|
301
|
+
requireNumber(value.width, `${path}/width`, issues);
|
|
302
|
+
requireNumber(value.height, `${path}/height`, issues);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (value.type === "circle") {
|
|
306
|
+
requireNumber(value.cx, `${path}/cx`, issues);
|
|
307
|
+
requireNumber(value.cy, `${path}/cy`, issues);
|
|
308
|
+
requireNumber(value.radius, `${path}/radius`, issues);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (value.type === "path") {
|
|
312
|
+
if (typeof value.d !== "string" || !value.d.trim())
|
|
313
|
+
issues.push(issue(`${path}/d`, `missing_${label}_path`, `Path ${label} requires d.`));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
issues.push(issue(`${path}/type`, `invalid_${label}_type`, `${label} type must be 'rect', 'circle', or 'path'.`));
|
|
317
|
+
}
|
|
318
|
+
function validateImageOptions(element, path, issues) {
|
|
319
|
+
validateImageFit(element.fit, `${path}/fit`, issues);
|
|
320
|
+
if (!element.source)
|
|
321
|
+
return;
|
|
322
|
+
requireNumber(element.source.x, `${path}/source/x`, issues);
|
|
323
|
+
requireNumber(element.source.y, `${path}/source/y`, issues);
|
|
324
|
+
requireNumber(element.source.width, `${path}/source/width`, issues);
|
|
325
|
+
requireNumber(element.source.height, `${path}/source/height`, issues);
|
|
326
|
+
requireNumber(element.source.imageWidth, `${path}/source/imageWidth`, issues);
|
|
327
|
+
requireNumber(element.source.imageHeight, `${path}/source/imageHeight`, issues);
|
|
328
|
+
if ((0, utils_1.isFiniteNumber)(element.source.width) && element.source.width <= 0)
|
|
329
|
+
issues.push(issue(`${path}/source/width`, "invalid_source_width", "Image source width must be positive."));
|
|
330
|
+
if ((0, utils_1.isFiniteNumber)(element.source.height) && element.source.height <= 0)
|
|
331
|
+
issues.push(issue(`${path}/source/height`, "invalid_source_height", "Image source height must be positive."));
|
|
332
|
+
if ((0, utils_1.isFiniteNumber)(element.source.imageWidth) && element.source.imageWidth <= 0)
|
|
333
|
+
issues.push(issue(`${path}/source/imageWidth`, "invalid_source_image_width", "Image source imageWidth must be positive."));
|
|
334
|
+
if ((0, utils_1.isFiniteNumber)(element.source.imageHeight) && element.source.imageHeight <= 0)
|
|
335
|
+
issues.push(issue(`${path}/source/imageHeight`, "invalid_source_image_height", "Image source imageHeight must be positive."));
|
|
336
|
+
}
|
|
337
|
+
function validateImageFit(value, path, issues) {
|
|
338
|
+
if (value !== undefined && value !== "fill" && value !== "contain" && value !== "cover") {
|
|
339
|
+
issues.push(issue(path, "invalid_image_fit", "fit must be 'fill', 'contain', or 'cover'."));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function validateAnimation(element, path, definition, issues) {
|
|
343
|
+
if (!element.animate)
|
|
344
|
+
return;
|
|
345
|
+
const animatable = new Set(definition.animatable);
|
|
346
|
+
for (const [property, animation] of Object.entries(element.animate)) {
|
|
347
|
+
if (!animatable.has(property)) {
|
|
348
|
+
issues.push(issue(`${path}/animate/${property}`, "unsupported_animation_property", `Property '${property}' is not animatable in the primitive JSON core.`));
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const hasStaticValue = property in element || (POSITION_ANIMATION_PROPERTIES.has(property) && "position" in element);
|
|
352
|
+
if (!hasStaticValue && property !== "opacity") {
|
|
353
|
+
issues.push(issue(`${path}/animate/${property}`, "missing_static_animation_property", `Animated property '${property}' must also have a static value.`));
|
|
354
|
+
}
|
|
355
|
+
if (!animation || typeof animation !== "object") {
|
|
356
|
+
issues.push(issue(`${path}/animate/${property}`, "invalid_animation", "Animation must be an object with from/to or keyframes."));
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (Array.isArray(animation)) {
|
|
360
|
+
issues.push(issue(`${path}/animate/${property}`, "invalid_animation", "Animation must be an object. Use { keyframes: [...] }, not a raw keyframe array."));
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const values = [animation.from, animation.to].filter((value) => value !== undefined);
|
|
364
|
+
let hasTimeline = animation.from !== undefined || animation.to !== undefined;
|
|
365
|
+
if (animation.duration !== undefined && !(0, utils_1.isFiniteNumber)(animation.duration)) {
|
|
366
|
+
issues.push(issue(`${path}/animate/${property}/duration`, "invalid_animation_duration", "Animation duration must be a finite number."));
|
|
367
|
+
}
|
|
368
|
+
if (animation.delay !== undefined && !(0, utils_1.isFiniteNumber)(animation.delay)) {
|
|
369
|
+
issues.push(issue(`${path}/animate/${property}/delay`, "invalid_animation_delay", "Animation delay must be a finite number."));
|
|
370
|
+
}
|
|
371
|
+
if (animation.keyframes !== undefined) {
|
|
372
|
+
if (!Array.isArray(animation.keyframes)) {
|
|
373
|
+
issues.push(issue(`${path}/animate/${property}/keyframes`, "invalid_animation_keyframes", "Animation keyframes must be an array of [time,value] pairs."));
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
hasTimeline = hasTimeline || animation.keyframes.length > 0;
|
|
377
|
+
for (const [index, frame] of animation.keyframes.entries()) {
|
|
378
|
+
if (!Array.isArray(frame) || frame.length !== 2) {
|
|
379
|
+
issues.push(issue(`${path}/animate/${property}/keyframes/${index}`, "invalid_animation_keyframe", "Animation keyframe must be [time,value]."));
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (!(0, utils_1.isFiniteNumber)(frame[0])) {
|
|
383
|
+
issues.push(issue(`${path}/animate/${property}/keyframes/${index}/0`, "invalid_animation_keyframe_time", "Animation keyframe time must be a finite number of seconds."));
|
|
384
|
+
}
|
|
385
|
+
values.push(frame[1]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!hasTimeline) {
|
|
390
|
+
issues.push(issue(`${path}/animate/${property}`, "invalid_animation", "Animation must define from/to or keyframes."));
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
for (const value of values) {
|
|
394
|
+
if ((property === "fill" || property === "stroke") && typeof value === "string")
|
|
395
|
+
continue;
|
|
396
|
+
if (!(0, utils_1.isFiniteNumber)(value)) {
|
|
397
|
+
issues.push(issue(`${path}/animate/${property}`, "invalid_animation_value", `Animation values for '${property}' must be finite numbers${property === "fill" || property === "stroke" ? " or color strings" : ""}.`));
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function validateEndpoint(value, path, ids, issues) {
|
|
404
|
+
if ((0, utils_1.isPoint2)(value))
|
|
405
|
+
return;
|
|
406
|
+
if (typeof value !== "string") {
|
|
407
|
+
issues.push(issue(path, "invalid_endpoint", "Endpoint must be [x,y] or a reference like 'box.right'."));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const { id, anchor } = (0, utils_1.parseReference)(value);
|
|
411
|
+
const target = ids.get(id);
|
|
412
|
+
if (!target) {
|
|
413
|
+
issues.push(issue(path, "unknown_reference", `Unknown reference '${value}'.`));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (!utils_1.ANCHORS.has(anchor))
|
|
417
|
+
issues.push(issue(path, "invalid_anchor", `Unknown anchor '${anchor}'.`));
|
|
418
|
+
if (!(0, utils_1.elementBox)(target)) {
|
|
419
|
+
issues.push(issue(path, "unresolvable_reference", `Reference '${value}' points to an element without a resolvable 2D box.`));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function requireNumber(value, path, issues) {
|
|
423
|
+
if (!(0, utils_1.isFiniteNumber)(value))
|
|
424
|
+
issues.push(issue(path, "missing_number", "A finite number is required."));
|
|
425
|
+
}
|
|
426
|
+
function isPoint3(value) {
|
|
427
|
+
return Array.isArray(value) && value.length === 3 && (0, utils_1.isFiniteNumber)(value[0]) && (0, utils_1.isFiniteNumber)(value[1]) && (0, utils_1.isFiniteNumber)(value[2]);
|
|
428
|
+
}
|
|
429
|
+
function issue(path, code, message, suggestion) {
|
|
430
|
+
return { path, code, message, ...(suggestion ? { suggestion } : {}) };
|
|
431
|
+
}
|
|
432
|
+
function warning(path, code, message, suggestion) {
|
|
433
|
+
return { path, code, message, ...(suggestion ? { suggestion } : {}) };
|
|
434
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|