@sebastianwessel/isostate 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.
Files changed (61) hide show
  1. package/dist/animation/animation-engine.d.ts +78 -0
  2. package/dist/animation/animation-engine.d.ts.map +1 -0
  3. package/dist/animation/controller.d.ts +130 -0
  4. package/dist/animation/controller.d.ts.map +1 -0
  5. package/dist/browser/isostate.runtime.js +2449 -0
  6. package/dist/browser/isostate.runtime.js.map +1 -0
  7. package/dist/chunks/errors-DyqEkrm5.js +29 -0
  8. package/dist/chunks/errors-DyqEkrm5.js.map +1 -0
  9. package/dist/chunks/index-CDQt8CfR.js +2380 -0
  10. package/dist/chunks/index-CDQt8CfR.js.map +1 -0
  11. package/dist/dsl/compiler.d.ts +13 -0
  12. package/dist/dsl/compiler.d.ts.map +1 -0
  13. package/dist/dsl/index.d.ts +7 -0
  14. package/dist/dsl/index.d.ts.map +1 -0
  15. package/dist/dsl/index.js +2191 -0
  16. package/dist/dsl/index.js.map +1 -0
  17. package/dist/dsl/scene-parser.d.ts +6 -0
  18. package/dist/dsl/scene-parser.d.ts.map +1 -0
  19. package/dist/dsl/scene-validator.d.ts +11 -0
  20. package/dist/dsl/scene-validator.d.ts.map +1 -0
  21. package/dist/index.d.ts +12 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +49 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/rendering/animation-css.d.ts +3 -0
  26. package/dist/rendering/animation-css.d.ts.map +1 -0
  27. package/dist/rendering/asset-node.d.ts +13 -0
  28. package/dist/rendering/asset-node.d.ts.map +1 -0
  29. package/dist/rendering/rendering-engine.d.ts +77 -0
  30. package/dist/rendering/rendering-engine.d.ts.map +1 -0
  31. package/dist/rendering/theme.d.ts +2 -0
  32. package/dist/rendering/theme.d.ts.map +1 -0
  33. package/dist/runtime/index.d.ts +3 -0
  34. package/dist/runtime/index.d.ts.map +1 -0
  35. package/dist/runtime/index.js +3 -0
  36. package/dist/runtime/index.js.map +1 -0
  37. package/dist/runtime/mount-scene.d.ts +52 -0
  38. package/dist/runtime/mount-scene.d.ts.map +1 -0
  39. package/dist/types/asset-registry.d.ts +26 -0
  40. package/dist/types/asset-registry.d.ts.map +1 -0
  41. package/dist/types/assets.d.ts +37 -0
  42. package/dist/types/assets.d.ts.map +1 -0
  43. package/dist/types/errors.d.ts +23 -0
  44. package/dist/types/errors.d.ts.map +1 -0
  45. package/dist/types/index.d.ts +8 -0
  46. package/dist/types/index.d.ts.map +1 -0
  47. package/dist/types/node.d.ts +251 -0
  48. package/dist/types/node.d.ts.map +1 -0
  49. package/dist/types/runtime-bundle.d.ts +44 -0
  50. package/dist/types/runtime-bundle.d.ts.map +1 -0
  51. package/dist/types/scene.d.ts +94 -0
  52. package/dist/types/scene.d.ts.map +1 -0
  53. package/dist/types/validation.d.ts +40 -0
  54. package/dist/types/validation.d.ts.map +1 -0
  55. package/dist/utils/easing.d.ts +25 -0
  56. package/dist/utils/easing.d.ts.map +1 -0
  57. package/dist/utils/index.d.ts +3 -0
  58. package/dist/utils/index.d.ts.map +1 -0
  59. package/dist/utils/projection.d.ts +22 -0
  60. package/dist/utils/projection.d.ts.map +1 -0
  61. package/package.json +63 -0
@@ -0,0 +1,2191 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { V as ValidationErrorClass, P as ParseError } from '../chunks/errors-DyqEkrm5.js';
3
+ import { parse } from 'yaml';
4
+
5
+ const BUILT_IN_TEXT_ASSET_ID$1 = "text";
6
+ const BUILT_IN_PRIMITIVE_ASSET_IDS$1 = new Set(["rectangle", "circle", "polygon", "line"]);
7
+ const MAX_TEXT_CHARACTERS = 1000;
8
+ const MAX_TEXT_LINES = 20;
9
+ const MAX_PRIMITIVE_POINTS = 100;
10
+ const VALID_ENTRY_ANIMATIONS = new Set([
11
+ "fade-in",
12
+ "fade-in-grow",
13
+ "fall-in",
14
+ "rise-from-ground",
15
+ "slide-in-left",
16
+ "slide-in-right",
17
+ "flip-in",
18
+ "none",
19
+ ]);
20
+ const VALID_EXIT_ANIMATIONS = new Set([
21
+ "fade-out",
22
+ "fade-out-shrink",
23
+ "fall-through-ground",
24
+ "rise-away",
25
+ "slide-out-left",
26
+ "slide-out-right",
27
+ "flip-out",
28
+ "none",
29
+ ]);
30
+ const VALID_AMBIENT_ANIMATIONS = new Set([
31
+ "pulse",
32
+ "float",
33
+ "shake",
34
+ "glow",
35
+ "spin",
36
+ "blink",
37
+ "bounce",
38
+ ]);
39
+ const VALID_CONNECTOR_AMBIENT_ANIMATIONS = new Set([...VALID_AMBIENT_ANIMATIONS, "flow"]);
40
+ const VALID_CONNECTOR_PATTERNS = new Set(["solid", "dashed", "dotted"]);
41
+ const VALID_CONNECTOR_VARIANTS = new Set(["line", "road"]);
42
+ const VALID_CONNECTOR_ENDPOINTS = new Set(["none", "arrow", "dot", "circle", "diamond", "bar"]);
43
+ const VALID_CONNECTOR_DIRECTIONS = new Set(["route", "reverse"]);
44
+ const VALID_CONNECTOR_SIDES = new Set(["auto", "top", "right", "bottom", "left", "front", "back"]);
45
+ const VALID_CONNECTOR_ROUTING_MODES = new Set(["straight", "orthogonal", "manual"]);
46
+ const VALID_CONNECTOR_ROUTING_PREFERENCES = new Set(["direct", "fewest-bends", "shortest"]);
47
+ const VALID_CONNECTOR_LANES = new Set(["none", "center-dashed"]);
48
+ const VALID_TEXT_ALIGN = new Set(["start", "middle", "end"]);
49
+ const VALID_TEXT_WEIGHT = new Set(["normal", "bold"]);
50
+ const VALID_LINE_CAPS = new Set(["butt", "round", "square"]);
51
+ const VALID_LINE_JOINS = new Set(["miter", "round", "bevel"]);
52
+ function issue(code, message, extras = {}) {
53
+ return { code, message, ...extras };
54
+ }
55
+ function isValidIdentifier(value) {
56
+ return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value);
57
+ }
58
+ function isValidPosition(value) {
59
+ return Array.isArray(value) && value.length === 2 && value.every((part) => Number.isFinite(part) && part >= 0);
60
+ }
61
+ function isValidPositiveNumber(value) {
62
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
63
+ }
64
+ function defaultElementLayer(document) {
65
+ const structures = document.header.layers.find((layer) => layer.name === "structures");
66
+ return structures?.name ?? document.header.layers[0]?.name ?? "";
67
+ }
68
+ function defaultFloorLayer$1(document) {
69
+ const ground = document.header.layers.find((layer) => layer.name === "ground");
70
+ return ground?.name ?? document.header.layers[0]?.name ?? "";
71
+ }
72
+ function defaultConnectorLayer(document) {
73
+ const connectors = document.header.layers.find((layer) => layer.name === "connectors");
74
+ const ground = document.header.layers.find((layer) => layer.name === "ground");
75
+ return connectors?.name ?? ground?.name ?? document.header.layers[0]?.name ?? "";
76
+ }
77
+ function declaredAssetNames(document) {
78
+ return new Set(document.header.assets.map((asset) => asset.id));
79
+ }
80
+ function hasUrlAssetSource(document, assetId) {
81
+ return Boolean(document.header.assetBaseUrl && document.header.assets.some((asset) => asset.id === assetId));
82
+ }
83
+ function isBuiltInAsset(assetId) {
84
+ return assetId === BUILT_IN_TEXT_ASSET_ID$1 || BUILT_IN_PRIMITIVE_ASSET_IDS$1.has(assetId);
85
+ }
86
+ function isPrimitiveAsset(assetId) {
87
+ return BUILT_IN_PRIMITIVE_ASSET_IDS$1.has(assetId);
88
+ }
89
+ function hasExternalAssetReferences(document) {
90
+ if (document.header.floor?.asset && !isBuiltInAsset(document.header.floor.asset)) {
91
+ return true;
92
+ }
93
+ for (const scene of document.scenes) {
94
+ for (const element of [...(scene.elements ?? []), ...(scene.add?.elements ?? [])]) {
95
+ if (!isBuiltInAsset(element.asset))
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+ function hasBuiltInElements(document) {
102
+ for (const scene of document.scenes) {
103
+ for (const element of [...(scene.elements ?? []), ...(scene.add?.elements ?? [])]) {
104
+ if (isBuiltInAsset(element.asset))
105
+ return true;
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+ function declaredLayerNames(document) {
111
+ return new Set(document.header.layers.map((layer) => layer.name));
112
+ }
113
+ function validateHeader(document, errors) {
114
+ const assets = document.header.assets;
115
+ if (assets.length === 0 && (hasExternalAssetReferences(document) || !hasBuiltInElements(document))) {
116
+ errors.push(issue("NO_ASSETS", "Header must declare at least one asset"));
117
+ }
118
+ const assetNames = new Set();
119
+ for (const asset of assets) {
120
+ if (!isValidIdentifier(asset.id)) {
121
+ errors.push(issue("INVALID_IDENTIFIER", `Asset "${asset.id}" must be kebab-case`, {
122
+ assetName: asset.id,
123
+ }));
124
+ }
125
+ if (assetNames.has(asset.id)) {
126
+ errors.push(issue("DUPLICATE_ASSET_ID", `Duplicate asset "${asset.id}"`, {
127
+ assetName: asset.id,
128
+ }));
129
+ }
130
+ assetNames.add(asset.id);
131
+ if (isBuiltInAsset(asset.id)) {
132
+ errors.push(issue("BUILTIN_ASSET_ID_RESERVED", `Asset "${asset.id}" is reserved for a built-in asset`, {
133
+ assetName: asset.id,
134
+ }));
135
+ continue;
136
+ }
137
+ if (!hasUrlAssetSource(document, asset.id)) {
138
+ errors.push(issue("ASSET_URL_REQUIRED", `Asset "${asset.id}" has no URL source`, {
139
+ assetName: asset.id,
140
+ }));
141
+ }
142
+ if (asset.anchor !== undefined &&
143
+ (!isValidPosition(asset.anchor) || asset.anchor.some((part) => part < 0 || part > 1))) {
144
+ errors.push(issue("INVALID_ASSET_ANCHOR", `Asset "${asset.id}" anchor must use normalized values from 0 to 1`, {
145
+ assetName: asset.id,
146
+ }));
147
+ }
148
+ }
149
+ const layers = document.header.layers;
150
+ if (layers.length === 0) {
151
+ errors.push(issue("NO_LAYERS", "Header must declare at least one layer"));
152
+ }
153
+ const layerNames = new Set();
154
+ for (const layer of layers) {
155
+ if (!isValidIdentifier(layer.name)) {
156
+ errors.push(issue("INVALID_IDENTIFIER", `Layer "${layer.name}" must be kebab-case`, {
157
+ layerName: layer.name,
158
+ }));
159
+ }
160
+ if (layerNames.has(layer.name)) {
161
+ errors.push(issue("DUPLICATE_LAYER_NAME", `Duplicate layer "${layer.name}"`, {
162
+ layerName: layer.name,
163
+ }));
164
+ }
165
+ layerNames.add(layer.name);
166
+ if (layer.order !== undefined && (!Number.isFinite(layer.order) || !Number.isInteger(layer.order))) {
167
+ errors.push(issue("INVALID_LAYER_ORDER", "Layer order must be a finite integer", {
168
+ layerName: layer.name,
169
+ }));
170
+ }
171
+ }
172
+ const floor = document.header.floor;
173
+ if (floor) {
174
+ if (floor.size !== undefined && !isValidPositiveTuple(floor.size)) {
175
+ errors.push(issue("INVALID_FLOOR_SIZE", "Floor size must be positive"));
176
+ }
177
+ if (floor.layer !== undefined && !layerNames.has(floor.layer)) {
178
+ errors.push(issue("LAYER_NOT_FOUND", `Floor layer "${floor.layer}" is not declared`, {
179
+ layerName: floor.layer,
180
+ }));
181
+ }
182
+ if (floor.asset !== undefined && !assetNames.has(floor.asset)) {
183
+ errors.push(issue("ASSET_NOT_DECLARED", `Floor asset "${floor.asset}" is not declared`, {
184
+ assetName: floor.asset,
185
+ }));
186
+ }
187
+ if (floor.asset !== undefined && isBuiltInAsset(floor.asset)) {
188
+ errors.push(issue("INVALID_FLOOR_ASSET", "Floor asset cannot use a built-in generated asset", { assetName: floor.asset }));
189
+ }
190
+ }
191
+ }
192
+ function isValidPositiveTuple(value) {
193
+ return Array.isArray(value) && value.length === 2 && value.every((part) => Number.isFinite(part) && part > 0);
194
+ }
195
+ function validateTimelineShape(document, errors) {
196
+ if (document.scenes.length === 0) {
197
+ errors.push(issue("NO_SCENES", "Document must contain at least one scene"));
198
+ return;
199
+ }
200
+ const sceneIds = new Set();
201
+ for (const scene of document.scenes) {
202
+ if (!isValidIdentifier(scene.id)) {
203
+ errors.push(issue("INVALID_IDENTIFIER", `Scene "${scene.id}" must be kebab-case`, {
204
+ sceneId: scene.id,
205
+ }));
206
+ }
207
+ if (sceneIds.has(scene.id)) {
208
+ errors.push(issue("DUPLICATE_SCENE_ID", `Duplicate scene "${scene.id}"`, {
209
+ sceneId: scene.id,
210
+ }));
211
+ }
212
+ sceneIds.add(scene.id);
213
+ }
214
+ const first = document.scenes[0];
215
+ if (first.elements === undefined ||
216
+ first.add !== undefined ||
217
+ first.update !== undefined ||
218
+ first.remove !== undefined) {
219
+ errors.push(issue("INVALID_INITIAL_SCENE", "Initial scene must use elements and no delta operations", { sceneId: first.id }));
220
+ }
221
+ for (const scene of document.scenes.slice(1)) {
222
+ if (scene.elements !== undefined || scene.connections !== undefined) {
223
+ errors.push(issue("INVALID_SCENE_DELTA", "Delta scenes may not use top-level elements or connections", {
224
+ sceneId: scene.id,
225
+ }));
226
+ }
227
+ }
228
+ }
229
+ function validatePlacement(placement, document, errors, sceneId) {
230
+ validateElementCommon(placement, document, errors, sceneId);
231
+ validateGeneratedContentForAsset(placement, placement.asset, errors, sceneId);
232
+ if (!isBuiltInAsset(placement.asset) && !declaredAssetNames(document).has(placement.asset)) {
233
+ errors.push(issue("ASSET_NOT_DECLARED", `Asset "${placement.asset}" is not declared`, {
234
+ sceneId,
235
+ elementId: placement.id,
236
+ assetName: placement.asset,
237
+ }));
238
+ }
239
+ if (!isValidPosition(placement.at)) {
240
+ errors.push(issue("INVALID_POSITION", "Element at must be finite and non-negative", {
241
+ sceneId,
242
+ elementId: placement.id,
243
+ }));
244
+ }
245
+ }
246
+ function validatePatch(patch, document, errors, sceneId, currentAsset) {
247
+ validateElementCommon(patch, document, errors, sceneId);
248
+ if (currentAsset !== undefined) {
249
+ validateGeneratedContentForAsset(patch, currentAsset, errors, sceneId);
250
+ }
251
+ if (patch.at !== undefined && !isValidPosition(patch.at)) {
252
+ errors.push(issue("INVALID_POSITION", "Element at must be finite and non-negative", {
253
+ sceneId,
254
+ elementId: patch.id,
255
+ }));
256
+ }
257
+ }
258
+ function validateElementCommon(element, document, errors, sceneId) {
259
+ if (!isValidIdentifier(element.id)) {
260
+ errors.push(issue("INVALID_IDENTIFIER", `Element "${element.id}" must be kebab-case`, {
261
+ sceneId,
262
+ elementId: element.id,
263
+ }));
264
+ }
265
+ if (element.size !== undefined && !isValidPositiveNumber(element.size)) {
266
+ errors.push(issue("INVALID_SIZE", "Element size must be greater than zero", {
267
+ sceneId,
268
+ elementId: element.id,
269
+ }));
270
+ }
271
+ if (element.size !== undefined && (!Number.isInteger(element.size) || element.size < 1)) {
272
+ errors.push(issue("INVALID_SIZE", "Element size must be a positive whole grid cell count", {
273
+ sceneId,
274
+ elementId: element.id,
275
+ }));
276
+ }
277
+ if (element.layer !== undefined && !declaredLayerNames(document).has(element.layer)) {
278
+ errors.push(issue("LAYER_NOT_FOUND", `Layer "${element.layer}" is not declared`, {
279
+ sceneId,
280
+ elementId: element.id,
281
+ layerName: element.layer,
282
+ }));
283
+ }
284
+ if (element.enter !== undefined && !VALID_ENTRY_ANIMATIONS.has(element.enter)) {
285
+ errors.push(issue("UNKNOWN_ANIMATION", `Unknown entry animation "${element.enter}"`, {
286
+ sceneId,
287
+ elementId: element.id,
288
+ }));
289
+ }
290
+ if (element.exit !== undefined && !VALID_EXIT_ANIMATIONS.has(element.exit)) {
291
+ errors.push(issue("UNKNOWN_ANIMATION", `Unknown exit animation "${element.exit}"`, {
292
+ sceneId,
293
+ elementId: element.id,
294
+ }));
295
+ }
296
+ validateAmbient(element.ambient, errors, sceneId, element.id);
297
+ }
298
+ function validateGeneratedContentForAsset(element, assetId, errors, sceneId) {
299
+ if (isBuiltInAsset(assetId)) {
300
+ if (assetId === BUILT_IN_TEXT_ASSET_ID$1) {
301
+ validateTextForAsset(element, errors, sceneId);
302
+ return;
303
+ }
304
+ validatePrimitiveForAsset(element, assetId, errors, sceneId);
305
+ return;
306
+ }
307
+ if (element.text !== undefined) {
308
+ errors.push(issue("TEXT_CONTENT_FOR_NON_TEXT_ASSET", "Only built-in text elements may define text content", {
309
+ sceneId,
310
+ elementId: element.id,
311
+ assetName: assetId,
312
+ }));
313
+ }
314
+ if (element.primitive !== undefined) {
315
+ errors.push(issue("GENERATED_CONTENT_FOR_EXTERNAL_ASSET", "Only built-in generated assets may define primitive content", {
316
+ sceneId,
317
+ elementId: element.id,
318
+ assetName: assetId,
319
+ }));
320
+ }
321
+ }
322
+ function validateTextForAsset(element, errors, sceneId) {
323
+ if (!element.text) {
324
+ errors.push(issue("TEXT_CONTENT_REQUIRED", "Built-in text elements require text content", { sceneId, elementId: element.id }));
325
+ return;
326
+ }
327
+ if (element.primitive !== undefined) {
328
+ errors.push(issue("PRIMITIVE_CONTENT_FOR_TEXT_ASSET", "Built-in text elements may not define primitive content", {
329
+ sceneId,
330
+ elementId: element.id,
331
+ }));
332
+ }
333
+ validateTextContent(element.text, errors, sceneId, element.id);
334
+ }
335
+ function validatePrimitiveForAsset(element, assetId, errors, sceneId) {
336
+ if (!isPrimitiveAsset(assetId))
337
+ return;
338
+ if (element.text !== undefined) {
339
+ errors.push(issue("TEXT_CONTENT_FOR_PRIMITIVE_ASSET", "Primitive elements may not define text content", {
340
+ sceneId,
341
+ elementId: element.id,
342
+ }));
343
+ }
344
+ const primitive = element.primitive;
345
+ if (!primitive) {
346
+ errors.push(issue("PRIMITIVE_CONTENT_REQUIRED", "Built-in primitive elements require primitive content", {
347
+ sceneId,
348
+ elementId: element.id,
349
+ assetName: assetId,
350
+ }));
351
+ return;
352
+ }
353
+ const activeKeys = Object.entries(primitive)
354
+ .filter(([, value]) => value !== undefined)
355
+ .map(([key]) => key);
356
+ if (activeKeys.length !== 1 || activeKeys[0] !== assetId) {
357
+ errors.push(issue("PRIMITIVE_CONTENT_MISMATCH", "Primitive content must define exactly the payload matching its asset id", {
358
+ sceneId,
359
+ elementId: element.id,
360
+ assetName: assetId,
361
+ }));
362
+ return;
363
+ }
364
+ const payload = primitive[assetId];
365
+ validatePrimitiveStyle(payload, errors, sceneId, element.id);
366
+ if (assetId === "rectangle") {
367
+ const rx = primitive.rectangle?.rx;
368
+ if (rx !== undefined && (!Number.isFinite(rx) || rx < 0 || rx > 0.5)) {
369
+ errors.push(issue("INVALID_PRIMITIVE_STYLE", "Rectangle rx must be from 0 to 0.5", {
370
+ sceneId,
371
+ elementId: element.id,
372
+ }));
373
+ }
374
+ }
375
+ if (assetId === "polygon") {
376
+ validatePrimitivePoints(primitive.polygon?.points, 3, errors, sceneId, element.id);
377
+ }
378
+ if (assetId === "line") {
379
+ validatePrimitivePoints(primitive.line?.points, 2, errors, sceneId, element.id);
380
+ if (primitive.line?.lineCap !== undefined && !VALID_LINE_CAPS.has(primitive.line.lineCap)) {
381
+ errors.push(issue("INVALID_PRIMITIVE_STYLE", "Line cap is invalid", {
382
+ sceneId,
383
+ elementId: element.id,
384
+ }));
385
+ }
386
+ if (primitive.line?.lineJoin !== undefined && !VALID_LINE_JOINS.has(primitive.line.lineJoin)) {
387
+ errors.push(issue("INVALID_PRIMITIVE_STYLE", "Line join is invalid", {
388
+ sceneId,
389
+ elementId: element.id,
390
+ }));
391
+ }
392
+ }
393
+ }
394
+ function validatePrimitiveStyle(style, errors, sceneId, elementId) {
395
+ if (!style)
396
+ return;
397
+ for (const token of [style.stroke, "fill" in style ? style.fill : undefined]) {
398
+ if (token !== undefined && !isSafeTextStyleToken(token)) {
399
+ errors.push(issue("INVALID_PRIMITIVE_STYLE", "Primitive color token is unsafe", {
400
+ sceneId,
401
+ elementId,
402
+ }));
403
+ }
404
+ }
405
+ if (style.strokeWidth !== undefined && (!Number.isFinite(style.strokeWidth) || style.strokeWidth < 0)) {
406
+ errors.push(issue("INVALID_PRIMITIVE_STYLE", "Primitive strokeWidth is invalid", {
407
+ sceneId,
408
+ elementId,
409
+ }));
410
+ }
411
+ if (style.opacity !== undefined && (!Number.isFinite(style.opacity) || style.opacity < 0 || style.opacity > 1)) {
412
+ errors.push(issue("INVALID_PRIMITIVE_STYLE", "Primitive opacity must be 0..1", {
413
+ sceneId,
414
+ elementId,
415
+ }));
416
+ }
417
+ if (style.dash !== undefined &&
418
+ (!isValidPositiveTuple(style.dash) || style.dash.some((part) => !Number.isFinite(part)))) {
419
+ errors.push(issue("INVALID_PRIMITIVE_STYLE", "Primitive dash is invalid", {
420
+ sceneId,
421
+ elementId,
422
+ }));
423
+ }
424
+ }
425
+ function validatePrimitivePoints(points, minCount, errors, sceneId, elementId) {
426
+ if (!points ||
427
+ points.length < minCount ||
428
+ points.length > MAX_PRIMITIVE_POINTS ||
429
+ points.some((point) => !isValidPosition(point) || point.some((part) => part < 0 || part > 1))) {
430
+ errors.push(issue("INVALID_PRIMITIVE_POINTS", "Primitive points must use normalized coordinates from 0 to 1", {
431
+ sceneId,
432
+ elementId,
433
+ }));
434
+ }
435
+ }
436
+ function validateTextContent(text, errors, sceneId, elementId) {
437
+ const value = normalizeTextValue(text.value);
438
+ const lines = value.split("\n");
439
+ if (value.length === 0 ||
440
+ value.length > MAX_TEXT_CHARACTERS ||
441
+ lines.length > MAX_TEXT_LINES ||
442
+ lines.every((line) => line.trim().length === 0)) {
443
+ errors.push(issue("INVALID_TEXT_CONTENT", `Text content must be non-empty, at most ${MAX_TEXT_CHARACTERS} characters, and at most ${MAX_TEXT_LINES} lines`, { sceneId, elementId }));
444
+ }
445
+ if (text.align !== undefined && !VALID_TEXT_ALIGN.has(text.align)) {
446
+ errors.push(issue("INVALID_TEXT_STYLE", "Text align must be start, middle, or end", {
447
+ sceneId,
448
+ elementId,
449
+ }));
450
+ }
451
+ if (text.fontSize !== undefined && !isValidPositiveNumber(text.fontSize)) {
452
+ errors.push(issue("INVALID_TEXT_STYLE", "Text fontSize must be greater than zero", {
453
+ sceneId,
454
+ elementId,
455
+ }));
456
+ }
457
+ if (text.lineHeight !== undefined && !isValidPositiveNumber(text.lineHeight)) {
458
+ errors.push(issue("INVALID_TEXT_STYLE", "Text lineHeight must be greater than zero", {
459
+ sceneId,
460
+ elementId,
461
+ }));
462
+ }
463
+ if (text.fontWeight !== undefined && !isValidTextWeight(text.fontWeight)) {
464
+ errors.push(issue("INVALID_TEXT_STYLE", "Text fontWeight must be normal, bold, or a positive finite number", {
465
+ sceneId,
466
+ elementId,
467
+ }));
468
+ }
469
+ if (text.fill !== undefined && !isSafeTextStyleToken(text.fill)) {
470
+ errors.push(issue("INVALID_TEXT_STYLE", "Text fill contains unsafe CSS syntax", {
471
+ sceneId,
472
+ elementId,
473
+ }));
474
+ }
475
+ }
476
+ function normalizeTextValue(value) {
477
+ return value.replace(/\r\n?/g, "\n");
478
+ }
479
+ function isValidTextWeight(value) {
480
+ if (typeof value === "number") {
481
+ return Number.isFinite(value) && value > 0;
482
+ }
483
+ return typeof value === "string" && VALID_TEXT_WEIGHT.has(value);
484
+ }
485
+ function isSafeTextStyleToken(value) {
486
+ const normalized = value.trim().toLowerCase();
487
+ return (normalized.length > 0 &&
488
+ !normalized.includes("url(") &&
489
+ !normalized.includes("javascript:") &&
490
+ !value.includes("<") &&
491
+ !value.includes(">") &&
492
+ !hasControlCharacters(value));
493
+ }
494
+ function hasControlCharacters(value) {
495
+ for (const char of value) {
496
+ const code = char.charCodeAt(0);
497
+ if (code < 32 || code === 127)
498
+ return true;
499
+ }
500
+ return false;
501
+ }
502
+ function validateRemoval(removal, errors, sceneId) {
503
+ if (!isValidIdentifier(removal.id)) {
504
+ errors.push(issue("INVALID_IDENTIFIER", `Element "${removal.id}" must be kebab-case`, {
505
+ sceneId,
506
+ elementId: removal.id,
507
+ }));
508
+ }
509
+ if (removal.exit !== undefined && !VALID_EXIT_ANIMATIONS.has(removal.exit)) {
510
+ errors.push(issue("UNKNOWN_ANIMATION", `Unknown exit animation "${removal.exit}"`, {
511
+ sceneId,
512
+ elementId: removal.id,
513
+ }));
514
+ }
515
+ }
516
+ function validateAmbient(ambient, errors, sceneId, elementId) {
517
+ validateAmbientWithSet(ambient, VALID_AMBIENT_ANIMATIONS, errors, sceneId, elementId);
518
+ }
519
+ function validateAmbientWithSet(ambient, validAnimations, errors, sceneId, elementId) {
520
+ for (const animation of ambient ?? []) {
521
+ if (!validAnimations.has(animation.name)) {
522
+ errors.push(issue("UNKNOWN_AMBIENT_ANIMATION", `Unknown ambient animation "${animation.name}"`, { sceneId, elementId }));
523
+ }
524
+ if (animation.infinite === false &&
525
+ (animation.iterations === undefined || !Number.isInteger(animation.iterations) || animation.iterations <= 0)) {
526
+ errors.push(issue("INVALID_AMBIENT_ITERATIONS", "Ambient iterations must be positive when infinite is false", {
527
+ sceneId,
528
+ elementId,
529
+ }));
530
+ }
531
+ }
532
+ }
533
+ function validateSceneObjectDeltas(document, errors) {
534
+ const elements = new Map();
535
+ const connectors = new Map();
536
+ const documentElementIds = collectDocumentElementIds(document);
537
+ const first = document.scenes[0];
538
+ if (!first?.elements)
539
+ return;
540
+ for (const element of first.elements) {
541
+ validatePlacement(element, document, errors, first.id);
542
+ if (elements.has(element.id)) {
543
+ errors.push(issue("DUPLICATE_ELEMENT_ID", `Duplicate element "${element.id}"`, {
544
+ sceneId: first.id,
545
+ elementId: element.id,
546
+ }));
547
+ }
548
+ elements.set(element.id, normalizePlacement(document, element));
549
+ }
550
+ for (const connection of first.connections ?? []) {
551
+ validateConnectionPlacement(connection, document, errors, first.id, elements, documentElementIds);
552
+ if (connectors.has(connection.id)) {
553
+ errors.push(issue("DUPLICATE_CONNECTOR_ID", `Duplicate connection "${connection.id}"`, {
554
+ sceneId: first.id,
555
+ elementId: connection.id,
556
+ }));
557
+ }
558
+ connectors.set(connection.id, normalizeConnectionPlacement(document, connection));
559
+ }
560
+ for (const scene of document.scenes.slice(1)) {
561
+ const updateIds = new Set();
562
+ for (const update of scene.update?.elements ?? []) {
563
+ const existing = elements.get(update.id)?.asset;
564
+ validatePatch(update, document, errors, scene.id, existing);
565
+ updateIds.add(update.id);
566
+ if (!elements.has(update.id)) {
567
+ errors.push(issue("ELEMENT_NOT_PRESENT", `Element "${update.id}" is not present`, {
568
+ sceneId: scene.id,
569
+ elementId: update.id,
570
+ }));
571
+ }
572
+ }
573
+ for (const removal of scene.remove?.elements ?? []) {
574
+ validateRemoval(removal, errors, scene.id);
575
+ if (updateIds.has(removal.id)) {
576
+ errors.push(issue("ELEMENT_DELTA_CONFLICT", `Element "${removal.id}" cannot be updated and removed in one scene`, {
577
+ sceneId: scene.id,
578
+ elementId: removal.id,
579
+ }));
580
+ }
581
+ if (!elements.has(removal.id)) {
582
+ errors.push(issue("ELEMENT_NOT_PRESENT", `Element "${removal.id}" is not present`, {
583
+ sceneId: scene.id,
584
+ elementId: removal.id,
585
+ }));
586
+ }
587
+ }
588
+ for (const add of scene.add?.elements ?? []) {
589
+ validatePlacement(add, document, errors, scene.id);
590
+ if (elements.has(add.id)) {
591
+ errors.push(issue("ELEMENT_ALREADY_PRESENT", `Element "${add.id}" is already present`, {
592
+ sceneId: scene.id,
593
+ elementId: add.id,
594
+ }));
595
+ }
596
+ }
597
+ const elementsForConnections = new Map(elements);
598
+ for (const update of scene.update?.elements ?? []) {
599
+ const existing = elementsForConnections.get(update.id);
600
+ if (existing) {
601
+ elementsForConnections.set(update.id, { ...existing, ...update });
602
+ }
603
+ }
604
+ for (const add of scene.add?.elements ?? []) {
605
+ elementsForConnections.set(add.id, normalizePlacement(document, add));
606
+ }
607
+ validateEndpointRemovalRule(scene, connectors, errors);
608
+ const connectorUpdateIds = new Set();
609
+ for (const update of scene.update?.connections ?? []) {
610
+ validateConnectionRemovalLikeId(update.id, errors, scene.id);
611
+ connectorUpdateIds.add(update.id);
612
+ const existing = connectors.get(update.id);
613
+ if (!existing) {
614
+ errors.push(issue("CONNECTOR_NOT_PRESENT", `Connection "${update.id}" is not present`, {
615
+ sceneId: scene.id,
616
+ elementId: update.id,
617
+ }));
618
+ validateConnectionPatch(update, document, errors, scene.id, elementsForConnections, documentElementIds);
619
+ continue;
620
+ }
621
+ validateConnectionPatch({
622
+ ...existing,
623
+ ...update,
624
+ style: mergeStyle(existing.style, update.style),
625
+ }, document, errors, scene.id, elementsForConnections, documentElementIds);
626
+ }
627
+ for (const removal of scene.remove?.connections ?? []) {
628
+ validateConnectionRemoval(removal, errors, scene.id);
629
+ if (connectorUpdateIds.has(removal.id)) {
630
+ errors.push(issue("CONNECTOR_DELTA_CONFLICT", `Connection "${removal.id}" cannot be updated and removed in one scene`, {
631
+ sceneId: scene.id,
632
+ elementId: removal.id,
633
+ }));
634
+ }
635
+ if (!connectors.has(removal.id)) {
636
+ errors.push(issue("CONNECTOR_NOT_PRESENT", `Connection "${removal.id}" is not present`, {
637
+ sceneId: scene.id,
638
+ elementId: removal.id,
639
+ }));
640
+ }
641
+ }
642
+ for (const add of scene.add?.connections ?? []) {
643
+ validateConnectionPlacement(add, document, errors, scene.id, elementsForConnections, documentElementIds);
644
+ if (connectors.has(add.id)) {
645
+ errors.push(issue("CONNECTOR_ALREADY_PRESENT", `Connection "${add.id}" is already present`, {
646
+ sceneId: scene.id,
647
+ elementId: add.id,
648
+ }));
649
+ }
650
+ }
651
+ for (const update of scene.update?.elements ?? []) {
652
+ const existing = elements.get(update.id);
653
+ if (existing) {
654
+ elements.set(update.id, { ...existing, ...update });
655
+ }
656
+ }
657
+ for (const add of scene.add?.elements ?? []) {
658
+ elements.set(add.id, normalizePlacement(document, add));
659
+ }
660
+ for (const update of scene.update?.connections ?? []) {
661
+ const existing = connectors.get(update.id);
662
+ if (existing) {
663
+ connectors.set(update.id, {
664
+ ...existing,
665
+ ...update,
666
+ style: mergeStyle(existing.style, update.style),
667
+ });
668
+ }
669
+ }
670
+ for (const add of scene.add?.connections ?? []) {
671
+ connectors.set(add.id, normalizeConnectionPlacement(document, add));
672
+ }
673
+ for (const removal of scene.remove?.connections ?? []) {
674
+ connectors.delete(removal.id);
675
+ }
676
+ for (const removal of scene.remove?.elements ?? []) {
677
+ elements.delete(removal.id);
678
+ }
679
+ }
680
+ }
681
+ function collectDocumentElementIds(document) {
682
+ const ids = new Set();
683
+ for (const scene of document.scenes) {
684
+ for (const element of scene.elements ?? [])
685
+ ids.add(element.id);
686
+ for (const element of scene.add?.elements ?? [])
687
+ ids.add(element.id);
688
+ }
689
+ return ids;
690
+ }
691
+ function validateEndpointRemovalRule(scene, connectors, errors) {
692
+ const removedElements = new Set((scene.remove?.elements ?? []).map((removal) => removal.id));
693
+ if (removedElements.size === 0)
694
+ return;
695
+ const removedConnections = new Set((scene.remove?.connections ?? []).map((removal) => removal.id));
696
+ for (const connection of connectors.values()) {
697
+ if (removedConnections.has(connection.id))
698
+ continue;
699
+ const endpointIds = [connection.from?.element, connection.to?.element];
700
+ if (endpointIds.some((id) => id !== undefined && removedElements.has(id))) {
701
+ errors.push(issue("CONNECTION_ENDPOINT_REMOVED", `Connection "${connection.id}" references an element removed in the same scene`, { sceneId: scene.id, elementId: connection.id }));
702
+ }
703
+ }
704
+ }
705
+ function validateConnectionPlacement(connection, document, errors, sceneId, elements, documentElementIds) {
706
+ validateConnectionCommon(connection, document, errors, sceneId, elements, documentElementIds);
707
+ }
708
+ function validateConnectionPatch(connection, document, errors, sceneId, elements, documentElementIds) {
709
+ validateConnectionCommon(connection, document, errors, sceneId, elements, documentElementIds);
710
+ }
711
+ function validateConnectionCommon(connection, document, errors, sceneId, elements, documentElementIds) {
712
+ validateConnectionRemovalLikeId(connection.id, errors, sceneId);
713
+ if (documentElementIds.has(connection.id)) {
714
+ errors.push(issue("DUPLICATE_SCENE_OBJECT_ID", `Connection "${connection.id}" collides with an element id`, {
715
+ sceneId,
716
+ elementId: connection.id,
717
+ }));
718
+ }
719
+ if (connection.layer !== undefined && !declaredLayerNames(document).has(connection.layer)) {
720
+ errors.push(issue("LAYER_NOT_FOUND", `Layer "${connection.layer}" is not declared`, {
721
+ sceneId,
722
+ elementId: connection.id,
723
+ layerName: connection.layer,
724
+ }));
725
+ }
726
+ if (connection.enter !== undefined && !VALID_ENTRY_ANIMATIONS.has(connection.enter)) {
727
+ errors.push(issue("UNKNOWN_ANIMATION", `Unknown entry animation "${connection.enter}"`, {
728
+ sceneId,
729
+ elementId: connection.id,
730
+ }));
731
+ }
732
+ if (connection.exit !== undefined && !VALID_EXIT_ANIMATIONS.has(connection.exit)) {
733
+ errors.push(issue("UNKNOWN_ANIMATION", `Unknown exit animation "${connection.exit}"`, {
734
+ sceneId,
735
+ elementId: connection.id,
736
+ }));
737
+ }
738
+ validateAmbientWithSet(connection.ambient, VALID_CONNECTOR_AMBIENT_ANIMATIONS, errors, sceneId, connection.id);
739
+ validateConnectionRouteSource(connection, errors, sceneId);
740
+ validateConnectionEndpoints(connection, elements, errors, sceneId);
741
+ validateConnectionRouting(connection, errors, sceneId);
742
+ validateConnectorStyle(connection.style, errors, sceneId, connection.id);
743
+ if (connection.start !== undefined && !VALID_CONNECTOR_ENDPOINTS.has(connection.start)) {
744
+ errors.push(issue("INVALID_CONNECTOR_ENDPOINT", "Invalid connector start endpoint", {
745
+ sceneId,
746
+ elementId: connection.id,
747
+ }));
748
+ }
749
+ if (connection.end !== undefined && !VALID_CONNECTOR_ENDPOINTS.has(connection.end)) {
750
+ errors.push(issue("INVALID_CONNECTOR_ENDPOINT", "Invalid connector end endpoint", {
751
+ sceneId,
752
+ elementId: connection.id,
753
+ }));
754
+ }
755
+ if (connection.direction !== undefined && !VALID_CONNECTOR_DIRECTIONS.has(connection.direction)) {
756
+ errors.push(issue("INVALID_CONNECTOR_DIRECTION", "Invalid connector direction", {
757
+ sceneId,
758
+ elementId: connection.id,
759
+ }));
760
+ }
761
+ }
762
+ function validateConnectionRouteSource(connection, errors, sceneId) {
763
+ const hasRoute = connection.route !== undefined;
764
+ const hasEndpointRoute = connection.from !== undefined || connection.to !== undefined;
765
+ if (hasRoute === hasEndpointRoute || (hasEndpointRoute && (!connection.from || !connection.to))) {
766
+ errors.push(issue("INVALID_CONNECTOR_ROUTE", "Connection must use either route or both from and to", {
767
+ sceneId,
768
+ elementId: connection.id,
769
+ }));
770
+ }
771
+ if (connection.route !== undefined) {
772
+ if (connection.route.length < 2 ||
773
+ connection.route.some((point) => !isValidPosition(point) || point.some((coordinate) => !Number.isInteger(coordinate)))) {
774
+ errors.push(issue("INVALID_CONNECTOR_ROUTE", "Manual connection route must contain at least two whole-grid points", {
775
+ sceneId,
776
+ elementId: connection.id,
777
+ }));
778
+ }
779
+ else if (!isGridAxisRoute(connection.route)) {
780
+ errors.push(issue("INVALID_CONNECTOR_ROUTE", "Manual connection route segments must follow one grid axis at a time", {
781
+ sceneId,
782
+ elementId: connection.id,
783
+ }));
784
+ }
785
+ }
786
+ }
787
+ function validateConnectionEndpoints(connection, elements, errors, sceneId) {
788
+ for (const key of ["from", "to"]) {
789
+ const endpoint = connection[key];
790
+ if (endpoint === undefined)
791
+ continue;
792
+ const hasElement = endpoint.element !== undefined;
793
+ const hasAt = endpoint.at !== undefined;
794
+ if (hasElement === hasAt) {
795
+ errors.push(issue("INVALID_CONNECTOR_ROUTE", "Connection endpoint must use exactly one of element or at", {
796
+ sceneId,
797
+ elementId: connection.id,
798
+ }));
799
+ }
800
+ if (endpoint.element !== undefined && !elements.has(endpoint.element)) {
801
+ errors.push(issue("CONNECTOR_ENDPOINT_NOT_FOUND", `Connection endpoint "${endpoint.element}" was not found`, {
802
+ sceneId,
803
+ elementId: connection.id,
804
+ }));
805
+ }
806
+ if (endpoint.at !== undefined && !isValidPosition(endpoint.at)) {
807
+ errors.push(issue("INVALID_CONNECTOR_ROUTE", "Endpoint at must be non-negative", {
808
+ sceneId,
809
+ elementId: connection.id,
810
+ }));
811
+ }
812
+ if (endpoint.side !== undefined && !VALID_CONNECTOR_SIDES.has(endpoint.side)) {
813
+ errors.push(issue("INVALID_CONNECTOR_ROUTE", "Invalid endpoint side", {
814
+ sceneId,
815
+ elementId: connection.id,
816
+ }));
817
+ }
818
+ if (endpoint.offset !== undefined &&
819
+ (!Number.isFinite(endpoint.offset) || endpoint.offset < -0.5 || endpoint.offset > 0.5)) {
820
+ errors.push(issue("INVALID_CONNECTOR_ROUTE", "Invalid endpoint offset", {
821
+ sceneId,
822
+ elementId: connection.id,
823
+ }));
824
+ }
825
+ }
826
+ }
827
+ function validateConnectionRouting(connection, errors, sceneId) {
828
+ const routing = connection.routing;
829
+ if (routing === undefined)
830
+ return;
831
+ if (connection.route !== undefined) {
832
+ errors.push(issue("INVALID_CONNECTOR_ROUTING", "Routing config is valid only with endpoint-routed connections", {
833
+ sceneId,
834
+ elementId: connection.id,
835
+ }));
836
+ }
837
+ if (routing.mode !== undefined && !VALID_CONNECTOR_ROUTING_MODES.has(routing.mode)) {
838
+ errors.push(issue("INVALID_CONNECTOR_ROUTING", "Invalid connector routing mode", {
839
+ sceneId,
840
+ elementId: connection.id,
841
+ }));
842
+ }
843
+ if (routing.avoid !== undefined) {
844
+ const avoid = routing.avoid;
845
+ if (!(avoid === "objects" || avoid === "none" || Array.isArray(avoid))) {
846
+ errors.push(issue("INVALID_CONNECTOR_ROUTING", "Invalid connector routing avoid", {
847
+ sceneId,
848
+ elementId: connection.id,
849
+ }));
850
+ }
851
+ }
852
+ for (const [name, value] of [
853
+ ["clearance", routing.clearance],
854
+ ["gridStep", routing.gridStep],
855
+ ["maxBends", routing.maxBends],
856
+ ]) {
857
+ if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
858
+ errors.push(issue("INVALID_CONNECTOR_ROUTING", `Invalid connector routing ${name}`, { sceneId, elementId: connection.id }));
859
+ }
860
+ }
861
+ if (routing.prefer !== undefined && !VALID_CONNECTOR_ROUTING_PREFERENCES.has(routing.prefer)) {
862
+ errors.push(issue("INVALID_CONNECTOR_ROUTING", "Invalid connector routing preference", {
863
+ sceneId,
864
+ elementId: connection.id,
865
+ }));
866
+ }
867
+ }
868
+ function validateConnectorStyle(style, errors, sceneId, connectionId) {
869
+ if (style === undefined)
870
+ return;
871
+ if (style.pattern !== undefined && !VALID_CONNECTOR_PATTERNS.has(style.pattern)) {
872
+ errors.push(issue("INVALID_CONNECTOR_STYLE", "Invalid connector pattern", {
873
+ sceneId,
874
+ elementId: connectionId,
875
+ }));
876
+ }
877
+ if (style.variant !== undefined && !VALID_CONNECTOR_VARIANTS.has(style.variant)) {
878
+ errors.push(issue("INVALID_CONNECTOR_STYLE", "Invalid connector variant", {
879
+ sceneId,
880
+ elementId: connectionId,
881
+ }));
882
+ }
883
+ if (style.lane !== undefined && !VALID_CONNECTOR_LANES.has(style.lane)) {
884
+ errors.push(issue("INVALID_CONNECTOR_STYLE", "Invalid connector lane", {
885
+ sceneId,
886
+ elementId: connectionId,
887
+ }));
888
+ }
889
+ for (const [name, value] of [
890
+ ["strokeWidth", style.strokeWidth],
891
+ ["outlineWidth", style.outlineWidth],
892
+ ]) {
893
+ if (value !== undefined && !isValidPositiveNumber(value)) {
894
+ errors.push(issue("INVALID_CONNECTOR_STYLE", `Invalid connector ${name}`, {
895
+ sceneId,
896
+ elementId: connectionId,
897
+ }));
898
+ }
899
+ }
900
+ if (style.opacity !== undefined && (!Number.isFinite(style.opacity) || style.opacity < 0 || style.opacity > 1)) {
901
+ errors.push(issue("INVALID_CONNECTOR_STYLE", "Connector opacity must be 0..1", {
902
+ sceneId,
903
+ elementId: connectionId,
904
+ }));
905
+ }
906
+ if (style.dash !== undefined &&
907
+ (!isValidPositiveTuple(style.dash) || style.dash.some((part) => !Number.isFinite(part)))) {
908
+ errors.push(issue("INVALID_CONNECTOR_STYLE", "Invalid connector dash", {
909
+ sceneId,
910
+ elementId: connectionId,
911
+ }));
912
+ }
913
+ for (const token of [style.stroke, style.outline]) {
914
+ if (token !== undefined && !isSafeTextStyleToken(token)) {
915
+ errors.push(issue("INVALID_CONNECTOR_STYLE", "Connector color token is unsafe", {
916
+ sceneId,
917
+ elementId: connectionId,
918
+ }));
919
+ }
920
+ }
921
+ }
922
+ function validateConnectionRemoval(removal, errors, sceneId) {
923
+ validateConnectionRemovalLikeId(removal.id, errors, sceneId);
924
+ if (removal.exit !== undefined && !VALID_EXIT_ANIMATIONS.has(removal.exit)) {
925
+ errors.push(issue("UNKNOWN_ANIMATION", `Unknown exit animation "${removal.exit}"`, {
926
+ sceneId,
927
+ elementId: removal.id,
928
+ }));
929
+ }
930
+ }
931
+ function validateConnectionRemovalLikeId(id, errors, sceneId) {
932
+ if (!isValidIdentifier(id)) {
933
+ errors.push(issue("INVALID_IDENTIFIER", `Connection "${id}" must be kebab-case`, {
934
+ sceneId,
935
+ elementId: id,
936
+ }));
937
+ }
938
+ }
939
+ function mergeStyle(base, patch) {
940
+ if (base === undefined)
941
+ return patch;
942
+ if (patch === undefined)
943
+ return base;
944
+ return { ...base, ...patch };
945
+ }
946
+ function validateWarnings(document, warnings) {
947
+ const usedAssets = new Set();
948
+ const usedLayers = new Set();
949
+ const snapshots = resolveSceneSnapshots(document);
950
+ if (document.header.floor?.asset && document.header.floor.visible !== false) {
951
+ usedAssets.add(document.header.floor.asset);
952
+ }
953
+ for (const snapshot of snapshots) {
954
+ for (const element of snapshot.elements) {
955
+ usedAssets.add(element.asset);
956
+ if (isBuiltInAsset(element.asset)) {
957
+ usedAssets.delete(element.asset);
958
+ }
959
+ usedLayers.add(element.layer);
960
+ if (document.header.floor?.size) {
961
+ const [columns, rows] = document.header.floor.size;
962
+ if (element.pos[0] + element.size > columns || element.pos[1] + element.size > rows) {
963
+ warnings.push(issue("ELEMENT_OUTSIDE_FLOOR", `Element "${element.id}" is outside floor bounds`, {
964
+ sceneId: snapshot.id,
965
+ elementId: element.id,
966
+ }));
967
+ }
968
+ }
969
+ }
970
+ for (const connector of snapshot.connectors) {
971
+ usedLayers.add(connector.layer);
972
+ if (document.header.floor?.size) {
973
+ const [columns, rows] = document.header.floor.size;
974
+ if (connector.route.some((point) => point[0] > columns || point[1] > rows)) {
975
+ warnings.push(issue("CONNECTOR_OUTSIDE_FLOOR", `Connection "${connector.id}" is outside floor bounds`, {
976
+ sceneId: snapshot.id,
977
+ elementId: connector.id,
978
+ }));
979
+ }
980
+ }
981
+ }
982
+ }
983
+ for (const asset of document.header.assets) {
984
+ if (!usedAssets.has(asset.id)) {
985
+ warnings.push(issue("UNREFERENCED_ASSET", `Asset "${asset.id}" is not used`, {
986
+ assetName: asset.id,
987
+ }));
988
+ }
989
+ }
990
+ for (const layer of document.header.layers) {
991
+ if (!usedLayers.has(layer.name) && layer.name !== defaultFloorLayer$1(document)) {
992
+ warnings.push(issue("UNREFERENCED_LAYER", `Layer "${layer.name}" is not used`, {
993
+ layerName: layer.name,
994
+ }));
995
+ }
996
+ }
997
+ }
998
+ function validateScene(document) {
999
+ const errors = [];
1000
+ const warnings = [];
1001
+ validateHeader(document, errors);
1002
+ validateTimelineShape(document, errors);
1003
+ validateSceneObjectDeltas(document, errors);
1004
+ if (errors.length === 0) {
1005
+ validateWarnings(document, warnings);
1006
+ }
1007
+ return {
1008
+ errors,
1009
+ warnings,
1010
+ isValid: errors.length === 0,
1011
+ };
1012
+ }
1013
+ function resolveSceneSnapshots(document) {
1014
+ const progress = deriveProgresses(document.scenes);
1015
+ const currentElements = new Map();
1016
+ const currentConnectors = new Map();
1017
+ const snapshots = [];
1018
+ for (const [index, scene] of document.scenes.entries()) {
1019
+ const exiting = [];
1020
+ const exitingConnectors = [];
1021
+ if (index === 0) {
1022
+ for (const element of scene.elements ?? []) {
1023
+ currentElements.set(element.id, normalizePlacement(document, element));
1024
+ }
1025
+ for (const connection of scene.connections ?? []) {
1026
+ currentConnectors.set(connection.id, normalizeConnectionPlacement(document, connection));
1027
+ }
1028
+ }
1029
+ else {
1030
+ for (const patch of scene.update?.elements ?? []) {
1031
+ const existing = currentElements.get(patch.id);
1032
+ if (existing) {
1033
+ currentElements.set(patch.id, { ...existing, ...patch });
1034
+ }
1035
+ }
1036
+ for (const element of scene.add?.elements ?? []) {
1037
+ currentElements.set(element.id, normalizePlacement(document, element));
1038
+ }
1039
+ for (const patch of scene.update?.connections ?? []) {
1040
+ const existing = currentConnectors.get(patch.id);
1041
+ if (existing) {
1042
+ currentConnectors.set(patch.id, {
1043
+ ...existing,
1044
+ ...patch,
1045
+ style: mergeStyle(existing.style, patch.style),
1046
+ });
1047
+ }
1048
+ }
1049
+ for (const connection of scene.add?.connections ?? []) {
1050
+ currentConnectors.set(connection.id, normalizeConnectionPlacement(document, connection));
1051
+ }
1052
+ for (const removal of scene.remove?.elements ?? []) {
1053
+ const existing = currentElements.get(removal.id);
1054
+ if (existing) {
1055
+ exiting.push(toRuntimeState(existing, "exiting", removal.exit));
1056
+ }
1057
+ }
1058
+ for (const removal of scene.remove?.connections ?? []) {
1059
+ const existing = currentConnectors.get(removal.id);
1060
+ if (existing) {
1061
+ exitingConnectors.push(toRuntimeConnectorState(document, existing, currentElements, "exiting", removal.exit));
1062
+ }
1063
+ }
1064
+ }
1065
+ const addedIds = new Set((scene.add?.elements ?? []).map((element) => element.id));
1066
+ const addedConnectorIds = new Set((scene.add?.connections ?? []).map((connection) => connection.id));
1067
+ const snapshotElements = Array.from(currentElements.values()).map((element) => toRuntimeState(element, index > 0 && addedIds.has(element.id) ? "entering" : "present"));
1068
+ const snapshotConnectors = Array.from(currentConnectors.values()).map((connection) => toRuntimeConnectorState(document, connection, currentElements, index > 0 && addedConnectorIds.has(connection.id) ? "entering" : "present"));
1069
+ for (const removed of exiting) {
1070
+ const existingIndex = snapshotElements.findIndex((element) => element.id === removed.id);
1071
+ if (existingIndex >= 0) {
1072
+ snapshotElements[existingIndex] = removed;
1073
+ }
1074
+ else {
1075
+ snapshotElements.push(removed);
1076
+ }
1077
+ }
1078
+ for (const removed of exitingConnectors) {
1079
+ const existingIndex = snapshotConnectors.findIndex((connector) => connector.id === removed.id);
1080
+ if (existingIndex >= 0) {
1081
+ snapshotConnectors[existingIndex] = removed;
1082
+ }
1083
+ else {
1084
+ snapshotConnectors.push(removed);
1085
+ }
1086
+ }
1087
+ snapshots.push({
1088
+ id: scene.id,
1089
+ progress: progress[index],
1090
+ elements: snapshotElements,
1091
+ connectors: snapshotConnectors,
1092
+ });
1093
+ for (const removal of scene.remove?.connections ?? []) {
1094
+ currentConnectors.delete(removal.id);
1095
+ }
1096
+ for (const removal of scene.remove?.elements ?? []) {
1097
+ currentElements.delete(removal.id);
1098
+ }
1099
+ }
1100
+ return snapshots;
1101
+ }
1102
+ function deriveProgresses(scenes) {
1103
+ if (scenes.length <= 1) {
1104
+ return scenes.map(() => 0);
1105
+ }
1106
+ return scenes.map((_, index) => index / (scenes.length - 1));
1107
+ }
1108
+ function normalizePlacement(document, element) {
1109
+ return {
1110
+ id: element.id,
1111
+ asset: element.asset,
1112
+ at: element.at,
1113
+ size: element.size,
1114
+ layer: element.layer ?? defaultElementLayer(document),
1115
+ enter: element.enter,
1116
+ exit: element.exit,
1117
+ ambient: element.ambient,
1118
+ text: element.text,
1119
+ primitive: element.primitive,
1120
+ };
1121
+ }
1122
+ function normalizeConnectionPlacement(document, connection) {
1123
+ return {
1124
+ id: connection.id,
1125
+ route: connection.route,
1126
+ from: connection.from,
1127
+ to: connection.to,
1128
+ routing: connection.routing,
1129
+ layer: connection.layer ?? defaultConnectorLayer(document),
1130
+ style: connection.style,
1131
+ start: connection.start,
1132
+ end: connection.end,
1133
+ direction: connection.direction,
1134
+ enter: connection.enter,
1135
+ exit: connection.exit,
1136
+ ambient: connection.ambient,
1137
+ };
1138
+ }
1139
+ function toRuntimeState(element, presence, exit) {
1140
+ return {
1141
+ id: element.id,
1142
+ asset: element.asset,
1143
+ pos: element.at,
1144
+ size: element.size ?? 1,
1145
+ layer: element.layer ?? "",
1146
+ presence,
1147
+ enter: presence === "entering" ? (element.enter ?? "fade-in") : element.enter,
1148
+ exit: presence === "exiting" ? (exit ?? element.exit ?? "fade-out") : (exit ?? element.exit),
1149
+ ambient: element.ambient,
1150
+ text: element.text,
1151
+ primitive: element.primitive,
1152
+ };
1153
+ }
1154
+ function toRuntimeConnectorState(document, connection, elements, presence, exit) {
1155
+ return {
1156
+ id: connection.id,
1157
+ route: resolveConnectorRoute(connection, elements),
1158
+ layer: connection.layer ?? defaultConnectorLayer(document),
1159
+ presence,
1160
+ style: resolveConnectorStyle(connection.style),
1161
+ start: connection.start ?? "none",
1162
+ end: connection.end ?? "arrow",
1163
+ direction: connection.direction ?? "route",
1164
+ enter: presence === "entering" ? (connection.enter ?? "fade-in") : (connection.enter ?? "fade-in"),
1165
+ exit: presence === "exiting" ? (exit ?? connection.exit ?? "fade-out") : (exit ?? connection.exit ?? "fade-out"),
1166
+ ambient: connection.ambient,
1167
+ };
1168
+ }
1169
+ function resolveConnectorStyle(style) {
1170
+ const variant = style?.variant ?? "line";
1171
+ const pattern = style?.pattern ?? "solid";
1172
+ const resolved = {
1173
+ variant,
1174
+ pattern,
1175
+ stroke: style?.stroke ?? "#2563eb",
1176
+ strokeWidth: style?.strokeWidth ?? (variant === "road" ? 14 : 3),
1177
+ opacity: style?.opacity ?? 1,
1178
+ outlineWidth: style?.outlineWidth ?? (variant === "road" ? 2 : 0),
1179
+ lane: style?.lane ?? "none",
1180
+ };
1181
+ const dash = style?.dash ?? defaultDash(pattern);
1182
+ if (dash) {
1183
+ resolved.dash = dash;
1184
+ }
1185
+ const outline = style?.outline ?? (variant === "road" ? "#ffffff" : undefined);
1186
+ if (outline) {
1187
+ resolved.outline = outline;
1188
+ }
1189
+ return resolved;
1190
+ }
1191
+ function defaultDash(pattern) {
1192
+ if (pattern === "dashed")
1193
+ return [12, 8];
1194
+ if (pattern === "dotted")
1195
+ return [0, 8];
1196
+ return undefined;
1197
+ }
1198
+ function resolveConnectorRoute(connection, elements) {
1199
+ if (connection.route)
1200
+ return connection.route;
1201
+ if (!connection.from || !connection.to)
1202
+ return [];
1203
+ const routing = connection.routing ?? {};
1204
+ const avoid = routing.avoid ?? "objects";
1205
+ const clearance = routing.clearance ?? 0.5;
1206
+ const obstacles = avoid === "none" ? [] : collectObstacles(elements, connection.from, connection.to, avoid, clearance);
1207
+ const starts = endpointCandidates(connection.from, elements);
1208
+ const ends = endpointCandidates(connection.to, elements);
1209
+ let best;
1210
+ let bestScore = Number.POSITIVE_INFINITY;
1211
+ for (const start of starts) {
1212
+ for (const end of ends) {
1213
+ const route = routeBetween(start, end, obstacles, routing.mode ?? "orthogonal", clearance);
1214
+ const score = routeScore(route, obstacles);
1215
+ if (score < bestScore) {
1216
+ best = route;
1217
+ bestScore = score;
1218
+ }
1219
+ }
1220
+ }
1221
+ return simplifyRoute(best ?? [starts[0]?.point ?? [0, 0], ends[0]?.point ?? [0, 0]]);
1222
+ }
1223
+ function endpointCandidates(endpoint, elements) {
1224
+ if (endpoint.at)
1225
+ return [{ point: endpoint.at, sideRank: 0 }];
1226
+ const element = endpoint.element ? elements.get(endpoint.element) : undefined;
1227
+ if (!element)
1228
+ return [{ point: [0, 0], sideRank: 0 }];
1229
+ const sides = endpoint.side && endpoint.side !== "auto"
1230
+ ? [normalizeSide(endpoint.side)]
1231
+ : ["right", "left", "bottom", "top"];
1232
+ return sides.map((side, index) => ({
1233
+ point: portForSide(element, side, endpoint.offset ?? 0),
1234
+ normal: normalForSide(side),
1235
+ sideRank: index,
1236
+ }));
1237
+ }
1238
+ function normalizeSide(side) {
1239
+ if (side === "front")
1240
+ return "bottom";
1241
+ if (side === "back")
1242
+ return "top";
1243
+ if (side === "auto")
1244
+ return "right";
1245
+ return side;
1246
+ }
1247
+ function portForSide(element, side, offset) {
1248
+ const x = element.at[0];
1249
+ const y = element.at[1];
1250
+ const size = element.size ?? 1;
1251
+ if (side === "top")
1252
+ return [x + size * (0.5 + offset), y];
1253
+ if (side === "right")
1254
+ return [x + size, y + size * (0.5 + offset)];
1255
+ if (side === "bottom")
1256
+ return [x + size * (0.5 - offset), y + size];
1257
+ return [x, y + size * (0.5 - offset)];
1258
+ }
1259
+ function normalForSide(side) {
1260
+ if (side === "top")
1261
+ return [0, -1];
1262
+ if (side === "right")
1263
+ return [1, 0];
1264
+ if (side === "bottom")
1265
+ return [0, 1];
1266
+ return [-1, 0];
1267
+ }
1268
+ function collectObstacles(elements, from, to, avoid, clearance) {
1269
+ const endpointIds = new Set([from.element, to.element].filter(Boolean));
1270
+ const avoidIds = Array.isArray(avoid) ? new Set(avoid) : undefined;
1271
+ const obstacles = [];
1272
+ for (const element of elements.values()) {
1273
+ if (endpointIds.has(element.id) || element.asset === BUILT_IN_TEXT_ASSET_ID$1) {
1274
+ continue;
1275
+ }
1276
+ if (avoidIds && !avoidIds.has(element.id))
1277
+ continue;
1278
+ const size = element.size ?? 1;
1279
+ obstacles.push({
1280
+ minX: element.at[0] - clearance,
1281
+ minY: element.at[1] - clearance,
1282
+ maxX: element.at[0] + size + clearance,
1283
+ maxY: element.at[1] + size + clearance,
1284
+ });
1285
+ }
1286
+ return obstacles;
1287
+ }
1288
+ function routeBetween(start, end, obstacles, mode = "orthogonal", clearance = 0.5) {
1289
+ const startExit = offsetPort(start, clearance);
1290
+ const endEntry = offsetPort(end, clearance);
1291
+ const direct = [startExit, endEntry];
1292
+ if (mode === "straight")
1293
+ return direct;
1294
+ if (isGridAxisRoute(direct) && !routeIntersectsObstacles(direct, obstacles)) {
1295
+ return withEndpointPorts(start, end, direct);
1296
+ }
1297
+ const candidates = [
1298
+ [startExit, [endEntry[0], startExit[1]], endEntry],
1299
+ [startExit, [startExit[0], endEntry[1]], endEntry],
1300
+ ];
1301
+ const blocking = obstacles.find((obstacle) => segmentIntersectsRect(startExit, endEntry, obstacle));
1302
+ if (blocking) {
1303
+ const leftX = Math.max(0, blocking.minX);
1304
+ const rightX = Math.max(0, blocking.maxX);
1305
+ const topY = Math.max(0, blocking.minY);
1306
+ const bottomY = Math.max(0, blocking.maxY);
1307
+ candidates.push([startExit, [leftX, startExit[1]], [leftX, endEntry[1]], endEntry], [startExit, [rightX, startExit[1]], [rightX, endEntry[1]], endEntry], [startExit, [startExit[0], topY], [endEntry[0], topY], endEntry], [startExit, [startExit[0], bottomY], [endEntry[0], bottomY], endEntry]);
1308
+ }
1309
+ const route = candidates
1310
+ .map((route) => simplifyRoute(route))
1311
+ .sort((a, b) => routeScore(a, obstacles) - routeScore(b, obstacles) || routeLength(a) - routeLength(b))[0] ??
1312
+ direct;
1313
+ return withEndpointPorts(start, end, route);
1314
+ }
1315
+ function offsetPort(endpoint, clearance) {
1316
+ if (!endpoint.normal || clearance <= 0)
1317
+ return endpoint.point;
1318
+ return [endpoint.point[0] + endpoint.normal[0] * clearance, endpoint.point[1] + endpoint.normal[1] * clearance];
1319
+ }
1320
+ function withEndpointPorts(start, end, route) {
1321
+ return simplifyRoute([start.point, ...route, end.point]);
1322
+ }
1323
+ function routeScore(route, obstacles) {
1324
+ const intersections = routeIntersectsObstacles(route, obstacles) ? 1000 : 0;
1325
+ return intersections + bendCount(route) * 10 + routeLength(route);
1326
+ }
1327
+ function routeLength(route) {
1328
+ let length = 0;
1329
+ for (let index = 1; index < route.length; index += 1) {
1330
+ length += Math.abs(route[index][0] - route[index - 1][0]) + Math.abs(route[index][1] - route[index - 1][1]);
1331
+ }
1332
+ return length;
1333
+ }
1334
+ function bendCount(route) {
1335
+ let bends = 0;
1336
+ for (let index = 2; index < route.length; index += 1) {
1337
+ const a = route[index - 2];
1338
+ const b = route[index - 1];
1339
+ const c = route[index];
1340
+ if ((b[0] - a[0]) * (c[1] - b[1]) !== (b[1] - a[1]) * (c[0] - b[0])) {
1341
+ bends += 1;
1342
+ }
1343
+ }
1344
+ return bends;
1345
+ }
1346
+ function routeIntersectsObstacles(route, obstacles) {
1347
+ for (let index = 1; index < route.length; index += 1) {
1348
+ if (obstacles.some((obstacle) => segmentIntersectsRect(route[index - 1], route[index], obstacle))) {
1349
+ return true;
1350
+ }
1351
+ }
1352
+ return false;
1353
+ }
1354
+ function segmentIntersectsRect(a, b, rect) {
1355
+ if ((a[0] < rect.minX && b[0] < rect.minX) ||
1356
+ (a[0] > rect.maxX && b[0] > rect.maxX) ||
1357
+ (a[1] < rect.minY && b[1] < rect.minY) ||
1358
+ (a[1] > rect.maxY && b[1] > rect.maxY)) {
1359
+ return false;
1360
+ }
1361
+ if (pointInsideRect(a, rect) || pointInsideRect(b, rect))
1362
+ return true;
1363
+ const corners = [
1364
+ [rect.minX, rect.minY],
1365
+ [rect.maxX, rect.minY],
1366
+ [rect.maxX, rect.maxY],
1367
+ [rect.minX, rect.maxY],
1368
+ ];
1369
+ return [
1370
+ [corners[0], corners[1]],
1371
+ [corners[1], corners[2]],
1372
+ [corners[2], corners[3]],
1373
+ [corners[3], corners[0]],
1374
+ ].some(([c, d]) => segmentsIntersect(a, b, c, d));
1375
+ }
1376
+ function pointInsideRect(point, rect) {
1377
+ return point[0] > rect.minX && point[0] < rect.maxX && point[1] > rect.minY && point[1] < rect.maxY;
1378
+ }
1379
+ function segmentsIntersect(a, b, c, d) {
1380
+ const ccw = (p1, p2, p3) => (p3[1] - p1[1]) * (p2[0] - p1[0]) > (p2[1] - p1[1]) * (p3[0] - p1[0]);
1381
+ return ccw(a, c, d) !== ccw(b, c, d) && ccw(a, b, c) !== ccw(a, b, d);
1382
+ }
1383
+ function simplifyRoute(route) {
1384
+ const simplified = [];
1385
+ for (const point of route) {
1386
+ const previous = simplified[simplified.length - 1];
1387
+ if (previous && previous[0] === point[0] && previous[1] === point[1]) {
1388
+ continue;
1389
+ }
1390
+ simplified.push(point);
1391
+ while (simplified.length >= 3) {
1392
+ const a = simplified[simplified.length - 3];
1393
+ const b = simplified[simplified.length - 2];
1394
+ const c = simplified[simplified.length - 1];
1395
+ if ((b[0] - a[0]) * (c[1] - b[1]) === (b[1] - a[1]) * (c[0] - b[0])) {
1396
+ simplified.splice(simplified.length - 2, 1);
1397
+ }
1398
+ else {
1399
+ break;
1400
+ }
1401
+ }
1402
+ }
1403
+ return simplified;
1404
+ }
1405
+ function isGridAxisRoute(route) {
1406
+ for (let index = 1; index < route.length; index += 1) {
1407
+ const previous = route[index - 1];
1408
+ const current = route[index];
1409
+ if (previous[0] !== current[0] && previous[1] !== current[1]) {
1410
+ return false;
1411
+ }
1412
+ }
1413
+ return true;
1414
+ }
1415
+
1416
+ const DEFAULT_VERSION = "0.1.0";
1417
+ const RUNTIME_BUNDLE_FORMAT = "isostate-runtime-bundle";
1418
+ const BUILT_IN_TEXT_ASSET_ID = "text";
1419
+ const BUILT_IN_PRIMITIVE_ASSET_IDS = new Set(["rectangle", "circle", "polygon", "line"]);
1420
+ function compileScene(document, options = {}) {
1421
+ const { version = DEFAULT_VERSION } = options;
1422
+ const scenes = resolveSceneSnapshots(document).map((scene) => normalizeValue(scene));
1423
+ const bundleWithoutDigest = {
1424
+ _format: RUNTIME_BUNDLE_FORMAT,
1425
+ _version: version,
1426
+ grid: { cellSize: document.header.grid?.cellSize ?? 64 },
1427
+ floor: compileFloor(document, scenes),
1428
+ layout: compileLayout(),
1429
+ theme: document.header.theme ?? "light",
1430
+ layers: compileLayers(document.header.layers),
1431
+ scenes,
1432
+ };
1433
+ if (document.header.className) {
1434
+ bundleWithoutDigest.className = document.header.className;
1435
+ }
1436
+ const assets = compileUrlAssets(document, scenes);
1437
+ if (Object.keys(assets).length > 0) {
1438
+ bundleWithoutDigest.assets = assets;
1439
+ }
1440
+ return {
1441
+ ...bundleWithoutDigest,
1442
+ _digest: digestBundle(bundleWithoutDigest),
1443
+ };
1444
+ }
1445
+ function compileUrlAssets(document, scenes) {
1446
+ const assets = {};
1447
+ for (const name of uniqueReferencedAssetNames(document, scenes)) {
1448
+ const url = resolveAssetUrl(document, name);
1449
+ if (!url) {
1450
+ throw new ValidationErrorClass("ASSET_URL_REQUIRED", `Asset "${name}" must resolve through header.assetBaseUrl`, {
1451
+ asset: name,
1452
+ });
1453
+ }
1454
+ const entry = document.header.assets.find((asset) => asset.id === name);
1455
+ assets[name] = normalizeValue({
1456
+ url,
1457
+ ...(entry?.anchor ? { anchor: entry.anchor } : {}),
1458
+ });
1459
+ }
1460
+ return assets;
1461
+ }
1462
+ function toJs(bundle, options = {}) {
1463
+ const { minify = true } = options;
1464
+ const json = canonicalStringify(bundle, minify ? undefined : 2);
1465
+ return `export default ${json};`;
1466
+ }
1467
+ function toJson(bundle) {
1468
+ return canonicalStringify(bundle, 2);
1469
+ }
1470
+ function fromJs(moduleString) {
1471
+ if (!moduleString.startsWith("export default ") || !moduleString.endsWith(";")) {
1472
+ throw new ValidationErrorClass("INVALID_RUNTIME_BUNDLE_MODULE", "Invalid JS module: expected exact default export");
1473
+ }
1474
+ const jsonString = moduleString.slice("export default ".length, -1);
1475
+ const bundle = parseRuntimeBundleJson(jsonString, "INVALID_RUNTIME_BUNDLE_MODULE");
1476
+ const minify = !jsonString.includes("\n");
1477
+ if (moduleString !== toJs(bundle, { minify })) {
1478
+ throw new ValidationErrorClass("INVALID_RUNTIME_BUNDLE_MODULE", "Invalid JS module: non-canonical bundle export");
1479
+ }
1480
+ return bundle;
1481
+ }
1482
+ function fromJson(jsonString) {
1483
+ const bundle = parseRuntimeBundleJson(jsonString, "INVALID_RUNTIME_BUNDLE_JSON");
1484
+ if (jsonString !== toJson(bundle)) {
1485
+ throw new ValidationErrorClass("INVALID_RUNTIME_BUNDLE_JSON", "Invalid JSON bundle: non-canonical serialization");
1486
+ }
1487
+ return bundle;
1488
+ }
1489
+ function parseRuntimeBundleJson(jsonString, code) {
1490
+ try {
1491
+ return JSON.parse(jsonString);
1492
+ }
1493
+ catch {
1494
+ throw new ValidationErrorClass(code, "Invalid runtime bundle: expected valid JSON");
1495
+ }
1496
+ }
1497
+ function compileFloor(document, scenes) {
1498
+ const floor = document.header.floor ?? {};
1499
+ const origin = floor.origin ?? [0, 0];
1500
+ const compiled = {
1501
+ size: floor.size ?? deriveFloorSize(scenes, origin),
1502
+ origin,
1503
+ visible: floor.visible ?? true,
1504
+ layer: floor.layer ?? defaultFloorLayer(document),
1505
+ };
1506
+ if (floor.asset !== undefined) {
1507
+ compiled.asset = floor.asset;
1508
+ }
1509
+ return normalizeValue(compiled);
1510
+ }
1511
+ function deriveFloorSize(scenes, origin) {
1512
+ let maxX = origin[0] + 1;
1513
+ let maxY = origin[1] + 1;
1514
+ for (const scene of scenes) {
1515
+ for (const element of scene.elements) {
1516
+ if (element.presence === "removed")
1517
+ continue;
1518
+ maxX = Math.max(maxX, element.pos[0] + element.size);
1519
+ maxY = Math.max(maxY, element.pos[1] + element.size);
1520
+ }
1521
+ for (const connector of scene.connectors) {
1522
+ if (connector.presence === "removed")
1523
+ continue;
1524
+ for (const point of connector.route) {
1525
+ maxX = Math.max(maxX, point[0]);
1526
+ maxY = Math.max(maxY, point[1]);
1527
+ }
1528
+ }
1529
+ }
1530
+ return [Math.max(1, Math.ceil(maxX - origin[0])), Math.max(1, Math.ceil(maxY - origin[1]))];
1531
+ }
1532
+ function compileLayout() {
1533
+ return normalizeValue({
1534
+ fit: "contain",
1535
+ align: [0.5, 0.5],
1536
+ padding: { x: 64, y: 64 },
1537
+ bounds: "union",
1538
+ });
1539
+ }
1540
+ function compileLayers(layers) {
1541
+ return layers
1542
+ .map((layer, index) => ({
1543
+ name: layer.name,
1544
+ order: layer.order ?? index,
1545
+ }))
1546
+ .sort((a, b) => a.order - b.order || a.name.localeCompare(b.name));
1547
+ }
1548
+ function resolveAssetUrl(document, assetId) {
1549
+ const base = document.header.assetBaseUrl;
1550
+ const entry = document.header.assets.find((asset) => asset.id === assetId);
1551
+ if (!base || !entry)
1552
+ return undefined;
1553
+ const path = entry.path ?? entry.id;
1554
+ const file = path.endsWith(".svg") ? path : `${path}.svg`;
1555
+ return `${base.replace(/\/+$/, "")}/${file.replace(/^\/+/, "")}`;
1556
+ }
1557
+ function uniqueReferencedAssetNames(document, scenes) {
1558
+ const names = new Set();
1559
+ if (document.header.floor?.asset && document.header.floor.visible !== false) {
1560
+ names.add(document.header.floor.asset);
1561
+ }
1562
+ for (const scene of scenes) {
1563
+ for (const element of scene.elements) {
1564
+ if (!isBuiltInGeneratedAsset(element.asset)) {
1565
+ names.add(element.asset);
1566
+ }
1567
+ }
1568
+ }
1569
+ return Array.from(names).sort();
1570
+ }
1571
+ function isBuiltInGeneratedAsset(assetId) {
1572
+ return assetId === BUILT_IN_TEXT_ASSET_ID || BUILT_IN_PRIMITIVE_ASSET_IDS.has(assetId);
1573
+ }
1574
+ function defaultFloorLayer(document) {
1575
+ const ground = document.header.layers.find((layer) => layer.name === "ground");
1576
+ return ground?.name ?? document.header.layers[0]?.name ?? "";
1577
+ }
1578
+ function digestBundle(bundle) {
1579
+ return createHash("sha256").update(canonicalStringify(bundle)).digest("hex");
1580
+ }
1581
+ function canonicalStringify(value, space) {
1582
+ const normalized = normalizeValue(value);
1583
+ return JSON.stringify(normalized, null, space);
1584
+ }
1585
+ function normalizeValue(value) {
1586
+ if (Array.isArray(value)) {
1587
+ return value.map((item) => (item === undefined ? null : normalizeValue(item)));
1588
+ }
1589
+ if (!isPlainObject(value))
1590
+ return value;
1591
+ const normalized = {};
1592
+ for (const key of Object.keys(value).sort()) {
1593
+ const child = value[key];
1594
+ if (child !== undefined) {
1595
+ normalized[key] = normalizeValue(child);
1596
+ }
1597
+ }
1598
+ return normalized;
1599
+ }
1600
+ function isPlainObject(value) {
1601
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1602
+ }
1603
+
1604
+ const IDENTIFIER_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1605
+ const ASSET_PATH_PATTERN = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*(?:\.svg)?$/;
1606
+ function fail(code, message) {
1607
+ throw new ParseError(code, message, { line: 0, column: 0 });
1608
+ }
1609
+ function isObject(value) {
1610
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1611
+ }
1612
+ function assertKnownFields(obj, allowed, context) {
1613
+ for (const field of Object.keys(obj)) {
1614
+ if (!allowed.has(field)) {
1615
+ fail("UNKNOWN_FIELD", `Unknown field "${field}" in ${context}`);
1616
+ }
1617
+ }
1618
+ }
1619
+ function requireObject(value, context) {
1620
+ if (!isObject(value)) {
1621
+ fail("DSL_SCHEMA_TYPE_ERROR", `${context} must be a mapping object`);
1622
+ }
1623
+ return value;
1624
+ }
1625
+ function requireArray(value, context) {
1626
+ if (!Array.isArray(value)) {
1627
+ fail("DSL_SCHEMA_TYPE_ERROR", `${context} must be an array`);
1628
+ }
1629
+ return value;
1630
+ }
1631
+ function requireString(value, context) {
1632
+ if (typeof value !== "string") {
1633
+ fail("DSL_SCHEMA_TYPE_ERROR", `${context} must be a string`);
1634
+ }
1635
+ return value;
1636
+ }
1637
+ function requireNumber(value, context) {
1638
+ if (typeof value !== "number" || Number.isNaN(value)) {
1639
+ fail("DSL_SCHEMA_TYPE_ERROR", `${context} must be a number`);
1640
+ }
1641
+ return value;
1642
+ }
1643
+ function requireBoolean(value, context) {
1644
+ if (typeof value !== "boolean") {
1645
+ fail("DSL_SCHEMA_TYPE_ERROR", `${context} must be a boolean`);
1646
+ }
1647
+ return value;
1648
+ }
1649
+ function requireIdentifier(value, context) {
1650
+ const identifier = requireString(value, context);
1651
+ if (!IDENTIFIER_PATTERN.test(identifier)) {
1652
+ fail("INVALID_IDENTIFIER", `${context} must be kebab-case`);
1653
+ }
1654
+ return identifier;
1655
+ }
1656
+ function parseTuple2(value, context) {
1657
+ if (!Array.isArray(value) || value.length !== 2) {
1658
+ fail("DSL_SCHEMA_TYPE_ERROR", `${context} must be a two-number tuple`);
1659
+ }
1660
+ return [requireNumber(value[0], `${context}[0]`), requireNumber(value[1], `${context}[1]`)];
1661
+ }
1662
+ function parseAssets(raw) {
1663
+ return requireArray(raw, "header.assets").map((item, index) => {
1664
+ const asset = requireObject(item, `header.assets[${index}]`);
1665
+ assertKnownFields(asset, new Set(["id", "path", "anchor"]), `header.assets[${index}]`);
1666
+ const parsed = {
1667
+ id: requireIdentifier(asset.id, `header.assets[${index}].id`),
1668
+ };
1669
+ if (asset.path !== undefined) {
1670
+ const path = requireString(asset.path, `header.assets[${index}].path`);
1671
+ if (!ASSET_PATH_PATTERN.test(path)) {
1672
+ fail("INVALID_ASSET_PATH", `header.assets[${index}].path must be a relative SVG asset path`);
1673
+ }
1674
+ parsed.path = path;
1675
+ }
1676
+ if (asset.anchor !== undefined) {
1677
+ parsed.anchor = parseTuple2(asset.anchor, `header.assets[${index}].anchor`);
1678
+ }
1679
+ return parsed;
1680
+ });
1681
+ }
1682
+ function parseGrid(raw) {
1683
+ if (raw === undefined)
1684
+ return undefined;
1685
+ const grid = requireObject(raw, "header.grid");
1686
+ assertKnownFields(grid, new Set(["cellSize"]), "header.grid");
1687
+ return {
1688
+ cellSize: grid.cellSize === undefined ? undefined : requireNumber(grid.cellSize, "header.grid.cellSize"),
1689
+ };
1690
+ }
1691
+ function parseFloor(raw) {
1692
+ if (raw === undefined)
1693
+ return undefined;
1694
+ const floor = requireObject(raw, "header.floor");
1695
+ assertKnownFields(floor, new Set(["size", "origin", "layer", "visible", "asset"]), "header.floor");
1696
+ const parsed = {};
1697
+ if (floor.size !== undefined) {
1698
+ parsed.size = parseTuple2(floor.size, "header.floor.size");
1699
+ }
1700
+ if (floor.origin !== undefined) {
1701
+ parsed.origin = parseTuple2(floor.origin, "header.floor.origin");
1702
+ }
1703
+ if (floor.layer !== undefined) {
1704
+ parsed.layer = requireIdentifier(floor.layer, "header.floor.layer");
1705
+ }
1706
+ if (floor.visible !== undefined) {
1707
+ parsed.visible = requireBoolean(floor.visible, "header.floor.visible");
1708
+ }
1709
+ if (floor.asset !== undefined) {
1710
+ parsed.asset = requireIdentifier(floor.asset, "header.floor.asset");
1711
+ }
1712
+ return parsed;
1713
+ }
1714
+ function parseLayers(raw) {
1715
+ return requireArray(raw, "header.layers").map((item, index) => {
1716
+ const layer = requireObject(item, `header.layers[${index}]`);
1717
+ assertKnownFields(layer, new Set(["name", "order"]), `header.layers[${index}]`);
1718
+ const parsed = {
1719
+ name: requireIdentifier(layer.name, `header.layers[${index}].name`),
1720
+ };
1721
+ if (layer.order !== undefined) {
1722
+ parsed.order = requireNumber(layer.order, `header.layers[${index}].order`);
1723
+ }
1724
+ return parsed;
1725
+ });
1726
+ }
1727
+ function parseHeader(raw) {
1728
+ const header = requireObject(raw, "header");
1729
+ assertKnownFields(header, new Set(["version", "name", "className", "assetBaseUrl", "assets", "grid", "floor", "theme", "layers"]), "header");
1730
+ const parsed = {
1731
+ assets: parseAssets(header.assets),
1732
+ layers: parseLayers(header.layers),
1733
+ };
1734
+ const floor = parseFloor(header.floor);
1735
+ if (floor !== undefined) {
1736
+ parsed.floor = floor;
1737
+ }
1738
+ if (header.version !== undefined) {
1739
+ parsed.version = requireString(header.version, "header.version");
1740
+ }
1741
+ if (header.name !== undefined) {
1742
+ parsed.name = requireString(header.name, "header.name");
1743
+ }
1744
+ if (header.className !== undefined) {
1745
+ parsed.className = requireString(header.className, "header.className");
1746
+ }
1747
+ if (header.assetBaseUrl !== undefined) {
1748
+ parsed.assetBaseUrl = requireString(header.assetBaseUrl, "header.assetBaseUrl");
1749
+ }
1750
+ if (header.grid !== undefined) {
1751
+ parsed.grid = parseGrid(header.grid);
1752
+ }
1753
+ if (header.theme !== undefined) {
1754
+ parsed.theme = requireString(header.theme, "header.theme");
1755
+ }
1756
+ return parsed;
1757
+ }
1758
+ function parseAmbient(raw, context) {
1759
+ return requireArray(raw, context).map((item, index) => {
1760
+ const ambient = requireObject(item, `${context}[${index}]`);
1761
+ assertKnownFields(ambient, new Set(["name", "infinite", "iterations"]), `${context}[${index}]`);
1762
+ const parsed = {
1763
+ name: requireIdentifier(ambient.name, `${context}[${index}].name`),
1764
+ };
1765
+ if (ambient.infinite !== undefined) {
1766
+ parsed.infinite = requireBoolean(ambient.infinite, `${context}[${index}].infinite`);
1767
+ }
1768
+ if (ambient.iterations !== undefined) {
1769
+ parsed.iterations = requireNumber(ambient.iterations, `${context}[${index}].iterations`);
1770
+ }
1771
+ return parsed;
1772
+ });
1773
+ }
1774
+ function parsePointArray(raw, context) {
1775
+ return requireArray(raw, context).map((point, index) => parseTuple2(point, `${context}[${index}]`));
1776
+ }
1777
+ function parseTextContent(raw, context) {
1778
+ const text = requireObject(raw, context);
1779
+ assertKnownFields(text, new Set(["value", "align", "fontSize", "fontWeight", "lineHeight", "fill"]), context);
1780
+ const parsed = {
1781
+ value: requireString(text.value, `${context}.value`),
1782
+ };
1783
+ if (text.align !== undefined) {
1784
+ parsed.align = requireString(text.align, `${context}.align`);
1785
+ }
1786
+ if (text.fontSize !== undefined) {
1787
+ parsed.fontSize = requireNumber(text.fontSize, `${context}.fontSize`);
1788
+ }
1789
+ if (text.fontWeight !== undefined) {
1790
+ if (typeof text.fontWeight === "number") {
1791
+ parsed.fontWeight = requireNumber(text.fontWeight, `${context}.fontWeight`);
1792
+ }
1793
+ else {
1794
+ parsed.fontWeight = requireString(text.fontWeight, `${context}.fontWeight`);
1795
+ }
1796
+ }
1797
+ if (text.lineHeight !== undefined) {
1798
+ parsed.lineHeight = requireNumber(text.lineHeight, `${context}.lineHeight`);
1799
+ }
1800
+ if (text.fill !== undefined) {
1801
+ parsed.fill = requireString(text.fill, `${context}.fill`);
1802
+ }
1803
+ return parsed;
1804
+ }
1805
+ function parsePrimitiveStyle(raw, context, allowed) {
1806
+ assertKnownFields(raw, new Set(allowed), context);
1807
+ const parsed = {};
1808
+ if (raw.fill !== undefined)
1809
+ parsed.fill = requireString(raw.fill, `${context}.fill`);
1810
+ if (raw.stroke !== undefined)
1811
+ parsed.stroke = requireString(raw.stroke, `${context}.stroke`);
1812
+ if (raw.strokeWidth !== undefined) {
1813
+ parsed.strokeWidth = requireNumber(raw.strokeWidth, `${context}.strokeWidth`);
1814
+ }
1815
+ if (raw.opacity !== undefined) {
1816
+ parsed.opacity = requireNumber(raw.opacity, `${context}.opacity`);
1817
+ }
1818
+ if (raw.dash !== undefined)
1819
+ parsed.dash = parseTuple2(raw.dash, `${context}.dash`);
1820
+ return parsed;
1821
+ }
1822
+ function parsePrimitiveContent(raw, context) {
1823
+ const primitive = requireObject(raw, context);
1824
+ assertKnownFields(primitive, new Set(["rectangle", "circle", "polygon", "line"]), context);
1825
+ const parsed = {};
1826
+ if (primitive.rectangle !== undefined) {
1827
+ const rectangle = requireObject(primitive.rectangle, `${context}.rectangle`);
1828
+ parsed.rectangle = parsePrimitiveStyle(rectangle, `${context}.rectangle`, [
1829
+ "fill",
1830
+ "stroke",
1831
+ "strokeWidth",
1832
+ "opacity",
1833
+ "dash",
1834
+ "rx",
1835
+ ]);
1836
+ if (rectangle.rx !== undefined) {
1837
+ parsed.rectangle.rx = requireNumber(rectangle.rx, `${context}.rectangle.rx`);
1838
+ }
1839
+ }
1840
+ if (primitive.circle !== undefined) {
1841
+ parsed.circle = parsePrimitiveStyle(requireObject(primitive.circle, `${context}.circle`), `${context}.circle`, [
1842
+ "fill",
1843
+ "stroke",
1844
+ "strokeWidth",
1845
+ "opacity",
1846
+ "dash",
1847
+ ]);
1848
+ }
1849
+ if (primitive.polygon !== undefined) {
1850
+ const polygon = requireObject(primitive.polygon, `${context}.polygon`);
1851
+ parsed.polygon = {
1852
+ ...parsePrimitiveStyle(polygon, `${context}.polygon`, [
1853
+ "points",
1854
+ "fill",
1855
+ "stroke",
1856
+ "strokeWidth",
1857
+ "opacity",
1858
+ "dash",
1859
+ ]),
1860
+ points: parsePointArray(polygon.points, `${context}.polygon.points`),
1861
+ };
1862
+ }
1863
+ if (primitive.line !== undefined) {
1864
+ const line = requireObject(primitive.line, `${context}.line`);
1865
+ const parsedLine = {
1866
+ ...parsePrimitiveStyle(line, `${context}.line`, [
1867
+ "points",
1868
+ "stroke",
1869
+ "strokeWidth",
1870
+ "opacity",
1871
+ "dash",
1872
+ "lineCap",
1873
+ "lineJoin",
1874
+ ]),
1875
+ points: parsePointArray(line.points, `${context}.line.points`),
1876
+ };
1877
+ if (line.lineCap !== undefined) {
1878
+ parsedLine.lineCap = requireString(line.lineCap, `${context}.line.lineCap`);
1879
+ }
1880
+ if (line.lineJoin !== undefined) {
1881
+ parsedLine.lineJoin = requireString(line.lineJoin, `${context}.line.lineJoin`);
1882
+ }
1883
+ parsed.line = parsedLine;
1884
+ }
1885
+ return parsed;
1886
+ }
1887
+ function parsePlacement(raw, context) {
1888
+ const element = requireObject(raw, context);
1889
+ assertKnownFields(element, new Set(["id", "asset", "at", "size", "layer", "enter", "exit", "ambient", "text", "primitive"]), context);
1890
+ const parsed = {
1891
+ id: requireIdentifier(element.id, `${context}.id`),
1892
+ asset: requireIdentifier(element.asset, `${context}.asset`),
1893
+ at: parseTuple2(element.at, `${context}.at`),
1894
+ };
1895
+ if (element.size !== undefined) {
1896
+ parsed.size = requireNumber(element.size, `${context}.size`);
1897
+ }
1898
+ if (element.layer !== undefined) {
1899
+ parsed.layer = requireIdentifier(element.layer, `${context}.layer`);
1900
+ }
1901
+ if (element.enter !== undefined) {
1902
+ parsed.enter = requireString(element.enter, `${context}.enter`);
1903
+ }
1904
+ if (element.exit !== undefined) {
1905
+ parsed.exit = requireString(element.exit, `${context}.exit`);
1906
+ }
1907
+ if (element.ambient !== undefined) {
1908
+ parsed.ambient = parseAmbient(element.ambient, `${context}.ambient`);
1909
+ }
1910
+ if (element.text !== undefined) {
1911
+ parsed.text = parseTextContent(element.text, `${context}.text`);
1912
+ }
1913
+ if (element.primitive !== undefined) {
1914
+ parsed.primitive = parsePrimitiveContent(element.primitive, `${context}.primitive`);
1915
+ }
1916
+ return parsed;
1917
+ }
1918
+ function parsePatch(raw, context) {
1919
+ const patch = requireObject(raw, context);
1920
+ assertKnownFields(patch, new Set(["id", "at", "size", "layer", "enter", "exit", "ambient", "text", "primitive"]), context);
1921
+ const parsed = {
1922
+ id: requireIdentifier(patch.id, `${context}.id`),
1923
+ };
1924
+ if (patch.at !== undefined) {
1925
+ parsed.at = parseTuple2(patch.at, `${context}.at`);
1926
+ }
1927
+ if (patch.size !== undefined) {
1928
+ parsed.size = requireNumber(patch.size, `${context}.size`);
1929
+ }
1930
+ if (patch.layer !== undefined) {
1931
+ parsed.layer = requireIdentifier(patch.layer, `${context}.layer`);
1932
+ }
1933
+ if (patch.enter !== undefined) {
1934
+ parsed.enter = requireString(patch.enter, `${context}.enter`);
1935
+ }
1936
+ if (patch.exit !== undefined) {
1937
+ parsed.exit = requireString(patch.exit, `${context}.exit`);
1938
+ }
1939
+ if (patch.ambient !== undefined) {
1940
+ parsed.ambient = parseAmbient(patch.ambient, `${context}.ambient`);
1941
+ }
1942
+ if (patch.text !== undefined) {
1943
+ parsed.text = parseTextContent(patch.text, `${context}.text`);
1944
+ }
1945
+ if (patch.primitive !== undefined) {
1946
+ parsed.primitive = parsePrimitiveContent(patch.primitive, `${context}.primitive`);
1947
+ }
1948
+ return parsed;
1949
+ }
1950
+ function parseRemoval(raw, context) {
1951
+ const removal = requireObject(raw, context);
1952
+ assertKnownFields(removal, new Set(["id", "exit"]), context);
1953
+ const parsed = {
1954
+ id: requireIdentifier(removal.id, `${context}.id`),
1955
+ };
1956
+ if (removal.exit !== undefined) {
1957
+ parsed.exit = requireString(removal.exit, `${context}.exit`);
1958
+ }
1959
+ return parsed;
1960
+ }
1961
+ function parseRoute(raw, context) {
1962
+ return requireArray(raw, context).map((point, index) => parseTuple2(point, `${context}[${index}]`));
1963
+ }
1964
+ function parseEndpointRef(raw, context) {
1965
+ const endpoint = requireObject(raw, context);
1966
+ assertKnownFields(endpoint, new Set(["element", "at", "side", "offset"]), context);
1967
+ const parsed = {};
1968
+ if (endpoint.element !== undefined) {
1969
+ parsed.element = requireIdentifier(endpoint.element, `${context}.element`);
1970
+ }
1971
+ if (endpoint.at !== undefined) {
1972
+ parsed.at = parseTuple2(endpoint.at, `${context}.at`);
1973
+ }
1974
+ if (endpoint.side !== undefined) {
1975
+ parsed.side = requireString(endpoint.side, `${context}.side`);
1976
+ }
1977
+ if (endpoint.offset !== undefined) {
1978
+ parsed.offset = requireNumber(endpoint.offset, `${context}.offset`);
1979
+ }
1980
+ return parsed;
1981
+ }
1982
+ function parseRouting(raw, context) {
1983
+ const routing = requireObject(raw, context);
1984
+ assertKnownFields(routing, new Set(["mode", "avoid", "clearance", "gridStep", "maxBends", "prefer"]), context);
1985
+ const parsed = {};
1986
+ if (routing.mode !== undefined) {
1987
+ parsed.mode = requireString(routing.mode, `${context}.mode`);
1988
+ }
1989
+ if (routing.avoid !== undefined) {
1990
+ if (Array.isArray(routing.avoid)) {
1991
+ parsed.avoid = requireArray(routing.avoid, `${context}.avoid`).map((item, index) => requireIdentifier(item, `${context}.avoid[${index}]`));
1992
+ }
1993
+ else {
1994
+ parsed.avoid = requireString(routing.avoid, `${context}.avoid`);
1995
+ }
1996
+ }
1997
+ if (routing.clearance !== undefined) {
1998
+ parsed.clearance = requireNumber(routing.clearance, `${context}.clearance`);
1999
+ }
2000
+ if (routing.gridStep !== undefined) {
2001
+ parsed.gridStep = requireNumber(routing.gridStep, `${context}.gridStep`);
2002
+ }
2003
+ if (routing.maxBends !== undefined) {
2004
+ parsed.maxBends = requireNumber(routing.maxBends, `${context}.maxBends`);
2005
+ }
2006
+ if (routing.prefer !== undefined) {
2007
+ parsed.prefer = requireString(routing.prefer, `${context}.prefer`);
2008
+ }
2009
+ return parsed;
2010
+ }
2011
+ function parseConnectorStyle(raw, context) {
2012
+ const style = requireObject(raw, context);
2013
+ assertKnownFields(style, new Set(["variant", "pattern", "stroke", "strokeWidth", "opacity", "dash", "outline", "outlineWidth", "lane"]), context);
2014
+ const parsed = {};
2015
+ if (style.variant !== undefined) {
2016
+ parsed.variant = requireString(style.variant, `${context}.variant`);
2017
+ }
2018
+ if (style.pattern !== undefined) {
2019
+ parsed.pattern = requireString(style.pattern, `${context}.pattern`);
2020
+ }
2021
+ if (style.stroke !== undefined) {
2022
+ parsed.stroke = requireString(style.stroke, `${context}.stroke`);
2023
+ }
2024
+ if (style.strokeWidth !== undefined) {
2025
+ parsed.strokeWidth = requireNumber(style.strokeWidth, `${context}.strokeWidth`);
2026
+ }
2027
+ if (style.opacity !== undefined) {
2028
+ parsed.opacity = requireNumber(style.opacity, `${context}.opacity`);
2029
+ }
2030
+ if (style.dash !== undefined) {
2031
+ parsed.dash = parseTuple2(style.dash, `${context}.dash`);
2032
+ }
2033
+ if (style.outline !== undefined) {
2034
+ parsed.outline = requireString(style.outline, `${context}.outline`);
2035
+ }
2036
+ if (style.outlineWidth !== undefined) {
2037
+ parsed.outlineWidth = requireNumber(style.outlineWidth, `${context}.outlineWidth`);
2038
+ }
2039
+ if (style.lane !== undefined) {
2040
+ parsed.lane = requireString(style.lane, `${context}.lane`);
2041
+ }
2042
+ return parsed;
2043
+ }
2044
+ function parseConnectionCommon(raw, context) {
2045
+ const connection = requireObject(raw, context);
2046
+ assertKnownFields(connection, new Set([
2047
+ "id",
2048
+ "route",
2049
+ "from",
2050
+ "to",
2051
+ "routing",
2052
+ "layer",
2053
+ "style",
2054
+ "start",
2055
+ "end",
2056
+ "direction",
2057
+ "enter",
2058
+ "exit",
2059
+ "ambient",
2060
+ ]), context);
2061
+ const parsed = {
2062
+ id: requireIdentifier(connection.id, `${context}.id`),
2063
+ };
2064
+ if (connection.route !== undefined) {
2065
+ parsed.route = parseRoute(connection.route, `${context}.route`);
2066
+ }
2067
+ if (connection.from !== undefined) {
2068
+ parsed.from = parseEndpointRef(connection.from, `${context}.from`);
2069
+ }
2070
+ if (connection.to !== undefined) {
2071
+ parsed.to = parseEndpointRef(connection.to, `${context}.to`);
2072
+ }
2073
+ if (connection.routing !== undefined) {
2074
+ parsed.routing = parseRouting(connection.routing, `${context}.routing`);
2075
+ }
2076
+ if (connection.layer !== undefined) {
2077
+ parsed.layer = requireIdentifier(connection.layer, `${context}.layer`);
2078
+ }
2079
+ if (connection.style !== undefined) {
2080
+ parsed.style = parseConnectorStyle(connection.style, `${context}.style`);
2081
+ }
2082
+ if (connection.start !== undefined) {
2083
+ parsed.start = requireString(connection.start, `${context}.start`);
2084
+ }
2085
+ if (connection.end !== undefined) {
2086
+ parsed.end = requireString(connection.end, `${context}.end`);
2087
+ }
2088
+ if (connection.direction !== undefined) {
2089
+ parsed.direction = requireString(connection.direction, `${context}.direction`);
2090
+ }
2091
+ if (connection.enter !== undefined) {
2092
+ parsed.enter = requireString(connection.enter, `${context}.enter`);
2093
+ }
2094
+ if (connection.exit !== undefined) {
2095
+ parsed.exit = requireString(connection.exit, `${context}.exit`);
2096
+ }
2097
+ if (connection.ambient !== undefined) {
2098
+ parsed.ambient = parseAmbient(connection.ambient, `${context}.ambient`);
2099
+ }
2100
+ return parsed;
2101
+ }
2102
+ function parseConnectionPlacement(raw, context) {
2103
+ return parseConnectionCommon(raw, context);
2104
+ }
2105
+ function parseConnectionPatch(raw, context) {
2106
+ return parseConnectionCommon(raw, context);
2107
+ }
2108
+ function parseConnectionRemoval(raw, context) {
2109
+ const removal = requireObject(raw, context);
2110
+ assertKnownFields(removal, new Set(["id", "exit"]), context);
2111
+ const parsed = {
2112
+ id: requireIdentifier(removal.id, `${context}.id`),
2113
+ };
2114
+ if (removal.exit !== undefined) {
2115
+ parsed.exit = requireString(removal.exit, `${context}.exit`);
2116
+ }
2117
+ return parsed;
2118
+ }
2119
+ function parseScenes(raw) {
2120
+ return requireArray(raw, "scenes").map((item, index) => {
2121
+ const scene = requireObject(item, `scenes[${index}]`);
2122
+ assertKnownFields(scene, new Set(["id", "elements", "connections", "add", "update", "remove"]), `scenes[${index}]`);
2123
+ const parsed = {
2124
+ id: requireIdentifier(scene.id, `scenes[${index}].id`),
2125
+ };
2126
+ if (scene.elements !== undefined) {
2127
+ parsed.elements = requireArray(scene.elements, `scenes[${index}].elements`).map((element, elementIndex) => parsePlacement(element, `scenes[${index}].elements[${elementIndex}]`));
2128
+ }
2129
+ if (scene.connections !== undefined) {
2130
+ parsed.connections = requireArray(scene.connections, `scenes[${index}].connections`).map((connection, connectionIndex) => parseConnectionPlacement(connection, `scenes[${index}].connections[${connectionIndex}]`));
2131
+ }
2132
+ if (scene.add !== undefined) {
2133
+ const add = requireObject(scene.add, `scenes[${index}].add`);
2134
+ assertKnownFields(add, new Set(["elements", "connections"]), `scenes[${index}].add`);
2135
+ parsed.add = {};
2136
+ if (add.elements !== undefined) {
2137
+ parsed.add.elements = requireArray(add.elements, `scenes[${index}].add.elements`).map((element, elementIndex) => parsePlacement(element, `scenes[${index}].add.elements[${elementIndex}]`));
2138
+ }
2139
+ if (add.connections !== undefined) {
2140
+ parsed.add.connections = requireArray(add.connections, `scenes[${index}].add.connections`).map((connection, connectionIndex) => parseConnectionPlacement(connection, `scenes[${index}].add.connections[${connectionIndex}]`));
2141
+ }
2142
+ }
2143
+ if (scene.update !== undefined) {
2144
+ const update = requireObject(scene.update, `scenes[${index}].update`);
2145
+ assertKnownFields(update, new Set(["elements", "connections"]), `scenes[${index}].update`);
2146
+ parsed.update = {};
2147
+ if (update.elements !== undefined) {
2148
+ parsed.update.elements = requireArray(update.elements, `scenes[${index}].update.elements`).map((patch, patchIndex) => parsePatch(patch, `scenes[${index}].update.elements[${patchIndex}]`));
2149
+ }
2150
+ if (update.connections !== undefined) {
2151
+ parsed.update.connections = requireArray(update.connections, `scenes[${index}].update.connections`).map((patch, patchIndex) => parseConnectionPatch(patch, `scenes[${index}].update.connections[${patchIndex}]`));
2152
+ }
2153
+ }
2154
+ if (scene.remove !== undefined) {
2155
+ const remove = requireObject(scene.remove, `scenes[${index}].remove`);
2156
+ assertKnownFields(remove, new Set(["elements", "connections"]), `scenes[${index}].remove`);
2157
+ parsed.remove = {};
2158
+ if (remove.elements !== undefined) {
2159
+ parsed.remove.elements = requireArray(remove.elements, `scenes[${index}].remove.elements`).map((removal, removalIndex) => parseRemoval(removal, `scenes[${index}].remove.elements[${removalIndex}]`));
2160
+ }
2161
+ if (remove.connections !== undefined) {
2162
+ parsed.remove.connections = requireArray(remove.connections, `scenes[${index}].remove.connections`).map((removal, removalIndex) => parseConnectionRemoval(removal, `scenes[${index}].remove.connections[${removalIndex}]`));
2163
+ }
2164
+ }
2165
+ return parsed;
2166
+ });
2167
+ }
2168
+ /**
2169
+ * Parse authored YAML into the v1 header + scene-delta document contract.
2170
+ */
2171
+ function parseScene(dsl) {
2172
+ let raw;
2173
+ try {
2174
+ raw = parse(dsl);
2175
+ }
2176
+ catch (err) {
2177
+ const message = err instanceof Error ? err.message : String(err);
2178
+ throw new ParseError("DSL_PARSE_SYNTAX_ERROR", `YAML syntax error: ${message}`, { line: 0, column: 0 });
2179
+ }
2180
+ if (!isObject(raw)) {
2181
+ fail("DSL_PARSE_SYNTAX_ERROR", "DSL must be a YAML mapping at the top level");
2182
+ }
2183
+ assertKnownFields(raw, new Set(["header", "scenes"]), "top level");
2184
+ return {
2185
+ header: parseHeader(raw.header),
2186
+ scenes: parseScenes(raw.scenes),
2187
+ };
2188
+ }
2189
+
2190
+ export { compileScene, deriveProgresses, fromJs, fromJson, parseScene, resolveSceneSnapshots, toJs, toJson, validateScene };
2191
+ //# sourceMappingURL=index.js.map