worldorbit 2.5.16 → 2.6.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 (35) hide show
  1. package/README.md +81 -15
  2. package/dist/browser/core/dist/index.js +1228 -110
  3. package/dist/browser/editor/dist/index.js +1896 -180
  4. package/dist/browser/markdown/dist/index.js +1071 -99
  5. package/dist/browser/viewer/dist/index.js +1127 -113
  6. package/dist/unpkg/core/dist/index.js +1228 -110
  7. package/dist/unpkg/editor/dist/index.js +1896 -180
  8. package/dist/unpkg/markdown/dist/index.js +1071 -99
  9. package/dist/unpkg/viewer/dist/index.js +1127 -113
  10. package/dist/unpkg/worldorbit-core.min.js +12 -12
  11. package/dist/unpkg/worldorbit-editor.min.js +295 -203
  12. package/dist/unpkg/worldorbit-markdown.min.js +66 -58
  13. package/dist/unpkg/worldorbit-viewer.min.js +84 -76
  14. package/dist/unpkg/worldorbit.js +1304 -124
  15. package/dist/unpkg/worldorbit.min.js +88 -80
  16. package/package.json +1 -1
  17. package/packages/core/dist/atlas-edit.js +75 -1
  18. package/packages/core/dist/atlas-validate.js +211 -8
  19. package/packages/core/dist/draft-parse.js +401 -22
  20. package/packages/core/dist/draft.d.ts +5 -2
  21. package/packages/core/dist/draft.js +103 -8
  22. package/packages/core/dist/format.js +99 -6
  23. package/packages/core/dist/load.js +9 -2
  24. package/packages/core/dist/normalize.js +1 -0
  25. package/packages/core/dist/scene.js +400 -64
  26. package/packages/core/dist/types.d.ts +60 -4
  27. package/packages/editor/dist/editor.js +702 -65
  28. package/packages/editor/dist/types.d.ts +3 -1
  29. package/packages/viewer/dist/atlas-state.js +11 -2
  30. package/packages/viewer/dist/atlas-viewer.js +19 -7
  31. package/packages/viewer/dist/render.js +31 -2
  32. package/packages/viewer/dist/theme.js +1 -0
  33. package/packages/viewer/dist/tooltip.js +9 -0
  34. package/packages/viewer/dist/types.d.ts +12 -2
  35. package/packages/viewer/dist/viewer.js +28 -1
@@ -5,6 +5,7 @@
5
5
  background: true,
6
6
  guides: true,
7
7
  relations: true,
8
+ events: true,
8
9
  orbits: true,
9
10
  objects: true,
10
11
  labels: true,
@@ -154,14 +155,17 @@
154
155
  }
155
156
  function createAtlasStateSnapshot(viewerState, renderOptions, filter, viewpointId) {
156
157
  return {
157
- version: "2.0",
158
+ version: "2.5",
158
159
  viewpointId,
160
+ activeEventId: renderOptions.activeEventId ?? null,
159
161
  viewerState: { ...viewerState },
160
162
  renderOptions: {
161
163
  preset: renderOptions.preset,
162
164
  projection: renderOptions.projection,
165
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
163
166
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
164
- scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0
167
+ scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
168
+ activeEventId: renderOptions.activeEventId ?? null
165
169
  },
166
170
  filter: normalizeViewerFilter(filter)
167
171
  };
@@ -172,8 +176,9 @@
172
176
  function deserializeViewerAtlasState(serialized) {
173
177
  const raw = JSON.parse(decodeURIComponent(serialized));
174
178
  return {
175
- version: "2.0",
179
+ version: raw.version === "2.0" ? "2.0" : "2.5",
176
180
  viewpointId: raw.viewpointId ?? null,
181
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
177
182
  viewerState: {
178
183
  scale: raw.viewerState?.scale ?? 1,
179
184
  rotationDeg: raw.viewerState?.rotationDeg ?? 0,
@@ -184,8 +189,10 @@
184
189
  renderOptions: {
185
190
  preset: raw.renderOptions?.preset,
186
191
  projection: raw.renderOptions?.projection,
192
+ camera: raw.renderOptions?.camera ? { ...raw.renderOptions.camera } : null,
187
193
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : void 0,
188
- scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0
194
+ scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0,
195
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null
189
196
  },
190
197
  filter: normalizeViewerFilter(raw.filter ?? null)
191
198
  };
@@ -200,8 +207,10 @@
200
207
  viewerState: { ...atlasState.viewerState },
201
208
  renderOptions: {
202
209
  ...atlasState.renderOptions,
210
+ camera: atlasState.renderOptions.camera ? { ...atlasState.renderOptions.camera } : null,
203
211
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : void 0,
204
- scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0
212
+ scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0,
213
+ activeEventId: atlasState.renderOptions.activeEventId ?? null
205
214
  },
206
215
  filter: atlasState.filter ? { ...atlasState.filter } : null
207
216
  }
@@ -219,6 +228,7 @@
219
228
  background: viewpoint.layers.background,
220
229
  guides: viewpoint.layers.guides,
221
230
  relations: viewpoint.layers.relations,
231
+ events: viewpoint.layers.events,
222
232
  orbits: viewpoint.layers["orbits-front"] === void 0 && viewpoint.layers["orbits-back"] === void 0 ? void 0 : viewpoint.layers["orbits-front"] !== false || viewpoint.layers["orbits-back"] !== false,
223
233
  objects: viewpoint.layers.objects,
224
234
  labels: viewpoint.layers.labels,
@@ -866,6 +876,7 @@
866
876
  system,
867
877
  groups: [],
868
878
  relations: [],
879
+ events: [],
869
880
  objects
870
881
  };
871
882
  }
@@ -1245,12 +1256,16 @@
1245
1256
  const height = frame.height;
1246
1257
  const padding = frame.padding;
1247
1258
  const layoutPreset = resolveLayoutPreset(document2);
1248
- const projection = resolveProjection(document2, options.projection);
1259
+ const schemaProjection = resolveProjection(document2, options.projection);
1260
+ const camera = normalizeViewCamera(options.camera ?? null);
1261
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
1249
1262
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1250
1263
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1251
1264
  const systemId = document2.system?.id ?? null;
1252
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
1253
- const relationships = buildSceneRelationships(document2.objects, objectMap);
1265
+ const activeEventId = options.activeEventId ?? null;
1266
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
1267
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
1268
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
1254
1269
  const positions = /* @__PURE__ */ new Map();
1255
1270
  const orbitDrafts = [];
1256
1271
  const leaderDrafts = [];
@@ -1259,7 +1274,7 @@
1259
1274
  const atObjects = [];
1260
1275
  const surfaceChildren = /* @__PURE__ */ new Map();
1261
1276
  const orbitChildren = /* @__PURE__ */ new Map();
1262
- for (const object of document2.objects) {
1277
+ for (const object of effectiveObjects) {
1263
1278
  const placement = object.placement;
1264
1279
  if (!placement) {
1265
1280
  rootObjects.push(object);
@@ -1286,7 +1301,7 @@
1286
1301
  surfaceChildren,
1287
1302
  objectMap,
1288
1303
  spacingFactor,
1289
- projection,
1304
+ projection: renderProjection,
1290
1305
  scaleModel
1291
1306
  };
1292
1307
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -1298,7 +1313,7 @@
1298
1313
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1299
1314
  secondaryRoots.forEach((object, index) => {
1300
1315
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
1301
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
1316
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
1302
1317
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
1303
1318
  });
1304
1319
  }
@@ -1354,38 +1369,48 @@
1354
1369
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1355
1370
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1356
1371
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1357
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1372
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1358
1373
  const relations = createSceneRelations(document2, objects);
1359
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1360
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1374
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1375
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1376
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1361
1377
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1362
- const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1363
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1378
+ const viewpoints = createSceneViewpoints(document2, schemaProjection, frame.preset, relationships, objectMap);
1379
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1364
1380
  return {
1365
1381
  width,
1366
1382
  height,
1367
1383
  padding,
1368
1384
  renderPreset: frame.preset,
1369
- projection,
1385
+ projection: schemaProjection,
1386
+ renderProjection,
1387
+ camera,
1370
1388
  scaleModel,
1371
1389
  title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1372
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1390
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1373
1391
  systemId,
1374
- viewMode: projection,
1392
+ viewMode: schemaProjection,
1375
1393
  layoutPreset,
1376
1394
  metadata: {
1377
1395
  format: document2.format,
1378
1396
  version: document2.version,
1379
- view: projection,
1397
+ view: schemaProjection,
1398
+ renderProjection,
1380
1399
  scale: String(document2.system?.properties.scale ?? layoutPreset),
1381
1400
  units: String(document2.system?.properties.units ?? "mixed"),
1382
- preset: frame.preset ?? "custom"
1401
+ preset: frame.preset ?? "custom",
1402
+ ...camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {},
1403
+ ...camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {},
1404
+ ...camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {},
1405
+ ...camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}
1383
1406
  },
1384
1407
  contentBounds,
1385
1408
  layers,
1386
1409
  groups,
1387
1410
  semanticGroups,
1388
1411
  viewpoints,
1412
+ events,
1413
+ activeEventId,
1389
1414
  objects,
1390
1415
  orbitVisuals,
1391
1416
  relations,
@@ -1404,6 +1429,56 @@
1404
1429
  y: center.y + dx * sin + dy * cos
1405
1430
  };
1406
1431
  }
1432
+ function createEffectiveObjects(objects, events, activeEventId) {
1433
+ const cloned = objects.map((object) => structuredClone(object));
1434
+ if (!activeEventId) {
1435
+ return cloned;
1436
+ }
1437
+ const activeEvent = events.find((event) => event.id === activeEventId);
1438
+ if (!activeEvent) {
1439
+ return cloned;
1440
+ }
1441
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1442
+ const referencedIds = /* @__PURE__ */ new Set([
1443
+ ...activeEvent.targetObjectId ? [activeEvent.targetObjectId] : [],
1444
+ ...activeEvent.participantObjectIds,
1445
+ ...activeEvent.positions.map((pose) => pose.objectId)
1446
+ ]);
1447
+ for (const objectId of referencedIds) {
1448
+ const object = objectMap.get(objectId);
1449
+ if (!object) {
1450
+ continue;
1451
+ }
1452
+ if (activeEvent.epoch) {
1453
+ object.epoch = activeEvent.epoch;
1454
+ }
1455
+ if (activeEvent.referencePlane) {
1456
+ object.referencePlane = activeEvent.referencePlane;
1457
+ }
1458
+ }
1459
+ for (const pose of activeEvent.positions) {
1460
+ const object = objectMap.get(pose.objectId);
1461
+ if (!object) {
1462
+ continue;
1463
+ }
1464
+ if (pose.placement) {
1465
+ object.placement = structuredClone(pose.placement);
1466
+ }
1467
+ if (pose.inner) {
1468
+ object.properties.inner = { ...pose.inner };
1469
+ }
1470
+ if (pose.outer) {
1471
+ object.properties.outer = { ...pose.outer };
1472
+ }
1473
+ if (pose.epoch) {
1474
+ object.epoch = pose.epoch;
1475
+ }
1476
+ if (pose.referencePlane) {
1477
+ object.referencePlane = pose.referencePlane;
1478
+ }
1479
+ }
1480
+ return cloned;
1481
+ }
1407
1482
  function resolveLayoutPreset(document2) {
1408
1483
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1409
1484
  switch (rawScale) {
@@ -1440,10 +1515,59 @@
1440
1515
  }
1441
1516
  }
1442
1517
  function resolveProjection(document2, projection) {
1443
- if (projection === "topdown" || projection === "isometric") {
1518
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1444
1519
  return projection;
1445
1520
  }
1446
- return String(document2.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1521
+ const documentView = String(document2.system?.properties.view ?? "topdown").toLowerCase();
1522
+ return parseViewProjection(documentView) ?? "topdown";
1523
+ }
1524
+ function resolveRenderProjection(projection, camera) {
1525
+ switch (projection) {
1526
+ case "topdown":
1527
+ return "topdown";
1528
+ case "isometric":
1529
+ return "isometric";
1530
+ case "orthographic":
1531
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null) ? "isometric" : "topdown";
1532
+ case "perspective":
1533
+ return "isometric";
1534
+ }
1535
+ }
1536
+ function normalizeViewCamera(camera) {
1537
+ if (!camera) {
1538
+ return null;
1539
+ }
1540
+ const normalized = {
1541
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
1542
+ elevation: normalizeFiniteCameraValue(camera.elevation),
1543
+ roll: normalizeFiniteCameraValue(camera.roll),
1544
+ distance: normalizePositiveCameraDistance(camera.distance)
1545
+ };
1546
+ return normalized.azimuth !== null || normalized.elevation !== null || normalized.roll !== null || normalized.distance !== null ? normalized : null;
1547
+ }
1548
+ function normalizeFiniteCameraValue(value) {
1549
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1550
+ }
1551
+ function normalizePositiveCameraDistance(value) {
1552
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1553
+ }
1554
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
1555
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
1556
+ if (projection !== renderProjection) {
1557
+ parts.push(`2D ${renderProjection} fallback`);
1558
+ }
1559
+ if (camera) {
1560
+ const cameraParts = [
1561
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
1562
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
1563
+ camera.roll !== null ? `roll ${camera.roll}` : null,
1564
+ camera.distance !== null ? `dist ${camera.distance}` : null
1565
+ ].filter(Boolean);
1566
+ if (cameraParts.length > 0) {
1567
+ parts.push(`camera ${cameraParts.join(" / ")}`);
1568
+ }
1569
+ }
1570
+ return parts.join(" - ");
1447
1571
  }
1448
1572
  function resolveScaleModel(layoutPreset, overrides) {
1449
1573
  const defaults = defaultScaleModel(layoutPreset);
@@ -1559,24 +1683,14 @@
1559
1683
  hidden: draft.object.properties.hidden === true
1560
1684
  };
1561
1685
  }
1562
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1686
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1563
1687
  const labels = [];
1564
1688
  const occupied = [];
1565
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1689
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1690
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1566
1691
  for (const object of visibleObjects) {
1567
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1568
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1569
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1570
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1571
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1572
- let attempts = 0;
1573
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1574
- labelY += direction * 14 * labelMultiplier;
1575
- secondaryY += direction * 14 * labelMultiplier;
1576
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1577
- attempts += 1;
1578
- }
1579
- occupied.push(bounds);
1692
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1693
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1580
1694
  labels.push({
1581
1695
  renderId: `${object.renderId}-label`,
1582
1696
  objectId: object.objectId,
@@ -1585,17 +1699,128 @@
1585
1699
  semanticGroupIds: [...object.semanticGroupIds],
1586
1700
  label: object.label,
1587
1701
  secondaryLabel: object.secondaryLabel,
1588
- x: object.x,
1589
- y: labelY,
1590
- secondaryY,
1591
- textAnchor: "middle",
1592
- direction: direction < 0 ? "above" : "below",
1702
+ x: placement.x,
1703
+ y: placement.labelY,
1704
+ secondaryY: placement.secondaryY,
1705
+ textAnchor: placement.textAnchor,
1706
+ direction: placement.direction,
1593
1707
  hidden: object.hidden
1594
1708
  });
1595
1709
  }
1596
1710
  return labels;
1597
1711
  }
1598
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1712
+ function compareLabelPlacementOrder(left, right) {
1713
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1714
+ if (priorityDiff !== 0) {
1715
+ return priorityDiff;
1716
+ }
1717
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1718
+ if (renderPriorityDiff !== 0) {
1719
+ return renderPriorityDiff;
1720
+ }
1721
+ return left.sortKey - right.sortKey;
1722
+ }
1723
+ function labelPlacementPriority(object) {
1724
+ switch (object.object.type) {
1725
+ case "star":
1726
+ return 0;
1727
+ case "planet":
1728
+ return 1;
1729
+ case "moon":
1730
+ return 2;
1731
+ case "belt":
1732
+ case "ring":
1733
+ return 3;
1734
+ case "asteroid":
1735
+ case "comet":
1736
+ return 4;
1737
+ case "structure":
1738
+ case "phenomenon":
1739
+ return 5;
1740
+ }
1741
+ }
1742
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1743
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1744
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1745
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1746
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1747
+ const rect = createLabelRect(object, placement, labelMultiplier);
1748
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1749
+ return placement;
1750
+ }
1751
+ }
1752
+ }
1753
+ return null;
1754
+ }
1755
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1756
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1757
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1758
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1759
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1760
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1761
+ const preferHorizontal = object.object.type === "structure" || object.object.type === "phenomenon" || object.object.placement?.mode === "at" || object.object.placement?.mode === "surface" || object.object.placement?.mode === "free";
1762
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1763
+ }
1764
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1765
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1766
+ return object.y >= parent.y ? "below" : "above";
1767
+ }
1768
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1769
+ }
1770
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1771
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1772
+ return object.x >= parent.x ? "right" : "left";
1773
+ }
1774
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1775
+ }
1776
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1777
+ const step = 14 * labelMultiplier;
1778
+ switch (direction) {
1779
+ case "above": {
1780
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1781
+ return {
1782
+ x: object.x,
1783
+ labelY,
1784
+ secondaryY: labelY - 16 * labelMultiplier,
1785
+ textAnchor: "middle",
1786
+ direction
1787
+ };
1788
+ }
1789
+ case "below": {
1790
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1791
+ return {
1792
+ x: object.x,
1793
+ labelY,
1794
+ secondaryY: labelY + 16 * labelMultiplier,
1795
+ textAnchor: "middle",
1796
+ direction
1797
+ };
1798
+ }
1799
+ case "left": {
1800
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1801
+ const labelY = object.y - 4 * labelMultiplier;
1802
+ return {
1803
+ x,
1804
+ labelY,
1805
+ secondaryY: labelY + 16 * labelMultiplier,
1806
+ textAnchor: "end",
1807
+ direction
1808
+ };
1809
+ }
1810
+ case "right": {
1811
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1812
+ const labelY = object.y - 4 * labelMultiplier;
1813
+ return {
1814
+ x,
1815
+ labelY,
1816
+ secondaryY: labelY + 16 * labelMultiplier,
1817
+ textAnchor: "start",
1818
+ direction
1819
+ };
1820
+ }
1821
+ }
1822
+ }
1823
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1599
1824
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1600
1825
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1601
1826
  return [
@@ -1610,6 +1835,10 @@
1610
1835
  id: "relations",
1611
1836
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1612
1837
  },
1838
+ {
1839
+ id: "events",
1840
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1841
+ },
1613
1842
  {
1614
1843
  id: "objects",
1615
1844
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1621,7 +1850,7 @@
1621
1850
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1622
1851
  ];
1623
1852
  }
1624
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1853
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1625
1854
  const groups = /* @__PURE__ */ new Map();
1626
1855
  const ensureGroup = (groupId) => {
1627
1856
  if (!groupId) {
@@ -1670,7 +1899,7 @@
1670
1899
  }
1671
1900
  }
1672
1901
  for (const group of groups.values()) {
1673
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1902
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1674
1903
  }
1675
1904
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1676
1905
  }
@@ -1704,6 +1933,29 @@
1704
1933
  };
1705
1934
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1706
1935
  }
1936
+ function createSceneEvents(events, objects, activeEventId) {
1937
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1938
+ return events.map((event) => {
1939
+ const objectIds = [.../* @__PURE__ */ new Set([
1940
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1941
+ ...event.participantObjectIds
1942
+ ])];
1943
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1944
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1945
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1946
+ return {
1947
+ renderId: `${createRenderId(event.id)}-event`,
1948
+ eventId: event.id,
1949
+ event,
1950
+ objectIds,
1951
+ participantIds: [...event.participantObjectIds],
1952
+ targetObjectId: event.targetObjectId,
1953
+ x: centroidX,
1954
+ y: centroidY,
1955
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1956
+ };
1957
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1958
+ }
1707
1959
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1708
1960
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1709
1961
  const drafts = /* @__PURE__ */ new Map();
@@ -1751,13 +2003,18 @@
1751
2003
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1752
2004
  const title = document2.system?.title ?? document2.system?.properties.title;
1753
2005
  const label = title ? `${String(title)} Overview` : "Overview";
2006
+ const camera = normalizeViewCamera(null);
2007
+ const renderProjection = resolveRenderProjection(projection, camera);
1754
2008
  return {
1755
2009
  id: "overview",
1756
2010
  label,
1757
2011
  summary: "Fit the whole system with the current atlas defaults.",
1758
2012
  objectId: null,
1759
2013
  selectedObjectId: null,
2014
+ eventIds: [],
1760
2015
  projection,
2016
+ renderProjection,
2017
+ camera,
1761
2018
  preset,
1762
2019
  rotationDeg: 0,
1763
2020
  scale: null,
@@ -1793,6 +2050,9 @@
1793
2050
  draft.select = normalizedValue;
1794
2051
  }
1795
2052
  return;
2053
+ case "events":
2054
+ draft.eventIds = splitListValue(normalizedValue);
2055
+ return;
1796
2056
  case "projection":
1797
2057
  case "view":
1798
2058
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1804,6 +2064,30 @@
1804
2064
  case "angle":
1805
2065
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1806
2066
  return;
2067
+ case "camera.azimuth":
2068
+ draft.camera = {
2069
+ ...draft.camera ?? createEmptyViewCamera(),
2070
+ azimuth: parseFiniteNumber(normalizedValue)
2071
+ };
2072
+ return;
2073
+ case "camera.elevation":
2074
+ draft.camera = {
2075
+ ...draft.camera ?? createEmptyViewCamera(),
2076
+ elevation: parseFiniteNumber(normalizedValue)
2077
+ };
2078
+ return;
2079
+ case "camera.roll":
2080
+ draft.camera = {
2081
+ ...draft.camera ?? createEmptyViewCamera(),
2082
+ roll: parseFiniteNumber(normalizedValue)
2083
+ };
2084
+ return;
2085
+ case "camera.distance":
2086
+ draft.camera = {
2087
+ ...draft.camera ?? createEmptyViewCamera(),
2088
+ distance: parsePositiveNumber(normalizedValue)
2089
+ };
2090
+ return;
1807
2091
  case "zoom":
1808
2092
  case "scale":
1809
2093
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -1843,13 +2127,19 @@
1843
2127
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1844
2128
  const filter = normalizeViewpointFilter(draft.filter);
1845
2129
  const label = draft.label?.trim() || humanizeIdentifier(draft.id);
2130
+ const resolvedProjection = draft.projection ?? projection;
2131
+ const camera = normalizeViewCamera(draft.camera ?? null);
2132
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
1846
2133
  return {
1847
2134
  id: draft.id,
1848
2135
  label,
1849
2136
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1850
2137
  objectId,
1851
2138
  selectedObjectId,
1852
- projection: draft.projection ?? projection,
2139
+ eventIds: [...new Set(draft.eventIds ?? [])],
2140
+ projection: resolvedProjection,
2141
+ renderProjection,
2142
+ camera,
1853
2143
  preset: draft.preset ?? preset,
1854
2144
  rotationDeg: draft.rotationDeg ?? 0,
1855
2145
  scale: draft.scale ?? null,
@@ -1866,6 +2156,14 @@
1866
2156
  groupIds: []
1867
2157
  };
1868
2158
  }
2159
+ function createEmptyViewCamera() {
2160
+ return {
2161
+ azimuth: null,
2162
+ elevation: null,
2163
+ roll: null,
2164
+ distance: null
2165
+ };
2166
+ }
1869
2167
  function normalizeViewpointFilter(filter) {
1870
2168
  if (!filter) {
1871
2169
  return null;
@@ -1879,7 +2177,18 @@
1879
2177
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1880
2178
  }
1881
2179
  function parseViewProjection(value) {
1882
- return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
2180
+ switch (value.toLowerCase()) {
2181
+ case "topdown":
2182
+ return "topdown";
2183
+ case "isometric":
2184
+ return "isometric";
2185
+ case "orthographic":
2186
+ return "orthographic";
2187
+ case "perspective":
2188
+ return "perspective";
2189
+ default:
2190
+ return null;
2191
+ }
1883
2192
  }
1884
2193
  function parseRenderPreset(value) {
1885
2194
  const normalized = value.toLowerCase();
@@ -1906,7 +2215,7 @@
1906
2215
  next["orbits-front"] = enabled;
1907
2216
  continue;
1908
2217
  }
1909
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2218
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1910
2219
  next[rawLayer] = enabled;
1911
2220
  }
1912
2221
  }
@@ -1917,7 +2226,7 @@
1917
2226
  }
1918
2227
  function parseViewpointGroups(value, document2, relationships, objectMap) {
1919
2228
  return splitListValue(value).map((entry) => {
1920
- if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
2229
+ if (document2.schemaVersion === "2.1" || document2.schemaVersion === "2.5" || document2.groups.some((group) => group.id === entry)) {
1921
2230
  return entry;
1922
2231
  }
1923
2232
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -1954,7 +2263,7 @@
1954
2263
  }
1955
2264
  return parts.join(" - ");
1956
2265
  }
1957
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
2266
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1958
2267
  let minX = Number.POSITIVE_INFINITY;
1959
2268
  let minY = Number.POSITIVE_INFINITY;
1960
2269
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1984,7 +2293,7 @@
1984
2293
  for (const label of labels) {
1985
2294
  if (label.hidden)
1986
2295
  continue;
1987
- includeLabelBounds(label, include);
2296
+ includeLabelBounds(label, include, labelMultiplier);
1988
2297
  }
1989
2298
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1990
2299
  return createBounds(0, 0, width, height);
@@ -2022,13 +2331,10 @@
2022
2331
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
2023
2332
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
2024
2333
  }
2025
- function includeLabelBounds(label, include) {
2026
- const labelScale = 1;
2027
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
2028
- include(label.x - labelHalfWidth, label.y - 18);
2029
- include(label.x + labelHalfWidth, label.y + 8);
2030
- include(label.x - labelHalfWidth, label.secondaryY - 14);
2031
- include(label.x + labelHalfWidth, label.secondaryY + 8);
2334
+ function includeLabelBounds(label, include, labelMultiplier) {
2335
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
2336
+ include(bounds.left, bounds.top);
2337
+ include(bounds.right, bounds.bottom);
2032
2338
  }
2033
2339
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
2034
2340
  if (positions.has(object.id)) {
@@ -2418,7 +2724,7 @@
2418
2724
  return null;
2419
2725
  }
2420
2726
  }
2421
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2727
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2422
2728
  let minX = Number.POSITIVE_INFINITY;
2423
2729
  let minY = Number.POSITIVE_INFINITY;
2424
2730
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2447,7 +2753,7 @@
2447
2753
  }
2448
2754
  for (const label of labels) {
2449
2755
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2450
- includeLabelBounds(label, include);
2756
+ includeLabelBounds(label, include, labelMultiplier);
2451
2757
  }
2452
2758
  }
2453
2759
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2472,12 +2778,28 @@
2472
2778
  }
2473
2779
  return current.id;
2474
2780
  }
2475
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2781
+ function createLabelRect(object, placement, labelMultiplier) {
2782
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2783
+ }
2784
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2785
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2786
+ const labelWidth = labelHalfWidth * 2;
2787
+ const topPadding = direction === "above" ? 18 : 12;
2788
+ const bottomPadding = direction === "above" ? 8 : 12;
2789
+ let left = x - labelHalfWidth;
2790
+ let right = x + labelHalfWidth;
2791
+ if (textAnchor === "start") {
2792
+ left = x;
2793
+ right = x + labelWidth;
2794
+ } else if (textAnchor === "end") {
2795
+ left = x - labelWidth;
2796
+ right = x;
2797
+ }
2476
2798
  return {
2477
- left: x - labelHalfWidth,
2478
- right: x + labelHalfWidth,
2479
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2480
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2799
+ left,
2800
+ right,
2801
+ top: Math.min(labelY, secondaryY) - topPadding,
2802
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2481
2803
  };
2482
2804
  }
2483
2805
  function rectsOverlap(left, right) {
@@ -2664,11 +2986,6 @@
2664
2986
  function customColorFor(value) {
2665
2987
  return typeof value === "string" && value.trim() ? value : void 0;
2666
2988
  }
2667
- function estimateLabelHalfWidth(object, labelMultiplier) {
2668
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2669
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2670
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2671
- }
2672
2989
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2673
2990
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2674
2991
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2685,7 +3002,7 @@
2685
3002
  }
2686
3003
 
2687
3004
  // packages/core/dist/draft.js
2688
- function materializeAtlasDocument(document2) {
3005
+ function materializeAtlasDocument(document2, options = {}) {
2689
3006
  const system = document2.system ? {
2690
3007
  type: "system",
2691
3008
  id: document2.system.id,
@@ -2696,6 +3013,8 @@
2696
3013
  properties: materializeDraftSystemProperties(document2.system),
2697
3014
  info: materializeDraftSystemInfo(document2.system)
2698
3015
  } : null;
3016
+ const objects = document2.objects.map(cloneWorldOrbitObject);
3017
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2699
3018
  return {
2700
3019
  format: "worldorbit",
2701
3020
  version: "1.0",
@@ -2703,7 +3022,8 @@
2703
3022
  system,
2704
3023
  groups: structuredClone(document2.groups ?? []),
2705
3024
  relations: structuredClone(document2.relations ?? []),
2706
- objects: document2.objects.map(cloneWorldOrbitObject)
3025
+ events: document2.events.map(cloneWorldOrbitEvent),
3026
+ objects
2707
3027
  };
2708
3028
  }
2709
3029
  function cloneWorldOrbitObject(object) {
@@ -2725,6 +3045,75 @@
2725
3045
  info: { ...object.info }
2726
3046
  };
2727
3047
  }
3048
+ function cloneWorldOrbitEvent(event) {
3049
+ return {
3050
+ ...event,
3051
+ participantObjectIds: [...event.participantObjectIds],
3052
+ tags: [...event.tags],
3053
+ positions: event.positions.map(cloneWorldOrbitEventPose)
3054
+ };
3055
+ }
3056
+ function cloneWorldOrbitEventPose(pose) {
3057
+ return {
3058
+ objectId: pose.objectId,
3059
+ placement: clonePlacement(pose.placement),
3060
+ inner: pose.inner ? { ...pose.inner } : void 0,
3061
+ outer: pose.outer ? { ...pose.outer } : void 0,
3062
+ epoch: pose.epoch ?? null,
3063
+ referencePlane: pose.referencePlane ?? null
3064
+ };
3065
+ }
3066
+ function clonePlacement(placement) {
3067
+ return placement ? structuredClone(placement) : null;
3068
+ }
3069
+ function applyEventPoseOverrides(objects, events, activeEventId) {
3070
+ if (!activeEventId) {
3071
+ return;
3072
+ }
3073
+ const event = events.find((entry) => entry.id === activeEventId);
3074
+ if (!event) {
3075
+ return;
3076
+ }
3077
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
3078
+ const referencedIds = /* @__PURE__ */ new Set([
3079
+ ...event.targetObjectId ? [event.targetObjectId] : [],
3080
+ ...event.participantObjectIds,
3081
+ ...event.positions.map((pose) => pose.objectId)
3082
+ ]);
3083
+ for (const objectId of referencedIds) {
3084
+ const object = objectMap.get(objectId);
3085
+ if (!object) {
3086
+ continue;
3087
+ }
3088
+ if (event.epoch) {
3089
+ object.epoch = event.epoch;
3090
+ }
3091
+ if (event.referencePlane) {
3092
+ object.referencePlane = event.referencePlane;
3093
+ }
3094
+ }
3095
+ for (const pose of event.positions) {
3096
+ const object = objectMap.get(pose.objectId);
3097
+ if (!object) {
3098
+ continue;
3099
+ }
3100
+ if (pose.placement) {
3101
+ object.placement = clonePlacement(pose.placement);
3102
+ }
3103
+ if (pose.inner) {
3104
+ object.properties.inner = { ...pose.inner };
3105
+ }
3106
+ if (pose.outer) {
3107
+ object.properties.outer = { ...pose.outer };
3108
+ }
3109
+ if (pose.epoch) {
3110
+ object.epoch = pose.epoch;
3111
+ }
3112
+ if (pose.referencePlane) {
3113
+ object.referencePlane = pose.referencePlane;
3114
+ }
3115
+ }
3116
+ }
2728
3117
  function cloneProperties(properties) {
2729
3118
  const next = {};
2730
3119
  for (const [key, value] of Object.entries(properties)) {
@@ -2797,6 +3186,18 @@
2797
3186
  if (viewpoint.rotationDeg !== 0) {
2798
3187
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2799
3188
  }
3189
+ if (viewpoint.camera?.azimuth !== null) {
3190
+ info2[`${prefix}.camera.azimuth`] = String(viewpoint.camera?.azimuth);
3191
+ }
3192
+ if (viewpoint.camera?.elevation !== null) {
3193
+ info2[`${prefix}.camera.elevation`] = String(viewpoint.camera?.elevation);
3194
+ }
3195
+ if (viewpoint.camera?.roll !== null) {
3196
+ info2[`${prefix}.camera.roll`] = String(viewpoint.camera?.roll);
3197
+ }
3198
+ if (viewpoint.camera?.distance !== null) {
3199
+ info2[`${prefix}.camera.distance`] = String(viewpoint.camera?.distance);
3200
+ }
2800
3201
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2801
3202
  if (serializedLayers) {
2802
3203
  info2[`${prefix}.layers`] = serializedLayers;
@@ -2813,6 +3214,9 @@
2813
3214
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2814
3215
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2815
3216
  }
3217
+ if (viewpoint.events.length > 0) {
3218
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
3219
+ }
2816
3220
  }
2817
3221
  for (const annotation of system.annotations) {
2818
3222
  const prefix = `annotation.${annotation.id}`;
@@ -2837,7 +3241,7 @@
2837
3241
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2838
3242
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2839
3243
  }
2840
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3244
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2841
3245
  if (layers[key] !== void 0) {
2842
3246
  tokens.push(layers[key] ? key : `-${key}`);
2843
3247
  }
@@ -3011,6 +3415,7 @@
3011
3415
  const diagnostics = [];
3012
3416
  const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
3013
3417
  const groupIds = new Set(document2.groups.map((group) => group.id));
3418
+ const eventIds = new Set(document2.events.map((event) => event.id));
3014
3419
  if (!document2.system) {
3015
3420
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3016
3421
  }
@@ -3020,6 +3425,7 @@
3020
3425
  ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3021
3426
  ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
3022
3427
  ["relation", document2.relations.map((relation) => relation.id)],
3428
+ ["event", document2.events.map((event) => event.id)],
3023
3429
  ["object", document2.objects.map((object) => object.id)]
3024
3430
  ]) {
3025
3431
  for (const id of ids) {
@@ -3035,11 +3441,14 @@
3035
3441
  validateRelation(relation, objectMap, diagnostics);
3036
3442
  }
3037
3443
  for (const viewpoint of document2.system?.viewpoints ?? []) {
3038
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3444
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
3039
3445
  }
3040
3446
  for (const object of document2.objects) {
3041
3447
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3042
3448
  }
3449
+ for (const event of document2.events) {
3450
+ validateEvent(event, document2.system, objectMap, diagnostics);
3451
+ }
3043
3452
  return diagnostics;
3044
3453
  }
3045
3454
  function validateRelation(relation, objectMap, diagnostics) {
@@ -3057,15 +3466,24 @@
3057
3466
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3058
3467
  }
3059
3468
  }
3060
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3061
- if (!filter || sourceSchemaVersion !== "2.1") {
3062
- return;
3063
- }
3064
- for (const groupId of filter.groupIds) {
3065
- if (!groupIds.has(groupId)) {
3066
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3469
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
3470
+ const filter = viewpoint.filter;
3471
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
3472
+ if (filter) {
3473
+ for (const groupId of filter.groupIds) {
3474
+ if (!groupIds.has(groupId)) {
3475
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
3476
+ }
3477
+ }
3478
+ }
3479
+ for (const eventId of viewpoint.events ?? []) {
3480
+ if (!eventIds.has(eventId)) {
3481
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
3482
+ }
3067
3483
  }
3068
3484
  }
3485
+ validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
3486
+ validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
3069
3487
  }
3070
3488
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
3071
3489
  const placement = object.placement;
@@ -3078,6 +3496,12 @@
3078
3496
  }
3079
3497
  }
3080
3498
  }
3499
+ if (typeof object.epoch === "string" && !object.epoch.trim()) {
3500
+ diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
3501
+ }
3502
+ if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
3503
+ diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
3504
+ }
3081
3505
  if (orbitPlacement) {
3082
3506
  if (!objectMap.has(orbitPlacement.target)) {
3083
3507
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -3149,6 +3573,122 @@
3149
3573
  }
3150
3574
  }
3151
3575
  }
3576
+ function validateEvent(event, system, objectMap, diagnostics) {
3577
+ const fieldPrefix = `event.${event.id}`;
3578
+ const referencedIds = /* @__PURE__ */ new Set();
3579
+ if (!event.kind.trim()) {
3580
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3581
+ }
3582
+ if (typeof event.epoch === "string" && !event.epoch.trim()) {
3583
+ diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, void 0, `${fieldPrefix}.epoch`));
3584
+ }
3585
+ if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
3586
+ diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, void 0, `${fieldPrefix}.referencePlane`));
3587
+ }
3588
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3589
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3590
+ }
3591
+ if (event.targetObjectId) {
3592
+ referencedIds.add(event.targetObjectId);
3593
+ if (!objectMap.has(event.targetObjectId)) {
3594
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3595
+ }
3596
+ }
3597
+ const seenParticipants = /* @__PURE__ */ new Set();
3598
+ for (const participantId of event.participantObjectIds) {
3599
+ referencedIds.add(participantId);
3600
+ if (seenParticipants.has(participantId)) {
3601
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3602
+ continue;
3603
+ }
3604
+ seenParticipants.add(participantId);
3605
+ if (!objectMap.has(participantId)) {
3606
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3607
+ }
3608
+ }
3609
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3610
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3611
+ }
3612
+ if (event.positions.length === 0) {
3613
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3614
+ }
3615
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3616
+ diagnostics.push(warn("validate.event.kind.participants", `Event "${event.id}" looks like an eclipse or transit but references fewer than three bodies.`, void 0, `${fieldPrefix}.participants`));
3617
+ }
3618
+ const poseIds = /* @__PURE__ */ new Set();
3619
+ for (const pose of event.positions) {
3620
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3621
+ if (poseIds.has(pose.objectId)) {
3622
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3623
+ continue;
3624
+ }
3625
+ poseIds.add(pose.objectId);
3626
+ const object = objectMap.get(pose.objectId);
3627
+ if (!object) {
3628
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3629
+ continue;
3630
+ }
3631
+ if (!referencedIds.has(pose.objectId)) {
3632
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3633
+ }
3634
+ validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
3635
+ }
3636
+ const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
3637
+ if (event.positions.length > 0 && missingPoseIds.length > 0) {
3638
+ diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, void 0, `${fieldPrefix}.positions`));
3639
+ }
3640
+ }
3641
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
3642
+ const placement = pose.placement;
3643
+ if (!placement) {
3644
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3645
+ return;
3646
+ }
3647
+ if (placement.mode === "orbit") {
3648
+ if (!objectMap.has(placement.target)) {
3649
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3650
+ }
3651
+ if (placement.distance && placement.semiMajor) {
3652
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3653
+ }
3654
+ if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
3655
+ diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, void 0, `${fieldPrefix}.phase`));
3656
+ }
3657
+ if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
3658
+ diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, void 0, `${fieldPrefix}.inclination`));
3659
+ }
3660
+ if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
3661
+ diagnostics.push(warn("validate.event.pose.period.massMissing", `Event "${eventId}" pose "${pose.objectId}" sets "period" but its central mass cannot be derived.`, void 0, `${fieldPrefix}.period`));
3662
+ }
3663
+ return;
3664
+ }
3665
+ if (placement.mode === "surface") {
3666
+ const target = objectMap.get(placement.target);
3667
+ if (!target) {
3668
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3669
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3670
+ diagnostics.push(error("validate.event.pose.surface.target.invalid", `Event surface target "${placement.target}" on "${eventId}:${pose.objectId}" is not surface-capable.`, void 0, `${fieldPrefix}.surface`));
3671
+ }
3672
+ return;
3673
+ }
3674
+ if (placement.mode === "at") {
3675
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3676
+ diagnostics.push(error("validate.event.pose.at.objectType", `Only structures and phenomena may use "at" placement in events; found "${object.type}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3677
+ }
3678
+ const reference = placement.reference;
3679
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3680
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3681
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3682
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3683
+ } else if (reference.kind === "lagrange") {
3684
+ if (!objectMap.has(reference.primary)) {
3685
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3686
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3687
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3688
+ }
3689
+ }
3690
+ }
3691
+ }
3152
3692
  function validateAtTarget(object, objectMap, diagnostics) {
3153
3693
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3154
3694
  if (!reference) {
@@ -3254,6 +3794,52 @@
3254
3794
  return null;
3255
3795
  }
3256
3796
  }
3797
+ function validateProjection(projection, diagnostics, field, viewpointId) {
3798
+ if (projection !== "topdown" && projection !== "isometric" && projection !== "orthographic" && projection !== "perspective") {
3799
+ diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, void 0, field));
3800
+ }
3801
+ }
3802
+ function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
3803
+ if (!camera) {
3804
+ return;
3805
+ }
3806
+ const prefix = `viewpoint.${viewpointId}.camera`;
3807
+ for (const [key, value] of [
3808
+ ["azimuth", camera.azimuth],
3809
+ ["elevation", camera.elevation],
3810
+ ["roll", camera.roll],
3811
+ ["distance", camera.distance]
3812
+ ]) {
3813
+ if (value !== null && (!Number.isFinite(value) || key === "distance" && value <= 0)) {
3814
+ diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, void 0, `${prefix}.${key}`));
3815
+ }
3816
+ }
3817
+ if (camera.distance !== null && projection !== "perspective") {
3818
+ diagnostics.push(warn("validate.viewpoint.camera.distance.partialEffect", `Camera "distance" only has a semantic effect in perspective viewpoints; "${viewpointId}" uses "${projection}".`, void 0, `${prefix}.distance`));
3819
+ }
3820
+ if (projection === "topdown" && (camera.elevation !== null || camera.roll !== null)) {
3821
+ diagnostics.push(warn("validate.viewpoint.camera.topdownPartial", `Camera elevation/roll on topdown viewpoint "${viewpointId}" are currently stored for future 3D use and only partially affect 2D rendering.`, void 0, prefix));
3822
+ }
3823
+ if (projection === "isometric" && camera.elevation !== null) {
3824
+ diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, void 0, `${prefix}.elevation`));
3825
+ }
3826
+ if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
3827
+ diagnostics.push(warn("validate.viewpoint.rotation.cameraOverlap", `Viewpoint "${viewpointId}" uses camera.azimuth; keep "rotation" only for 2D screen rotation to avoid ambiguity.`, void 0, `${prefix}.azimuth`));
3828
+ }
3829
+ const hasAnchor = focusObjectId !== null && objectMap.has(focusObjectId) || selectedObjectId !== null && objectMap.has(selectedObjectId) || !!filter;
3830
+ if (!hasAnchor) {
3831
+ diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, void 0, prefix));
3832
+ }
3833
+ }
3834
+ function resolveEffectiveEpoch(system, object, event, pose) {
3835
+ return normalizeOptionalContextString(pose?.epoch) ?? normalizeOptionalContextString(event?.epoch) ?? normalizeOptionalContextString(object.epoch) ?? normalizeOptionalContextString(system?.epoch) ?? null;
3836
+ }
3837
+ function resolveEffectiveReferencePlane(system, object, event, pose) {
3838
+ return normalizeOptionalContextString(pose?.referencePlane) ?? normalizeOptionalContextString(event?.referencePlane) ?? normalizeOptionalContextString(object.referencePlane) ?? normalizeOptionalContextString(system?.referencePlane) ?? null;
3839
+ }
3840
+ function normalizeOptionalContextString(value) {
3841
+ return typeof value === "string" && value.trim() ? value.trim() : null;
3842
+ }
3257
3843
  function toleranceForField(object, field) {
3258
3844
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3259
3845
  if (typeof tolerance === "number") {
@@ -3349,6 +3935,23 @@
3349
3935
  });
3350
3936
  }
3351
3937
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3938
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3939
+ "orbit",
3940
+ "distance",
3941
+ "semiMajor",
3942
+ "eccentricity",
3943
+ "period",
3944
+ "angle",
3945
+ "inclination",
3946
+ "phase",
3947
+ "at",
3948
+ "surface",
3949
+ "free",
3950
+ "inner",
3951
+ "outer",
3952
+ "epoch",
3953
+ "referencePlane"
3954
+ ]);
3352
3955
  function parseWorldOrbitAtlas(source) {
3353
3956
  return parseAtlasSource(source);
3354
3957
  }
@@ -3363,12 +3966,15 @@
3363
3966
  const objectNodes = [];
3364
3967
  const groups = [];
3365
3968
  const relations = [];
3969
+ const events = [];
3970
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3366
3971
  let sawDefaults = false;
3367
3972
  let sawAtlas = false;
3368
3973
  const viewpointIds = /* @__PURE__ */ new Set();
3369
3974
  const annotationIds = /* @__PURE__ */ new Set();
3370
3975
  const groupIds = /* @__PURE__ */ new Set();
3371
3976
  const relationIds = /* @__PURE__ */ new Set();
3977
+ const eventIds = /* @__PURE__ */ new Set();
3372
3978
  for (let index = 0; index < lines.length; index++) {
3373
3979
  const rawLine = lines[index];
3374
3980
  const lineNumber = index + 1;
@@ -3386,7 +3992,7 @@
3386
3992
  if (!sawSchemaHeader) {
3387
3993
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3388
3994
  sawSchemaHeader = true;
3389
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3995
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3390
3996
  diagnostics.push({
3391
3997
  code: "parse.schema21.commentCompatibility",
3392
3998
  severity: "warning",
@@ -3399,7 +4005,7 @@
3399
4005
  continue;
3400
4006
  }
3401
4007
  if (indent === 0) {
3402
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
4008
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3403
4009
  if (section.kind === "system") {
3404
4010
  system = section.system;
3405
4011
  } else if (section.kind === "defaults") {
@@ -3418,6 +4024,7 @@
3418
4024
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3419
4025
  }
3420
4026
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4027
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3421
4028
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3422
4029
  const baseDocument = {
3423
4030
  format: "worldorbit",
@@ -3425,6 +4032,7 @@
3425
4032
  system,
3426
4033
  groups,
3427
4034
  relations,
4035
+ events: normalizedEvents,
3428
4036
  objects,
3429
4037
  diagnostics
3430
4038
  };
@@ -3454,13 +4062,13 @@
3454
4062
  return document2;
3455
4063
  }
3456
4064
  function assertDraftSchemaHeader(tokens, line) {
3457
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3458
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4065
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
4066
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", "schema 2.5", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3459
4067
  }
3460
4068
  const version = tokens[1].value.toLowerCase();
3461
- return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
4069
+ return version === "2.5" ? "2.5" : version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3462
4070
  }
3463
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
4071
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3464
4072
  const keyword = tokens[0]?.value.toLowerCase();
3465
4073
  switch (keyword) {
3466
4074
  case "system":
@@ -3478,6 +4086,8 @@
3478
4086
  return {
3479
4087
  kind: "defaults",
3480
4088
  system,
4089
+ sourceSchemaVersion,
4090
+ diagnostics,
3481
4091
  seenFields: /* @__PURE__ */ new Set()
3482
4092
  };
3483
4093
  case "atlas":
@@ -3497,7 +4107,7 @@
3497
4107
  if (!system) {
3498
4108
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3499
4109
  }
3500
- return startViewpointSection(tokens, line, system, viewpointIds);
4110
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3501
4111
  case "annotation":
3502
4112
  if (!system) {
3503
4113
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3509,6 +4119,9 @@
3509
4119
  case "relation":
3510
4120
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3511
4121
  return startRelationSection(tokens, line, relations, relationIds);
4122
+ case "event":
4123
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4124
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3512
4125
  case "object":
3513
4126
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3514
4127
  default:
@@ -3545,7 +4158,7 @@
3545
4158
  seenFields: /* @__PURE__ */ new Set()
3546
4159
  };
3547
4160
  }
3548
- function startViewpointSection(tokens, line, system, viewpointIds) {
4161
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3549
4162
  if (tokens.length !== 2) {
3550
4163
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3551
4164
  }
@@ -3562,10 +4175,12 @@
3562
4175
  summary: "",
3563
4176
  focusObjectId: null,
3564
4177
  selectedObjectId: null,
4178
+ events: [],
3565
4179
  projection: system.defaults.view,
3566
4180
  preset: system.defaults.preset,
3567
4181
  zoom: null,
3568
4182
  rotationDeg: 0,
4183
+ camera: null,
3569
4184
  layers: {},
3570
4185
  filter: null
3571
4186
  };
@@ -3574,10 +4189,15 @@
3574
4189
  return {
3575
4190
  kind: "viewpoint",
3576
4191
  viewpoint,
4192
+ sourceSchemaVersion,
4193
+ diagnostics,
3577
4194
  seenFields: /* @__PURE__ */ new Set(),
3578
4195
  inFilter: false,
3579
4196
  filterIndent: null,
3580
- seenFilterFields: /* @__PURE__ */ new Set()
4197
+ seenFilterFields: /* @__PURE__ */ new Set(),
4198
+ inCamera: false,
4199
+ cameraIndent: null,
4200
+ seenCameraFields: /* @__PURE__ */ new Set()
3581
4201
  };
3582
4202
  }
3583
4203
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -3664,6 +4284,51 @@
3664
4284
  seenFields: /* @__PURE__ */ new Set()
3665
4285
  };
3666
4286
  }
4287
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4288
+ if (tokens.length !== 2) {
4289
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4290
+ }
4291
+ const id = normalizeIdentifier(tokens[1].value);
4292
+ if (!id) {
4293
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4294
+ }
4295
+ if (eventIds.has(id)) {
4296
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4297
+ }
4298
+ const event = {
4299
+ id,
4300
+ kind: "",
4301
+ label: humanizeIdentifier2(id),
4302
+ summary: null,
4303
+ targetObjectId: null,
4304
+ participantObjectIds: [],
4305
+ timing: null,
4306
+ visibility: null,
4307
+ epoch: null,
4308
+ referencePlane: null,
4309
+ tags: [],
4310
+ color: null,
4311
+ hidden: false,
4312
+ positions: []
4313
+ };
4314
+ const rawPoses = [];
4315
+ events.push(event);
4316
+ eventPoseNodes.set(id, rawPoses);
4317
+ eventIds.add(id);
4318
+ return {
4319
+ kind: "event",
4320
+ event,
4321
+ sourceSchemaVersion,
4322
+ diagnostics,
4323
+ seenFields: /* @__PURE__ */ new Set(),
4324
+ rawPoses,
4325
+ inPositions: false,
4326
+ positionsIndent: null,
4327
+ activePose: null,
4328
+ poseIndent: null,
4329
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4330
+ };
4331
+ }
3667
4332
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3668
4333
  if (tokens.length < 3) {
3669
4334
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -3720,6 +4385,9 @@
3720
4385
  case "relation":
3721
4386
  applyRelationField(section, tokens, line);
3722
4387
  return;
4388
+ case "event":
4389
+ applyEventField(section, indent, tokens, line);
4390
+ return;
3723
4391
  case "object":
3724
4392
  applyObjectField(section, indent, tokens, line);
3725
4393
  return;
@@ -3762,6 +4430,12 @@
3762
4430
  const value = joinFieldValue(tokens, line);
3763
4431
  switch (key) {
3764
4432
  case "view":
4433
+ if (isSchema25Projection(value)) {
4434
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
4435
+ line,
4436
+ column: tokens[0].column
4437
+ });
4438
+ }
3765
4439
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
3766
4440
  return;
3767
4441
  case "scale":
@@ -3801,14 +4475,36 @@
3801
4475
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
3802
4476
  }
3803
4477
  function applyViewpointField2(section, indent, tokens, line) {
4478
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
4479
+ section.inCamera = false;
4480
+ section.cameraIndent = null;
4481
+ }
3804
4482
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
3805
4483
  section.inFilter = false;
3806
4484
  section.filterIndent = null;
3807
4485
  }
4486
+ if (section.inCamera) {
4487
+ applyViewpointCameraField(section, tokens, line);
4488
+ return;
4489
+ }
3808
4490
  if (section.inFilter) {
3809
4491
  applyViewpointFilterField(section, tokens, line);
3810
4492
  return;
3811
4493
  }
4494
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
4495
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4496
+ line,
4497
+ column: tokens[0].column
4498
+ });
4499
+ if (section.seenFields.has("camera")) {
4500
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
4501
+ }
4502
+ section.seenFields.add("camera");
4503
+ section.inCamera = true;
4504
+ section.cameraIndent = indent;
4505
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4506
+ return;
4507
+ }
3812
4508
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
3813
4509
  if (section.seenFields.has("filter")) {
3814
4510
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -3834,6 +4530,12 @@
3834
4530
  section.viewpoint.selectedObjectId = value;
3835
4531
  return;
3836
4532
  case "projection":
4533
+ if (isSchema25Projection(value)) {
4534
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
4535
+ line,
4536
+ column: tokens[0].column
4537
+ });
4538
+ }
3837
4539
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
3838
4540
  return;
3839
4541
  case "preset":
@@ -3845,13 +4547,49 @@
3845
4547
  case "rotation":
3846
4548
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3847
4549
  return;
4550
+ case "camera":
4551
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4552
+ line,
4553
+ column: tokens[0].column
4554
+ });
4555
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
4556
+ return;
3848
4557
  case "layers":
3849
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4558
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4559
+ return;
4560
+ case "events":
4561
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4562
+ line,
4563
+ column: tokens[0].column
4564
+ });
4565
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3850
4566
  return;
3851
4567
  default:
3852
4568
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
3853
4569
  }
3854
4570
  }
4571
+ function applyViewpointCameraField(section, tokens, line) {
4572
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
4573
+ const value = joinFieldValue(tokens, line);
4574
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4575
+ switch (key) {
4576
+ case "azimuth":
4577
+ camera.azimuth = parseFiniteNumber2(value, line, tokens[0].column, "camera.azimuth");
4578
+ break;
4579
+ case "elevation":
4580
+ camera.elevation = parseFiniteNumber2(value, line, tokens[0].column, "camera.elevation");
4581
+ break;
4582
+ case "roll":
4583
+ camera.roll = parseFiniteNumber2(value, line, tokens[0].column, "camera.roll");
4584
+ break;
4585
+ case "distance":
4586
+ camera.distance = parsePositiveNumber2(value, line, tokens[0].column, "camera.distance");
4587
+ break;
4588
+ default:
4589
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
4590
+ }
4591
+ section.viewpoint.camera = camera;
4592
+ }
3855
4593
  function applyViewpointFilterField(section, tokens, line) {
3856
4594
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
3857
4595
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -3951,6 +4689,126 @@
3951
4689
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
3952
4690
  }
3953
4691
  }
4692
+ function applyEventField(section, indent, tokens, line) {
4693
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4694
+ section.activePose = null;
4695
+ section.poseIndent = null;
4696
+ section.activePoseSeenFields.clear();
4697
+ }
4698
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4699
+ section.inPositions = false;
4700
+ section.positionsIndent = null;
4701
+ }
4702
+ if (section.activePose) {
4703
+ if (tokens[0]?.value === "epoch" || tokens[0]?.value === "referencePlane") {
4704
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
4705
+ line,
4706
+ column: tokens[0]?.column ?? 1
4707
+ });
4708
+ }
4709
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4710
+ return;
4711
+ }
4712
+ if (section.inPositions) {
4713
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4714
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4715
+ }
4716
+ const objectId = tokens[1].value;
4717
+ if (!objectId.trim()) {
4718
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4719
+ }
4720
+ const rawPose = {
4721
+ objectId,
4722
+ fields: [],
4723
+ location: { line, column: tokens[0].column }
4724
+ };
4725
+ section.rawPoses.push(rawPose);
4726
+ section.activePose = rawPose;
4727
+ section.poseIndent = indent;
4728
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4729
+ return;
4730
+ }
4731
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4732
+ if (section.seenFields.has("positions")) {
4733
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4734
+ }
4735
+ section.seenFields.add("positions");
4736
+ section.inPositions = true;
4737
+ section.positionsIndent = indent;
4738
+ return;
4739
+ }
4740
+ const key = requireUniqueField(tokens, section.seenFields, line);
4741
+ switch (key) {
4742
+ case "kind":
4743
+ section.event.kind = joinFieldValue(tokens, line);
4744
+ return;
4745
+ case "label":
4746
+ section.event.label = joinFieldValue(tokens, line);
4747
+ return;
4748
+ case "summary":
4749
+ section.event.summary = joinFieldValue(tokens, line);
4750
+ return;
4751
+ case "target":
4752
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4753
+ return;
4754
+ case "participants":
4755
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4756
+ return;
4757
+ case "timing":
4758
+ section.event.timing = joinFieldValue(tokens, line);
4759
+ return;
4760
+ case "visibility":
4761
+ section.event.visibility = joinFieldValue(tokens, line);
4762
+ return;
4763
+ case "epoch":
4764
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
4765
+ line,
4766
+ column: tokens[0].column
4767
+ });
4768
+ section.event.epoch = joinFieldValue(tokens, line);
4769
+ return;
4770
+ case "referenceplane":
4771
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
4772
+ line,
4773
+ column: tokens[0].column
4774
+ });
4775
+ section.event.referencePlane = joinFieldValue(tokens, line);
4776
+ return;
4777
+ case "tags":
4778
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4779
+ return;
4780
+ case "color":
4781
+ section.event.color = joinFieldValue(tokens, line);
4782
+ return;
4783
+ case "hidden":
4784
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4785
+ line,
4786
+ column: tokens[0].column
4787
+ });
4788
+ return;
4789
+ default:
4790
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4791
+ }
4792
+ }
4793
+ function parseEventPoseField(tokens, line, seenFields) {
4794
+ if (tokens.length < 2) {
4795
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4796
+ }
4797
+ const key = tokens[0].value;
4798
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4799
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4800
+ }
4801
+ if (seenFields.has(key)) {
4802
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4803
+ }
4804
+ seenFields.add(key);
4805
+ return {
4806
+ type: "field",
4807
+ key,
4808
+ values: tokens.slice(1).map((token) => token.value),
4809
+ location: { line, column: tokens[0].column }
4810
+ };
4811
+ }
3954
4812
  function applyObjectField(section, indent, tokens, line) {
3955
4813
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
3956
4814
  section.activeBlock = null;
@@ -4009,7 +4867,7 @@
4009
4867
  function parseObjectTypeTokens(tokens, line) {
4010
4868
  return parseTokenList(tokens, line, "objectTypes").filter((value) => value === "star" || value === "planet" || value === "moon" || value === "belt" || value === "asteroid" || value === "comet" || value === "ring" || value === "structure" || value === "phenomenon");
4011
4869
  }
4012
- function parseLayerTokens(tokens, line) {
4870
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4013
4871
  const layers = {};
4014
4872
  for (const token of parseTokenList(tokens, line, "layers")) {
4015
4873
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -4019,7 +4877,13 @@
4019
4877
  layers["orbits-front"] = enabled;
4020
4878
  continue;
4021
4879
  }
4022
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4880
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4881
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4882
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4883
+ line,
4884
+ column: tokens[0]?.column ?? 1
4885
+ });
4886
+ }
4023
4887
  layers[raw] = enabled;
4024
4888
  }
4025
4889
  }
@@ -4037,11 +4901,15 @@
4037
4901
  }
4038
4902
  function parseProjectionValue(value, line, column) {
4039
4903
  const normalized = value.toLowerCase();
4040
- if (normalized !== "topdown" && normalized !== "isometric") {
4904
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
4041
4905
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4042
4906
  }
4043
4907
  return normalized;
4044
4908
  }
4909
+ function isSchema25Projection(value) {
4910
+ const normalized = value.toLowerCase();
4911
+ return normalized === "orthographic" || normalized === "perspective";
4912
+ }
4045
4913
  function parsePresetValue(value, line, column) {
4046
4914
  const normalized = value.toLowerCase();
4047
4915
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -4071,6 +4939,48 @@
4071
4939
  groupIds: []
4072
4940
  };
4073
4941
  }
4942
+ function createEmptyViewCamera2() {
4943
+ return {
4944
+ azimuth: null,
4945
+ elevation: null,
4946
+ roll: null,
4947
+ distance: null
4948
+ };
4949
+ }
4950
+ function parseInlineViewCamera(tokens, line, current) {
4951
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
4952
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
4953
+ }
4954
+ const camera = current ? { ...current } : createEmptyViewCamera2();
4955
+ const seen = /* @__PURE__ */ new Set();
4956
+ for (let index = 0; index < tokens.length; index += 2) {
4957
+ const fieldToken = tokens[index];
4958
+ const valueToken = tokens[index + 1];
4959
+ const key = fieldToken.value.toLowerCase();
4960
+ if (seen.has(key)) {
4961
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
4962
+ }
4963
+ seen.add(key);
4964
+ const value = valueToken.value;
4965
+ switch (key) {
4966
+ case "azimuth":
4967
+ camera.azimuth = parseFiniteNumber2(value, line, fieldToken.column, "camera.azimuth");
4968
+ break;
4969
+ case "elevation":
4970
+ camera.elevation = parseFiniteNumber2(value, line, fieldToken.column, "camera.elevation");
4971
+ break;
4972
+ case "roll":
4973
+ camera.roll = parseFiniteNumber2(value, line, fieldToken.column, "camera.roll");
4974
+ break;
4975
+ case "distance":
4976
+ camera.distance = parsePositiveNumber2(value, line, fieldToken.column, "camera.distance");
4977
+ break;
4978
+ default:
4979
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
4980
+ }
4981
+ }
4982
+ return camera;
4983
+ }
4074
4984
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
4075
4985
  const fields = [];
4076
4986
  let index = 0;
@@ -4158,7 +5068,7 @@
4158
5068
  }
4159
5069
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4160
5070
  const fieldMap = collectDraftFields(node.fields);
4161
- const placement = extractDraftPlacement(node.objectType, fieldMap);
5071
+ const placement = extractPlacementFromFieldMap(fieldMap);
4162
5072
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
4163
5073
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4164
5074
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -4203,21 +5113,41 @@
4203
5113
  object.tolerances = tolerances;
4204
5114
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
4205
5115
  object.typedBlocks = typedBlocks;
4206
- if (sourceSchemaVersion !== "2.1") {
5116
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4207
5117
  if (object.groups || object.epoch || object.referencePlane || object.tidalLock !== void 0 || object.resonance || object.renderHints || object.deriveRules?.length || object.validationRules?.length || object.lockedFields?.length || object.tolerances?.length || object.typedBlocks) {
4208
5118
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4209
5119
  }
4210
5120
  }
4211
5121
  return object;
4212
5122
  }
4213
- function collectDraftFields(fields) {
5123
+ function normalizeDraftEvent(event, rawPoses) {
5124
+ return {
5125
+ ...event,
5126
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5127
+ tags: [...new Set(event.tags)],
5128
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5129
+ };
5130
+ }
5131
+ function normalizeDraftEventPose(rawPose) {
5132
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
5133
+ const placement = extractPlacementFromFieldMap(fieldMap);
5134
+ return {
5135
+ objectId: rawPose.objectId,
5136
+ placement,
5137
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5138
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
5139
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
5140
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
5141
+ };
5142
+ }
5143
+ function collectDraftFields(fields, _mode = "object") {
4214
5144
  const grouped = /* @__PURE__ */ new Map();
4215
5145
  for (const field of fields) {
4216
5146
  const spec = getDraftObjectFieldSpec(field.key);
4217
- if (!spec) {
5147
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
4218
5148
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4219
5149
  }
4220
- if (!spec.allowRepeat && grouped.has(field.key)) {
5150
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
4221
5151
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4222
5152
  }
4223
5153
  const existing = grouped.get(field.key) ?? [];
@@ -4226,7 +5156,7 @@
4226
5156
  }
4227
5157
  return grouped;
4228
5158
  }
4229
- function extractDraftPlacement(objectType, fieldMap) {
5159
+ function extractPlacementFromFieldMap(fieldMap) {
4230
5160
  const orbitField = fieldMap.get("orbit")?.[0];
4231
5161
  const atField = fieldMap.get("at")?.[0];
4232
5162
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4394,7 +5324,7 @@
4394
5324
  }
4395
5325
  }
4396
5326
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4397
- if (sourceSchemaVersion === "2.1") {
5327
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4398
5328
  return;
4399
5329
  }
4400
5330
  diagnostics.push({
@@ -4406,6 +5336,34 @@
4406
5336
  column: location.column
4407
5337
  });
4408
5338
  }
5339
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5340
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
5341
+ return;
5342
+ }
5343
+ diagnostics.push({
5344
+ code: "parse.schema25.featureCompatibility",
5345
+ severity: "warning",
5346
+ source: "parse",
5347
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5348
+ line: location.line,
5349
+ column: location.column
5350
+ });
5351
+ }
5352
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
5353
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
5354
+ }
5355
+ function schemaVersionRank(version) {
5356
+ switch (version) {
5357
+ case "2.0-draft":
5358
+ return 0;
5359
+ case "2.0":
5360
+ return 1;
5361
+ case "2.1":
5362
+ return 2;
5363
+ case "2.5":
5364
+ return 3;
5365
+ }
5366
+ }
4409
5367
  function preprocessAtlasSource(source) {
4410
5368
  const chars = [...source];
4411
5369
  const comments = [];
@@ -4493,8 +5451,9 @@
4493
5451
  }
4494
5452
 
4495
5453
  // packages/core/dist/load.js
4496
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5454
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
4497
5455
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
5456
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
4498
5457
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
4499
5458
  function detectWorldOrbitSchemaVersion(source) {
4500
5459
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -4508,6 +5467,9 @@
4508
5467
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
4509
5468
  return "2.1";
4510
5469
  }
5470
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
5471
+ return "2.5";
5472
+ }
4511
5473
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
4512
5474
  return "2.0";
4513
5475
  }
@@ -4568,7 +5530,7 @@
4568
5530
  }
4569
5531
  function loadWorldOrbitSourceWithDiagnostics(source) {
4570
5532
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
4571
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
5533
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1" || schemaVersion === "2.5") {
4572
5534
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
4573
5535
  }
4574
5536
  let ast;
@@ -4837,6 +5799,7 @@
4837
5799
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
4838
5800
  const leaderMarkup = layers.guides ? scene.leaders.filter((leader) => !leader.hidden).filter((leader) => visibleObjectIds.has(leader.objectId)).filter((leader) => layers.structures || !isStructureLike(leader.object)).map((leader) => `<line class="wo-leader wo-leader-${leader.mode}" x1="${leader.x1}" y1="${leader.y1}" x2="${leader.x2}" y2="${leader.y2}" data-render-id="${escapeXml(leader.renderId)}" data-group-id="${escapeAttribute(leader.groupId ?? "")}" />`).join("") : "";
4839
5801
  const relationMarkup = layers.relations ? scene.relations.filter((relation) => !relation.hidden).filter((relation) => visibleObjectIds.has(relation.fromObjectId) && visibleObjectIds.has(relation.toObjectId)).map((relation) => `<line class="wo-relation" x1="${relation.x1}" y1="${relation.y1}" x2="${relation.x2}" y2="${relation.y2}" data-render-id="${escapeXml(relation.renderId)}" data-relation-id="${escapeAttribute(relation.relationId)}" />`).join("") : "";
5802
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
4840
5803
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
4841
5804
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
4842
5805
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -4872,6 +5835,9 @@
4872
5835
  .wo-orbit-front { opacity: 0.9; }
4873
5836
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
4874
5837
  .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5838
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5839
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5840
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
4875
5841
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
4876
5842
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
4877
5843
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -4906,6 +5872,7 @@
4906
5872
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
4907
5873
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
4908
5874
  ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5875
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
4909
5876
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
4910
5877
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
4911
5878
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -4913,6 +5880,20 @@
4913
5880
  </g>
4914
5881
  </g>
4915
5882
  </svg>`;
5883
+ }
5884
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5885
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5886
+ if (participants.length === 0) {
5887
+ return "";
5888
+ }
5889
+ const stroke = event.event.color || theme.accent;
5890
+ const label = event.event.label || event.event.id;
5891
+ const lineMarkup = participants.map((object) => `<line class="wo-event-line" x1="${event.x}" y1="${event.y}" x2="${object.x}" y2="${object.y}" stroke="${escapeAttribute(stroke)}" data-event-id="${escapeAttribute(event.eventId)}" data-object-id="${escapeAttribute(object.objectId)}" />`).join("");
5892
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5893
+ ${lineMarkup}
5894
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5895
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5896
+ </g>`;
4916
5897
  }
4917
5898
  function renderDocumentToSvg(document2, options = {}) {
4918
5899
  return renderSceneToSvg(renderDocumentToScene(document2, options), options);
@@ -5512,6 +6493,13 @@
5512
6493
  value: `${details.object.resonance.targetObjectId} ${details.object.resonance.ratio}`
5513
6494
  });
5514
6495
  }
6496
+ if (details.relatedEvents.length > 0) {
6497
+ fields.set("events", {
6498
+ key: "events",
6499
+ label: "Events",
6500
+ value: details.relatedEvents.map((event) => event.event.label || event.event.id).join(", ")
6501
+ });
6502
+ }
5515
6503
  if (placement?.mode === "at") {
5516
6504
  fields.set("placement", {
5517
6505
  key: "placement",
@@ -5616,6 +6604,7 @@
5616
6604
  padding: options.padding,
5617
6605
  preset: options.preset,
5618
6606
  projection: options.projection,
6607
+ camera: options.camera ? { ...options.camera } : null,
5619
6608
  scaleModel: options.scaleModel ? { ...options.scaleModel } : void 0,
5620
6609
  theme: options.theme,
5621
6610
  layers: options.layers,
@@ -5904,6 +6893,11 @@
5904
6893
  if (currentInput.kind !== "scene" && viewpoint.projection !== scene.projection) {
5905
6894
  nextRenderOptions.projection = viewpoint.projection;
5906
6895
  }
6896
+ if (viewpoint.camera) {
6897
+ nextRenderOptions.camera = { ...viewpoint.camera };
6898
+ } else if (renderOptions.camera) {
6899
+ nextRenderOptions.camera = null;
6900
+ }
5907
6901
  if (viewpointLayers) {
5908
6902
  nextRenderOptions.layers = viewpointLayers;
5909
6903
  }
@@ -5926,6 +6920,12 @@
5926
6920
  emitAtlasStateChange();
5927
6921
  return true;
5928
6922
  },
6923
+ getActiveEventId() {
6924
+ return renderOptions.activeEventId ?? null;
6925
+ },
6926
+ setActiveEvent(id) {
6927
+ api.setRenderOptions({ activeEventId: id });
6928
+ },
5929
6929
  search(query, limit = 12) {
5930
6930
  return searchSceneObjects(scene, query, limit);
5931
6931
  },
@@ -6190,6 +7190,7 @@
6190
7190
  orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
6191
7191
  relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden && (orbit.objectId === renderObject.objectId || renderObject.ancestorIds.includes(orbit.objectId) || renderObject.childIds.includes(orbit.objectId))),
6192
7192
  relations: scene.relations.filter((relation) => !relation.hidden && (relation.fromObjectId === renderObject.objectId || relation.toObjectId === renderObject.objectId)),
7193
+ relatedEvents: scene.events.filter((event) => !event.hidden && (event.targetObjectId === renderObject.objectId || event.objectIds.includes(renderObject.objectId))),
6193
7194
  parent: getObjectById(renderObject.parentId),
6194
7195
  children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
6195
7196
  ancestors: renderObject.ancestorIds.map((ancestorId) => getObjectById(ancestorId)).filter(Boolean),
@@ -6503,16 +7504,19 @@
6503
7504
  function cloneRenderOptions(renderOptions) {
6504
7505
  return {
6505
7506
  ...renderOptions,
7507
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
6506
7508
  filter: renderOptions.filter ? { ...renderOptions.filter } : void 0,
6507
7509
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
6508
7510
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
6509
- theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme
7511
+ theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme,
7512
+ activeEventId: renderOptions.activeEventId ?? null
6510
7513
  };
6511
7514
  }
6512
7515
  function mergeRenderOptions(current, next) {
6513
7516
  return {
6514
7517
  ...current,
6515
7518
  ...next,
7519
+ camera: next.camera !== void 0 ? next.camera ? { ...next.camera } : null : current.camera ? { ...current.camera } : null,
6516
7520
  filter: next.filter !== void 0 ? normalizeViewerFilter(next.filter) : current.filter ? { ...current.filter } : void 0,
6517
7521
  scaleModel: next.scaleModel ? {
6518
7522
  ...current.scaleModel ?? {},
@@ -6526,7 +7530,7 @@
6526
7530
  };
6527
7531
  }
6528
7532
  function hasSceneAffectingRenderOptions(options) {
6529
- return options.width !== void 0 || options.height !== void 0 || options.padding !== void 0 || options.preset !== void 0 || options.projection !== void 0 || options.scaleModel !== void 0;
7533
+ return options.width !== void 0 || options.height !== void 0 || options.padding !== void 0 || options.preset !== void 0 || options.projection !== void 0 || options.camera !== void 0 || options.scaleModel !== void 0 || options.activeEventId !== void 0;
6530
7534
  }
6531
7535
  function resolveSourceRenderOptions2(loaded, renderOptions) {
6532
7536
  const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
@@ -7123,6 +8127,8 @@
7123
8127
  }
7124
8128
  function buildInspectorSnapshot() {
7125
8129
  const activeViewer = requireViewer();
8130
+ const scene = activeViewer.getScene();
8131
+ const camera = scene.camera;
7126
8132
  return {
7127
8133
  selection: activeViewer.getSelectionDetails(),
7128
8134
  activeViewpoint: activeViewer.getActiveViewpoint(),
@@ -7130,13 +8136,21 @@
7130
8136
  atlasState: activeViewer.getAtlasState(),
7131
8137
  visibleObjectIds: activeViewer.getVisibleObjects().map((object) => object.objectId),
7132
8138
  scene: {
7133
- title: activeViewer.getScene().title,
7134
- projection: activeViewer.getScene().projection,
7135
- renderPreset: activeViewer.getScene().renderPreset,
7136
- groupCount: activeViewer.getScene().groups.length,
7137
- semanticGroupCount: activeViewer.getScene().semanticGroups.length,
7138
- relationCount: activeViewer.getScene().relations.length,
7139
- viewpointCount: activeViewer.getScene().viewpoints.length
8139
+ title: scene.title,
8140
+ projection: scene.projection,
8141
+ renderProjection: scene.renderProjection,
8142
+ camera: camera ? {
8143
+ azimuth: camera.azimuth,
8144
+ elevation: camera.elevation,
8145
+ roll: camera.roll,
8146
+ distance: camera.distance
8147
+ } : null,
8148
+ renderPreset: scene.renderPreset,
8149
+ groupCount: scene.groups.length,
8150
+ semanticGroupCount: scene.semanticGroups.length,
8151
+ relationCount: scene.relations.length,
8152
+ eventCount: scene.events.length,
8153
+ viewpointCount: scene.viewpoints.length
7140
8154
  }
7141
8155
  };
7142
8156
  }