sketchmark 2.0.0 → 2.1.1

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 (56) hide show
  1. package/README.md +274 -188
  2. package/bin/editor-ui.cjs +2285 -0
  3. package/bin/preview-ui.cjs +74 -0
  4. package/bin/sketchmark.cjs +648 -2008
  5. package/dist/src/animatable.d.ts +21 -0
  6. package/dist/src/animatable.js +439 -0
  7. package/dist/src/builders/index.d.ts +1 -11
  8. package/dist/src/builders/index.js +1 -19
  9. package/dist/src/diagnostics.js +1 -64
  10. package/dist/src/edit.d.ts +27 -0
  11. package/dist/src/edit.js +162 -0
  12. package/dist/src/index.d.ts +4 -13
  13. package/dist/src/index.js +4 -13
  14. package/dist/src/keyframes.d.ts +48 -0
  15. package/dist/src/keyframes.js +182 -0
  16. package/dist/src/motion.d.ts +4 -0
  17. package/dist/src/motion.js +262 -0
  18. package/dist/src/normalize.js +120 -151
  19. package/dist/src/presets/characters.d.ts +15 -0
  20. package/dist/src/presets/characters.js +113 -0
  21. package/dist/src/presets/compose.d.ts +5 -0
  22. package/dist/src/presets/compose.js +80 -0
  23. package/dist/src/presets/effects.d.ts +40 -0
  24. package/dist/src/presets/effects.js +79 -0
  25. package/dist/src/presets/helpers.d.ts +33 -0
  26. package/dist/src/presets/helpers.js +165 -0
  27. package/dist/src/presets/index.d.ts +9 -0
  28. package/dist/src/presets/index.js +48 -0
  29. package/dist/src/presets/motions.d.ts +33 -0
  30. package/dist/src/presets/motions.js +75 -0
  31. package/dist/src/presets/scenes.d.ts +35 -0
  32. package/dist/src/presets/scenes.js +134 -0
  33. package/dist/src/presets/shapes.d.ts +71 -0
  34. package/dist/src/presets/shapes.js +96 -0
  35. package/dist/src/presets/transitions.d.ts +29 -0
  36. package/dist/src/presets/transitions.js +113 -0
  37. package/dist/src/presets/types.d.ts +34 -0
  38. package/dist/src/presets/types.js +2 -0
  39. package/dist/src/render/html.js +1 -4
  40. package/dist/src/render/svg.d.ts +2 -2
  41. package/dist/src/render/svg.js +86 -82
  42. package/dist/src/render/three-html.js +67 -113
  43. package/dist/src/scenes.js +1 -0
  44. package/dist/src/schema.js +218 -280
  45. package/dist/src/shapes/builtins.js +11 -47
  46. package/dist/src/shapes/common.js +12 -11
  47. package/dist/src/shapes/registry.d.ts +0 -1
  48. package/dist/src/shapes/registry.js +0 -4
  49. package/dist/src/shapes/types.d.ts +1 -3
  50. package/dist/src/types.d.ts +57 -288
  51. package/dist/src/utils.d.ts +2 -11
  52. package/dist/src/utils.js +13 -70
  53. package/dist/src/validate.js +321 -275
  54. package/dist/tests/run.js +576 -510
  55. package/package.json +46 -52
  56. package/schema/visual.schema.json +1086 -930
@@ -1,9 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateVisualDocument = validateVisualDocument;
4
- const shapes_1 = require("./shapes");
4
+ const animatable_1 = require("./animatable");
5
5
  const utils_1 = require("./utils");
6
- const POSITION_ANIMATION_PROPERTIES = new Set(["positionX", "positionY", "positionZ"]);
6
+ const ELEMENT_TYPES = new Set(["path", "text", "image", "point", "group"]);
7
+ const TOP_LEVEL_FIELDS = new Set(["version", "canvas", "elements"]);
8
+ const CANVAS_FIELDS = new Set(["width", "height", "background", "duration", "fps"]);
9
+ const COMMON_ELEMENT_FIELDS = new Set([
10
+ "id",
11
+ "type",
12
+ "opacity",
13
+ "fill",
14
+ "stroke",
15
+ "strokeWidth",
16
+ "strokeCap",
17
+ "strokeJoin",
18
+ "miterLimit",
19
+ "dashArray",
20
+ "dashOffset",
21
+ "drawStart",
22
+ "drawEnd",
23
+ "effects",
24
+ "blendMode",
25
+ "rotation",
26
+ "scale",
27
+ "scaleX",
28
+ "scaleY",
29
+ "origin",
30
+ "clip",
31
+ "mask",
32
+ "timeline"
33
+ ]);
34
+ const TYPE_FIELDS = {
35
+ path: new Set(["d", "x", "y"]),
36
+ text: new Set(["x", "y", "text", "lines", "align", "valign", "fontSize", "fontFamily", "weight", "fontStyle", "lineHeight", "letterSpacing", "maxWidth", "wrap"]),
37
+ image: new Set(["src", "x", "y", "width", "height", "fit", "source"]),
38
+ point: new Set(["x", "y"]),
39
+ group: new Set(["x", "y", "width", "height", "children"])
40
+ };
41
+ const TIMELINE_TRACK_FIELDS = new Set(["keyframes", "curve", "ease"]);
42
+ const TIMELINE_KEYFRAME_FIELDS = new Set(["time", "value", "in", "out", "interpolation"]);
7
43
  function validateVisualDocument(document) {
8
44
  const issues = [];
9
45
  const warnings = [];
@@ -11,183 +47,174 @@ function validateVisualDocument(document) {
11
47
  issues.push(issue("/", "invalid_document", "Document must be an object."));
12
48
  return { ok: false, issues, warnings };
13
49
  }
50
+ for (const key of Object.keys(document)) {
51
+ if (!TOP_LEVEL_FIELDS.has(key))
52
+ issues.push(issue(`/${key}`, "non_kernel_field", `Field '${key}' is not part of the render kernel.`));
53
+ }
14
54
  if (document.version !== 1)
15
55
  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
- }
56
+ validateCanvas(document.canvas, issues);
34
57
  const elements = document.elements ?? [];
35
58
  if (document.elements !== undefined && !Array.isArray(elements))
36
59
  issues.push(issue("/elements", "invalid_elements", "Document elements must be an array."));
37
60
  const allElements = Array.isArray(elements) ? (0, utils_1.flattenElements)(elements) : [];
38
- const ids = new Map();
61
+ const ids = new Set();
39
62
  for (const [index, element] of allElements.entries()) {
40
63
  const path = `/elements/${index}`;
41
64
  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 '-'.`));
65
+ if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(element.id)) {
66
+ issues.push(issue(`${path}/id`, "invalid_id", `Invalid id '${element.id}'. Use letters, numbers, '_', '-' or '.'.`));
44
67
  }
45
68
  else if (ids.has(element.id)) {
46
69
  issues.push(issue(`${path}/id`, "duplicate_id", `Duplicate element id '${element.id}'.`));
47
70
  }
48
71
  else {
49
- ids.set(element.id, element);
72
+ ids.add(element.id);
50
73
  }
51
74
  }
52
75
  }
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
- }
76
+ if (Array.isArray(elements))
77
+ validateElementList(elements, "/elements", issues, warnings);
75
78
  return { ok: issues.length === 0, issues, warnings };
76
79
  }
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."));
80
+ function validateCanvas(canvas, issues) {
81
+ if (!canvas || typeof canvas !== "object") {
82
+ issues.push(issue("/canvas", "missing_canvas", "Document must define canvas."));
84
83
  return;
85
84
  }
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'."));
85
+ for (const key of Object.keys(canvas)) {
86
+ if (!CANVAS_FIELDS.has(key))
87
+ issues.push(issue(`/canvas/${key}`, "non_kernel_canvas_field", `Canvas field '${key}' is not part of the render kernel.`));
88
+ }
89
+ if (!(0, utils_1.isFiniteNumber)(canvas.width))
90
+ issues.push(issue("/canvas/width", "missing_canvas_width", "Canvas width must be a number."));
91
+ if (!(0, utils_1.isFiniteNumber)(canvas.height))
92
+ issues.push(issue("/canvas/height", "missing_canvas_height", "Canvas height must be a number."));
93
+ if (canvas.background !== undefined && typeof canvas.background !== "string") {
94
+ issues.push(issue("/canvas/background", "invalid_canvas_background", "Canvas background must be a color string."));
95
+ }
96
+ if (canvas.duration !== undefined && (!(0, utils_1.isFiniteNumber)(canvas.duration) || canvas.duration < 0)) {
97
+ issues.push(issue("/canvas/duration", "invalid_canvas_duration", "Canvas duration must be a non-negative number."));
89
98
  }
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."));
99
+ if (canvas.fps !== undefined && (!(0, utils_1.isFiniteNumber)(canvas.fps) || canvas.fps <= 0)) {
100
+ issues.push(issue("/canvas/fps", "invalid_canvas_fps", "Canvas fps must be a positive number."));
92
101
  }
93
102
  }
94
- function validateElement(element, path, document, ids, issues, warnings) {
95
- if (!element || typeof element !== "object") {
103
+ function validateElementList(elements, path, issues, warnings) {
104
+ for (const [index, element] of elements.entries())
105
+ validateElement(element, `${path}/${index}`, issues, warnings);
106
+ }
107
+ function validateElement(element, path, issues, warnings) {
108
+ if (!element || typeof element !== "object" || Array.isArray(element)) {
96
109
  issues.push(issue(path, "invalid_element", "Element must be an object."));
97
110
  return;
98
111
  }
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.`));
112
+ const type = String(element.type ?? "");
113
+ if (!ELEMENT_TYPES.has(type)) {
114
+ issues.push(issue(`${path}/type`, "unsupported_type", `Unsupported kernel element type '${type}'.`));
102
115
  return;
103
116
  }
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;
117
+ validateElementFields(element, type, path, issues);
118
+ if (type === "path") {
119
+ if (typeof element.d !== "string" || !element.d?.trim()) {
120
+ issues.push(issue(`${path}/d`, "missing_path_d", "Path elements require a non-empty d string."));
121
+ }
122
+ if (element.x !== undefined)
123
+ requireNumber(element.x, `${path}/x`, issues);
124
+ if (element.y !== undefined)
125
+ requireNumber(element.y, `${path}/y`, issues);
126
+ }
127
+ if (type === "text") {
128
+ requireNumber(element.x, `${path}/x`, issues);
129
+ requireNumber(element.y, `${path}/y`, issues);
130
+ if (element.text !== undefined && typeof element.text !== "string") {
131
+ issues.push(issue(`${path}/text`, "invalid_text", "Text must be a string."));
132
+ }
133
+ const lines = element.lines;
134
+ if (lines !== undefined && (!Array.isArray(lines) || lines.some((line) => typeof line !== "string"))) {
135
+ issues.push(issue(`${path}/lines`, "invalid_lines", "Text lines must be an array of strings."));
136
+ }
137
+ if (typeof element.text === "string" && element.text.length > 80 && !element.wrap && !(0, utils_1.isFiniteNumber)(element.maxWidth)) {
138
+ warnings.push(warning(`${path}/text`, "long_text_no_wrap", "Long text should use maxWidth/wrap or explicit line breaks."));
139
+ }
108
140
  }
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'.`));
141
+ if (type === "image")
142
+ validateImage(element, path, issues);
143
+ if (type === "point") {
144
+ requireNumber(element.x, `${path}/x`, issues);
145
+ requireNumber(element.y, `${path}/y`, issues);
146
+ }
147
+ if (type === "group") {
148
+ requireNumber(element.x, `${path}/x`, issues);
149
+ requireNumber(element.y, `${path}/y`, issues);
150
+ if (element.width !== undefined)
151
+ requireNumber(element.width, `${path}/width`, issues);
152
+ if (element.height !== undefined)
153
+ requireNumber(element.height, `${path}/height`, issues);
154
+ const children = element.children;
155
+ if (!Array.isArray(children))
156
+ issues.push(issue(`${path}/children`, "invalid_group_children", "Group children must be an array."));
157
+ else
158
+ validateElementList(children, `${path}/children`, issues, warnings);
111
159
  }
112
- const context = createShapeValidationContext(document, path, ids, issues, warnings);
113
- definition.validateGeometry(element, context);
114
160
  validateStyle(element, path, issues);
115
- validateAnimation(element, path, definition, issues);
116
- definition.validateReferences?.(element, context);
117
- definition.validateWarnings?.(element, context);
161
+ validateTimeline(element, element.timeline, `${path}/timeline`, issues, warnings);
118
162
  }
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);
163
+ function validateElementFields(element, type, path, issues) {
164
+ const typeFields = TYPE_FIELDS[type] ?? new Set();
165
+ for (const key of Object.keys(element)) {
166
+ if (!COMMON_ELEMENT_FIELDS.has(key) && !typeFields.has(key)) {
167
+ issues.push(issue(`${path}/${key}`, "non_kernel_element_field", `Field '${key}' is not valid on kernel ${type} elements.`));
155
168
  }
156
- };
169
+ }
170
+ }
171
+ function validateImage(element, path, issues) {
172
+ if (typeof element.src !== "string" || !element.src)
173
+ issues.push(issue(`${path}/src`, "missing_image_src", "Image src must be a string."));
174
+ requireNumber(element.x, `${path}/x`, issues);
175
+ requireNumber(element.y, `${path}/y`, issues);
176
+ requireNumber(element.width, `${path}/width`, issues);
177
+ requireNumber(element.height, `${path}/height`, issues);
178
+ if ((0, utils_1.isFiniteNumber)(element.width) && element.width <= 0)
179
+ issues.push(issue(`${path}/width`, "invalid_image_width", "Image width must be positive."));
180
+ if ((0, utils_1.isFiniteNumber)(element.height) && element.height <= 0)
181
+ issues.push(issue(`${path}/height`, "invalid_image_height", "Image height must be positive."));
182
+ validateImageFit(element.fit, `${path}/fit`, issues);
183
+ if (!element.source)
184
+ return;
185
+ requireNumber(element.source.x, `${path}/source/x`, issues);
186
+ requireNumber(element.source.y, `${path}/source/y`, issues);
187
+ requireNumber(element.source.width, `${path}/source/width`, issues);
188
+ requireNumber(element.source.height, `${path}/source/height`, issues);
189
+ requireNumber(element.source.imageWidth, `${path}/source/imageWidth`, issues);
190
+ requireNumber(element.source.imageHeight, `${path}/source/imageHeight`, issues);
157
191
  }
158
192
  function validateStyle(element, path, issues) {
159
193
  validatePaint(element.fill, `${path}/fill`, issues);
160
194
  validatePaint(element.stroke, `${path}/stroke`, issues);
161
- if (element.strokeCap !== undefined && !["butt", "round", "square"].includes(String(element.strokeCap))) {
195
+ if (element.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(element.opacity) || element.opacity < 0 || element.opacity > 1))
196
+ issues.push(issue(`${path}/opacity`, "invalid_opacity", "Opacity must be between 0 and 1."));
197
+ if (element.strokeWidth !== undefined && !(0, utils_1.isFiniteNumber)(element.strokeWidth))
198
+ issues.push(issue(`${path}/strokeWidth`, "invalid_stroke_width", "strokeWidth must be a number."));
199
+ if (element.strokeCap !== undefined && !["butt", "round", "square"].includes(String(element.strokeCap)))
162
200
  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))) {
201
+ if (element.strokeJoin !== undefined && !["miter", "round", "bevel"].includes(String(element.strokeJoin)))
165
202
  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) {
203
+ for (const key of ["miterLimit", "dashOffset", "rotation", "scale", "scaleX", "scaleY", "fontSize", "lineHeight", "letterSpacing", "maxWidth"]) {
204
+ const value = element[key];
205
+ if (value !== undefined && !(0, utils_1.isFiniteNumber)(value))
206
+ issues.push(issue(`${path}/${key}`, "invalid_number", `${key} must be a finite number.`));
207
+ }
208
+ if (element.dashArray !== undefined && (!Array.isArray(element.dashArray) || element.dashArray.some((value) => !(0, utils_1.isFiniteNumber)(value))))
209
+ issues.push(issue(`${path}/dashArray`, "invalid_dash_array", "dashArray must be an array of numbers."));
210
+ if (element.drawStart !== undefined && (!(0, utils_1.isFiniteNumber)(element.drawStart) || element.drawStart < 0 || element.drawStart > 1))
211
+ issues.push(issue(`${path}/drawStart`, "invalid_draw_start", "drawStart must be between 0 and 1."));
212
+ if (element.drawEnd !== undefined && (!(0, utils_1.isFiniteNumber)(element.drawEnd) || element.drawEnd < 0 || element.drawEnd > 1))
213
+ issues.push(issue(`${path}/drawEnd`, "invalid_draw_end", "drawEnd must be between 0 and 1."));
214
+ if ((0, utils_1.isFiniteNumber)(element.drawStart) && (0, utils_1.isFiniteNumber)(element.drawEnd) && element.drawStart > element.drawEnd)
178
215
  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
- }
216
+ if (element.origin !== undefined && !(0, utils_1.isPoint2)(element.origin))
217
+ issues.push(issue(`${path}/origin`, "invalid_origin", "origin must be [x,y]."));
191
218
  validateEffects(element.effects, `${path}/effects`, issues);
192
219
  validateClip(element.clip, `${path}/clip`, issues);
193
220
  validateMask(element.mask, `${path}/mask`, issues);
@@ -195,8 +222,8 @@ function validateStyle(element, path, issues) {
195
222
  function validatePaint(value, path, issues) {
196
223
  if (value === undefined || typeof value === "string")
197
224
  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."));
225
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
226
+ issues.push(issue(path, "invalid_paint", "Paint must be a color string or structured paint object."));
200
227
  return;
201
228
  }
202
229
  if (value.type === "linearGradient") {
@@ -211,7 +238,7 @@ function validatePaint(value, path, issues) {
211
238
  if (!(0, utils_1.isPoint2)(value.center))
212
239
  issues.push(issue(`${path}/center`, "invalid_gradient_center", "Radial gradient center must be [x,y]."));
213
240
  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."));
241
+ issues.push(issue(`${path}/radius`, "invalid_gradient_radius", "Radial gradient radius must be non-negative."));
215
242
  if (value.focus !== undefined && !(0, utils_1.isPoint2)(value.focus))
216
243
  issues.push(issue(`${path}/focus`, "invalid_gradient_focus", "Radial gradient focus must be [x,y]."));
217
244
  validateGradientStops(value.stops, `${path}/stops`, issues);
@@ -221,20 +248,13 @@ function validatePaint(value, path, issues) {
221
248
  if (typeof value.src !== "string" || !value.src)
222
249
  issues.push(issue(`${path}/src`, "invalid_pattern_src", "Pattern src must be a string."));
223
250
  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."));
251
+ issues.push(issue(`${path}/width`, "invalid_pattern_width", "Pattern width must be positive."));
225
252
  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."));
253
+ issues.push(issue(`${path}/height`, "invalid_pattern_height", "Pattern height must be positive."));
231
254
  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
255
  return;
236
256
  }
237
- issues.push(issue(`${path}/type`, "invalid_paint_type", "Paint object type must be 'linearGradient', 'radialGradient', or 'pattern'."));
257
+ issues.push(issue(`${path}/type`, "invalid_paint_type", "Paint object type must be linearGradient, radialGradient, or pattern."));
238
258
  }
239
259
  function validateGradientStops(value, path, issues) {
240
260
  if (!Array.isArray(value) || value.length < 2) {
@@ -266,166 +286,192 @@ function validateEffects(value, path, issues) {
266
286
  const shadow = value.shadow;
267
287
  if (!shadow || typeof shadow !== "object" || Array.isArray(shadow)) {
268
288
  issues.push(issue(`${path}/shadow`, "invalid_shadow", "shadow must be an object."));
289
+ return;
269
290
  }
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
- }
291
+ requireNumber(shadow.dx, `${path}/shadow/dx`, issues);
292
+ requireNumber(shadow.dy, `${path}/shadow/dy`, issues);
293
+ requireNumber(shadow.blur, `${path}/shadow/blur`, issues);
294
+ if (typeof shadow.color !== "string")
295
+ issues.push(issue(`${path}/shadow/color`, "invalid_shadow_color", "shadow color must be a string."));
296
+ if (shadow.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(shadow.opacity) || shadow.opacity < 0 || shadow.opacity > 1))
297
+ issues.push(issue(`${path}/shadow/opacity`, "invalid_shadow_opacity", "shadow opacity must be between 0 and 1."));
280
298
  }
281
299
  }
282
300
  function validateClip(value, path, issues) {
283
- validateShape(value, path, "clip", issues);
301
+ if (value === undefined)
302
+ return;
303
+ if (!value || typeof value !== "object" || value.type !== "path" || typeof value.d !== "string" || !value.d.trim())
304
+ issues.push(issue(path, "invalid_clip", "clip must be { type:'path', d:string }."));
284
305
  }
285
306
  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)) {
307
+ if (value === undefined)
308
+ return;
309
+ if (!value || typeof value !== "object" || value.type !== "path" || typeof value.d !== "string" || !value.d.trim())
310
+ issues.push(issue(path, "invalid_mask", "mask must be { type:'path', d:string, opacity?:number }."));
311
+ if (value && value.opacity !== undefined && (!(0, utils_1.isFiniteNumber)(value.opacity) || value.opacity < 0 || value.opacity > 1))
288
312
  issues.push(issue(`${path}/opacity`, "invalid_mask_opacity", "mask opacity must be between 0 and 1."));
289
- }
290
313
  }
291
- function validateShape(value, path, label, issues) {
314
+ function validateTimeline(element, value, path, issues, warnings) {
292
315
  if (value === undefined)
293
316
  return;
294
- if (!value || typeof value !== "object") {
295
- issues.push(issue(path, `invalid_${label}`, `${label} must be a rect, circle, or path object.`));
317
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
318
+ issues.push(issue(path, "invalid_timeline", "timeline must be an object."));
296
319
  return;
297
320
  }
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);
321
+ if (value.start !== undefined && (!(0, utils_1.isFiniteNumber)(value.start) || value.start < 0))
322
+ issues.push(issue(`${path}/start`, "invalid_timeline_start", "timeline.start must be a non-negative number."));
323
+ if (value.end !== undefined && (!(0, utils_1.isFiniteNumber)(value.end) || value.end < 0))
324
+ issues.push(issue(`${path}/end`, "invalid_timeline_end", "timeline.end must be a non-negative number."));
325
+ if ((0, utils_1.isFiniteNumber)(value.start) && (0, utils_1.isFiniteNumber)(value.end) && value.end < value.start)
326
+ issues.push(issue(`${path}/end`, "invalid_timeline_range", "timeline.end must be greater than or equal to timeline.start."));
327
+ if (value.tracks === undefined)
303
328
  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);
329
+ if (!value.tracks || typeof value.tracks !== "object" || Array.isArray(value.tracks)) {
330
+ issues.push(issue(`${path}/tracks`, "invalid_timeline_tracks", "timeline.tracks must be an object."));
309
331
  return;
310
332
  }
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;
333
+ for (const [property, track] of Object.entries(value.tracks))
334
+ validateTrack(element, property, track, `${path}/tracks/${property}`, issues, warnings);
335
+ for (const message of (0, animatable_1.conflictWarningsForTracks)(Object.keys(value.tracks))) {
336
+ warnings.push(warning(`${path}/tracks`, "conflicting_timeline_tracks", message));
315
337
  }
316
- issues.push(issue(`${path}/type`, `invalid_${label}_type`, `${label} type must be 'rect', 'circle', or 'path'.`));
317
338
  }
318
- function validateImageOptions(element, path, issues) {
319
- validateImageFit(element.fit, `${path}/fit`, issues);
320
- if (!element.source)
339
+ function validateTrack(element, property, track, path, issues, warnings) {
340
+ if (!track || typeof track !== "object" || Array.isArray(track)) {
341
+ issues.push(issue(path, "invalid_timeline_track", "Timeline track must be an object."));
321
342
  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
343
  }
341
- }
342
- function validateAnimation(element, path, definition, issues) {
343
- if (!element.animate)
344
+ for (const key of Object.keys(track)) {
345
+ if (!TIMELINE_TRACK_FIELDS.has(key))
346
+ issues.push(issue(`${path}/${key}`, "non_kernel_timeline_track_field", `Timeline track field '${key}' is not part of the render kernel.`));
347
+ }
348
+ validateTimelineCurve(track.curve, `${path}/curve`, issues);
349
+ if (track.ease !== undefined && typeof track.ease !== "string")
350
+ issues.push(issue(`${path}/ease`, "invalid_timeline_ease", "Timeline ease must be a string."));
351
+ const propertySpec = (0, animatable_1.animatablePropertySpec)(element, property);
352
+ if (!propertySpec) {
353
+ const code = (0, animatable_1.knownAnimatableProperty)(property) ? "unsupported_timeline_track_for_element" : "unknown_timeline_track";
354
+ issues.push(issue(path, code, `Timeline track '${property}' is not a supported animatable property for ${element.type} elements.`));
355
+ }
356
+ if (!Array.isArray(track.keyframes) || !track.keyframes.length) {
357
+ issues.push(issue(`${path}/keyframes`, "invalid_timeline_keyframes", "Track keyframes must be a non-empty array."));
344
358
  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."));
359
+ }
360
+ let previous = Number.NEGATIVE_INFINITY;
361
+ for (const [index, frame] of track.keyframes.entries()) {
362
+ const time = validateKeyframe(frame, `${path}/keyframes/${index}`, issues, propertySpec);
363
+ if (time === undefined)
391
364
  continue;
365
+ if (time < previous)
366
+ issues.push(issue(`${path}/keyframes/${index}/time`, "unsorted_timeline_keyframes", "Keyframe times must be sorted."));
367
+ previous = time;
368
+ }
369
+ }
370
+ function validateKeyframe(frame, path, issues, propertySpec) {
371
+ if (Array.isArray(frame)) {
372
+ if (frame.length !== 2) {
373
+ issues.push(issue(path, "invalid_timeline_keyframe", "Keyframe tuple must be [time,value]."));
374
+ return undefined;
392
375
  }
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
- }
376
+ if (!(0, utils_1.isFiniteNumber)(frame[0])) {
377
+ issues.push(issue(`${path}/0`, "invalid_timeline_keyframe_time", "Keyframe time must be finite seconds."));
378
+ return undefined;
400
379
  }
380
+ validateTimelineValue(frame[1], `${path}/1`, issues, propertySpec);
381
+ return frame[0];
382
+ }
383
+ if (!frame || typeof frame !== "object") {
384
+ issues.push(issue(path, "invalid_timeline_keyframe", "Keyframe must be [time,value] or { time, value }."));
385
+ return undefined;
386
+ }
387
+ for (const key of Object.keys(frame)) {
388
+ if (!TIMELINE_KEYFRAME_FIELDS.has(key))
389
+ issues.push(issue(`${path}/${key}`, "non_kernel_timeline_keyframe_field", `Timeline keyframe field '${key}' is not part of the render kernel.`));
390
+ }
391
+ if (!(0, utils_1.isFiniteNumber)(frame.time)) {
392
+ issues.push(issue(`${path}/time`, "invalid_timeline_keyframe_time", "Keyframe time must be finite seconds."));
393
+ return undefined;
394
+ }
395
+ validateTimelineValue(frame.value, `${path}/value`, issues, propertySpec);
396
+ validateTimelineCurve(frame.in, `${path}/in`, issues);
397
+ validateTimelineCurve(frame.out, `${path}/out`, issues);
398
+ validateTimelineCurve(frame.interpolation, `${path}/interpolation`, issues);
399
+ return frame.time;
400
+ }
401
+ function validateTimelineValue(value, path, issues, propertySpec) {
402
+ if (!(0, animatable_1.isTimelineValue)(value)) {
403
+ issues.push(issue(path, "invalid_timeline_value", "Track value must be a JSON-safe timeline value."));
404
+ return;
405
+ }
406
+ if (propertySpec && !(0, animatable_1.validateMotionValueForProperty)(propertySpec, value)) {
407
+ issues.push(issue(path, "invalid_timeline_value_for_property", `Track value is not valid for '${propertySpec.property}'.`));
401
408
  }
402
409
  }
403
- function validateEndpoint(value, path, ids, issues) {
404
- if ((0, utils_1.isPoint2)(value))
410
+ function validateTimelineCurve(curve, path, issues) {
411
+ if (curve === undefined)
412
+ return;
413
+ if (!curve || typeof curve !== "object" || Array.isArray(curve)) {
414
+ issues.push(issue(path, "invalid_timeline_curve", "Timeline curve must be an object."));
405
415
  return;
406
- if (typeof value !== "string") {
407
- issues.push(issue(path, "invalid_endpoint", "Endpoint must be [x,y] or a reference like 'box.right'."));
416
+ }
417
+ if (curve.type === "hold") {
418
+ validateAllowedKeys(curve, path, new Set(["type"]), issues);
419
+ return;
420
+ }
421
+ if (curve.type === "cubicBezier") {
422
+ validateAllowedKeys(curve, path, new Set(["type", "x1", "y1", "x2", "y2"]), issues);
423
+ if (!(0, utils_1.isFiniteNumber)(curve.x1) || curve.x1 < 0 || curve.x1 > 1)
424
+ issues.push(issue(`${path}/x1`, "invalid_curve_x1", "cubicBezier.x1 must be between 0 and 1."));
425
+ if (!(0, utils_1.isFiniteNumber)(curve.x2) || curve.x2 < 0 || curve.x2 > 1)
426
+ issues.push(issue(`${path}/x2`, "invalid_curve_x2", "cubicBezier.x2 must be between 0 and 1."));
427
+ if (!(0, utils_1.isFiniteNumber)(curve.y1))
428
+ issues.push(issue(`${path}/y1`, "invalid_curve_y1", "cubicBezier.y1 must be a finite number."));
429
+ if (!(0, utils_1.isFiniteNumber)(curve.y2))
430
+ issues.push(issue(`${path}/y2`, "invalid_curve_y2", "cubicBezier.y2 must be a finite number."));
408
431
  return;
409
432
  }
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}'.`));
433
+ if (curve.type === "graph") {
434
+ validateAllowedKeys(curve, path, new Set(["type", "points"]), issues);
435
+ if (!Array.isArray(curve.points) || curve.points.length < 2) {
436
+ issues.push(issue(`${path}/points`, "invalid_curve_points", "Graph curve points must contain at least two [x,y] points."));
437
+ return;
438
+ }
439
+ let previousX = Number.NEGATIVE_INFINITY;
440
+ for (const [index, point] of curve.points.entries()) {
441
+ if (!(0, utils_1.isPoint2)(point)) {
442
+ issues.push(issue(`${path}/points/${index}`, "invalid_curve_point", "Graph curve points must be [x,y]."));
443
+ continue;
444
+ }
445
+ if (point[0] < 0 || point[0] > 1)
446
+ issues.push(issue(`${path}/points/${index}/0`, "invalid_curve_point_x", "Graph curve x values must be between 0 and 1."));
447
+ if (point[0] <= previousX)
448
+ issues.push(issue(`${path}/points/${index}/0`, "unsorted_curve_points", "Graph curve x values must be strictly increasing."));
449
+ previousX = point[0];
450
+ }
451
+ const first = curve.points[0];
452
+ const last = curve.points[curve.points.length - 1];
453
+ if ((0, utils_1.isPoint2)(first) && first[0] !== 0)
454
+ issues.push(issue(`${path}/points/0/0`, "invalid_curve_start", "Graph curve must start at x=0."));
455
+ if ((0, utils_1.isPoint2)(last) && last[0] !== 1)
456
+ issues.push(issue(`${path}/points/${curve.points.length - 1}/0`, "invalid_curve_end", "Graph curve must end at x=1."));
414
457
  return;
415
458
  }
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.`));
459
+ issues.push(issue(`${path}/type`, "invalid_timeline_curve_type", "Timeline curve type must be graph, cubicBezier, or hold."));
460
+ }
461
+ function validateAllowedKeys(value, path, allowed, issues) {
462
+ for (const key of Object.keys(value)) {
463
+ if (!allowed.has(key))
464
+ issues.push(issue(`${path}/${key}`, "non_kernel_timeline_curve_field", `Timeline curve field '${key}' is not part of this curve type.`));
420
465
  }
421
466
  }
467
+ function validateImageFit(value, path, issues) {
468
+ if (value !== undefined && value !== "fill" && value !== "contain" && value !== "cover")
469
+ issues.push(issue(path, "invalid_image_fit", "fit must be 'fill', 'contain', or 'cover'."));
470
+ }
422
471
  function requireNumber(value, path, issues) {
423
472
  if (!(0, utils_1.isFiniteNumber)(value))
424
473
  issues.push(issue(path, "missing_number", "A finite number is required."));
425
474
  }
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
475
  function issue(path, code, message, suggestion) {
430
476
  return { path, code, message, ...(suggestion ? { suggestion } : {}) };
431
477
  }