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
@@ -61,6 +61,7 @@ var WorldOrbit = (() => {
61
61
  background: true,
62
62
  guides: true,
63
63
  relations: true,
64
+ events: true,
64
65
  orbits: true,
65
66
  objects: true,
66
67
  labels: true,
@@ -210,14 +211,17 @@ var WorldOrbit = (() => {
210
211
  }
211
212
  function createAtlasStateSnapshot(viewerState, renderOptions, filter, viewpointId) {
212
213
  return {
213
- version: "2.0",
214
+ version: "2.5",
214
215
  viewpointId,
216
+ activeEventId: renderOptions.activeEventId ?? null,
215
217
  viewerState: { ...viewerState },
216
218
  renderOptions: {
217
219
  preset: renderOptions.preset,
218
220
  projection: renderOptions.projection,
221
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
219
222
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
220
- scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0
223
+ scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
224
+ activeEventId: renderOptions.activeEventId ?? null
221
225
  },
222
226
  filter: normalizeViewerFilter(filter)
223
227
  };
@@ -228,8 +232,9 @@ var WorldOrbit = (() => {
228
232
  function deserializeViewerAtlasState(serialized) {
229
233
  const raw = JSON.parse(decodeURIComponent(serialized));
230
234
  return {
231
- version: "2.0",
235
+ version: raw.version === "2.0" ? "2.0" : "2.5",
232
236
  viewpointId: raw.viewpointId ?? null,
237
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
233
238
  viewerState: {
234
239
  scale: raw.viewerState?.scale ?? 1,
235
240
  rotationDeg: raw.viewerState?.rotationDeg ?? 0,
@@ -240,8 +245,10 @@ var WorldOrbit = (() => {
240
245
  renderOptions: {
241
246
  preset: raw.renderOptions?.preset,
242
247
  projection: raw.renderOptions?.projection,
248
+ camera: raw.renderOptions?.camera ? { ...raw.renderOptions.camera } : null,
243
249
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : void 0,
244
- scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0
250
+ scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0,
251
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null
245
252
  },
246
253
  filter: normalizeViewerFilter(raw.filter ?? null)
247
254
  };
@@ -256,8 +263,10 @@ var WorldOrbit = (() => {
256
263
  viewerState: { ...atlasState.viewerState },
257
264
  renderOptions: {
258
265
  ...atlasState.renderOptions,
266
+ camera: atlasState.renderOptions.camera ? { ...atlasState.renderOptions.camera } : null,
259
267
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : void 0,
260
- scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0
268
+ scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0,
269
+ activeEventId: atlasState.renderOptions.activeEventId ?? null
261
270
  },
262
271
  filter: atlasState.filter ? { ...atlasState.filter } : null
263
272
  }
@@ -275,6 +284,7 @@ var WorldOrbit = (() => {
275
284
  background: viewpoint.layers.background,
276
285
  guides: viewpoint.layers.guides,
277
286
  relations: viewpoint.layers.relations,
287
+ events: viewpoint.layers.events,
278
288
  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,
279
289
  objects: viewpoint.layers.objects,
280
290
  labels: viewpoint.layers.labels,
@@ -922,6 +932,7 @@ var WorldOrbit = (() => {
922
932
  system,
923
933
  groups: [],
924
934
  relations: [],
935
+ events: [],
925
936
  objects
926
937
  };
927
938
  }
@@ -1301,12 +1312,16 @@ var WorldOrbit = (() => {
1301
1312
  const height = frame.height;
1302
1313
  const padding = frame.padding;
1303
1314
  const layoutPreset = resolveLayoutPreset(document2);
1304
- const projection = resolveProjection(document2, options.projection);
1315
+ const schemaProjection = resolveProjection(document2, options.projection);
1316
+ const camera = normalizeViewCamera(options.camera ?? null);
1317
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
1305
1318
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1306
1319
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1307
1320
  const systemId = document2.system?.id ?? null;
1308
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
1309
- const relationships = buildSceneRelationships(document2.objects, objectMap);
1321
+ const activeEventId = options.activeEventId ?? null;
1322
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
1323
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
1324
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
1310
1325
  const positions = /* @__PURE__ */ new Map();
1311
1326
  const orbitDrafts = [];
1312
1327
  const leaderDrafts = [];
@@ -1315,7 +1330,7 @@ var WorldOrbit = (() => {
1315
1330
  const atObjects = [];
1316
1331
  const surfaceChildren = /* @__PURE__ */ new Map();
1317
1332
  const orbitChildren = /* @__PURE__ */ new Map();
1318
- for (const object of document2.objects) {
1333
+ for (const object of effectiveObjects) {
1319
1334
  const placement = object.placement;
1320
1335
  if (!placement) {
1321
1336
  rootObjects.push(object);
@@ -1342,7 +1357,7 @@ var WorldOrbit = (() => {
1342
1357
  surfaceChildren,
1343
1358
  objectMap,
1344
1359
  spacingFactor,
1345
- projection,
1360
+ projection: renderProjection,
1346
1361
  scaleModel
1347
1362
  };
1348
1363
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -1354,7 +1369,7 @@ var WorldOrbit = (() => {
1354
1369
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1355
1370
  secondaryRoots.forEach((object, index) => {
1356
1371
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
1357
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
1372
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
1358
1373
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
1359
1374
  });
1360
1375
  }
@@ -1410,38 +1425,48 @@ var WorldOrbit = (() => {
1410
1425
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1411
1426
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1412
1427
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1413
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1428
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1414
1429
  const relations = createSceneRelations(document2, objects);
1415
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1416
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1430
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1431
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1432
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1417
1433
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1418
- const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1419
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1434
+ const viewpoints = createSceneViewpoints(document2, schemaProjection, frame.preset, relationships, objectMap);
1435
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1420
1436
  return {
1421
1437
  width,
1422
1438
  height,
1423
1439
  padding,
1424
1440
  renderPreset: frame.preset,
1425
- projection,
1441
+ projection: schemaProjection,
1442
+ renderProjection,
1443
+ camera,
1426
1444
  scaleModel,
1427
1445
  title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1428
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1446
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1429
1447
  systemId,
1430
- viewMode: projection,
1448
+ viewMode: schemaProjection,
1431
1449
  layoutPreset,
1432
1450
  metadata: {
1433
1451
  format: document2.format,
1434
1452
  version: document2.version,
1435
- view: projection,
1453
+ view: schemaProjection,
1454
+ renderProjection,
1436
1455
  scale: String(document2.system?.properties.scale ?? layoutPreset),
1437
1456
  units: String(document2.system?.properties.units ?? "mixed"),
1438
- preset: frame.preset ?? "custom"
1457
+ preset: frame.preset ?? "custom",
1458
+ ...camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {},
1459
+ ...camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {},
1460
+ ...camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {},
1461
+ ...camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}
1439
1462
  },
1440
1463
  contentBounds,
1441
1464
  layers,
1442
1465
  groups,
1443
1466
  semanticGroups,
1444
1467
  viewpoints,
1468
+ events,
1469
+ activeEventId,
1445
1470
  objects,
1446
1471
  orbitVisuals,
1447
1472
  relations,
@@ -1460,6 +1485,56 @@ var WorldOrbit = (() => {
1460
1485
  y: center.y + dx * sin + dy * cos
1461
1486
  };
1462
1487
  }
1488
+ function createEffectiveObjects(objects, events, activeEventId) {
1489
+ const cloned = objects.map((object) => structuredClone(object));
1490
+ if (!activeEventId) {
1491
+ return cloned;
1492
+ }
1493
+ const activeEvent = events.find((event) => event.id === activeEventId);
1494
+ if (!activeEvent) {
1495
+ return cloned;
1496
+ }
1497
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1498
+ const referencedIds = /* @__PURE__ */ new Set([
1499
+ ...activeEvent.targetObjectId ? [activeEvent.targetObjectId] : [],
1500
+ ...activeEvent.participantObjectIds,
1501
+ ...activeEvent.positions.map((pose) => pose.objectId)
1502
+ ]);
1503
+ for (const objectId of referencedIds) {
1504
+ const object = objectMap.get(objectId);
1505
+ if (!object) {
1506
+ continue;
1507
+ }
1508
+ if (activeEvent.epoch) {
1509
+ object.epoch = activeEvent.epoch;
1510
+ }
1511
+ if (activeEvent.referencePlane) {
1512
+ object.referencePlane = activeEvent.referencePlane;
1513
+ }
1514
+ }
1515
+ for (const pose of activeEvent.positions) {
1516
+ const object = objectMap.get(pose.objectId);
1517
+ if (!object) {
1518
+ continue;
1519
+ }
1520
+ if (pose.placement) {
1521
+ object.placement = structuredClone(pose.placement);
1522
+ }
1523
+ if (pose.inner) {
1524
+ object.properties.inner = { ...pose.inner };
1525
+ }
1526
+ if (pose.outer) {
1527
+ object.properties.outer = { ...pose.outer };
1528
+ }
1529
+ if (pose.epoch) {
1530
+ object.epoch = pose.epoch;
1531
+ }
1532
+ if (pose.referencePlane) {
1533
+ object.referencePlane = pose.referencePlane;
1534
+ }
1535
+ }
1536
+ return cloned;
1537
+ }
1463
1538
  function resolveLayoutPreset(document2) {
1464
1539
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1465
1540
  switch (rawScale) {
@@ -1496,10 +1571,59 @@ var WorldOrbit = (() => {
1496
1571
  }
1497
1572
  }
1498
1573
  function resolveProjection(document2, projection) {
1499
- if (projection === "topdown" || projection === "isometric") {
1574
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1500
1575
  return projection;
1501
1576
  }
1502
- return String(document2.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1577
+ const documentView = String(document2.system?.properties.view ?? "topdown").toLowerCase();
1578
+ return parseViewProjection(documentView) ?? "topdown";
1579
+ }
1580
+ function resolveRenderProjection(projection, camera) {
1581
+ switch (projection) {
1582
+ case "topdown":
1583
+ return "topdown";
1584
+ case "isometric":
1585
+ return "isometric";
1586
+ case "orthographic":
1587
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null) ? "isometric" : "topdown";
1588
+ case "perspective":
1589
+ return "isometric";
1590
+ }
1591
+ }
1592
+ function normalizeViewCamera(camera) {
1593
+ if (!camera) {
1594
+ return null;
1595
+ }
1596
+ const normalized = {
1597
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
1598
+ elevation: normalizeFiniteCameraValue(camera.elevation),
1599
+ roll: normalizeFiniteCameraValue(camera.roll),
1600
+ distance: normalizePositiveCameraDistance(camera.distance)
1601
+ };
1602
+ return normalized.azimuth !== null || normalized.elevation !== null || normalized.roll !== null || normalized.distance !== null ? normalized : null;
1603
+ }
1604
+ function normalizeFiniteCameraValue(value) {
1605
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1606
+ }
1607
+ function normalizePositiveCameraDistance(value) {
1608
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1609
+ }
1610
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
1611
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
1612
+ if (projection !== renderProjection) {
1613
+ parts.push(`2D ${renderProjection} fallback`);
1614
+ }
1615
+ if (camera) {
1616
+ const cameraParts = [
1617
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
1618
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
1619
+ camera.roll !== null ? `roll ${camera.roll}` : null,
1620
+ camera.distance !== null ? `dist ${camera.distance}` : null
1621
+ ].filter(Boolean);
1622
+ if (cameraParts.length > 0) {
1623
+ parts.push(`camera ${cameraParts.join(" / ")}`);
1624
+ }
1625
+ }
1626
+ return parts.join(" - ");
1503
1627
  }
1504
1628
  function resolveScaleModel(layoutPreset, overrides) {
1505
1629
  const defaults = defaultScaleModel(layoutPreset);
@@ -1615,24 +1739,14 @@ var WorldOrbit = (() => {
1615
1739
  hidden: draft.object.properties.hidden === true
1616
1740
  };
1617
1741
  }
1618
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1742
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1619
1743
  const labels = [];
1620
1744
  const occupied = [];
1621
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1745
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1746
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1622
1747
  for (const object of visibleObjects) {
1623
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1624
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1625
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1626
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1627
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1628
- let attempts = 0;
1629
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1630
- labelY += direction * 14 * labelMultiplier;
1631
- secondaryY += direction * 14 * labelMultiplier;
1632
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1633
- attempts += 1;
1634
- }
1635
- occupied.push(bounds);
1748
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1749
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1636
1750
  labels.push({
1637
1751
  renderId: `${object.renderId}-label`,
1638
1752
  objectId: object.objectId,
@@ -1641,17 +1755,128 @@ var WorldOrbit = (() => {
1641
1755
  semanticGroupIds: [...object.semanticGroupIds],
1642
1756
  label: object.label,
1643
1757
  secondaryLabel: object.secondaryLabel,
1644
- x: object.x,
1645
- y: labelY,
1646
- secondaryY,
1647
- textAnchor: "middle",
1648
- direction: direction < 0 ? "above" : "below",
1758
+ x: placement.x,
1759
+ y: placement.labelY,
1760
+ secondaryY: placement.secondaryY,
1761
+ textAnchor: placement.textAnchor,
1762
+ direction: placement.direction,
1649
1763
  hidden: object.hidden
1650
1764
  });
1651
1765
  }
1652
1766
  return labels;
1653
1767
  }
1654
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1768
+ function compareLabelPlacementOrder(left, right) {
1769
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1770
+ if (priorityDiff !== 0) {
1771
+ return priorityDiff;
1772
+ }
1773
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1774
+ if (renderPriorityDiff !== 0) {
1775
+ return renderPriorityDiff;
1776
+ }
1777
+ return left.sortKey - right.sortKey;
1778
+ }
1779
+ function labelPlacementPriority(object) {
1780
+ switch (object.object.type) {
1781
+ case "star":
1782
+ return 0;
1783
+ case "planet":
1784
+ return 1;
1785
+ case "moon":
1786
+ return 2;
1787
+ case "belt":
1788
+ case "ring":
1789
+ return 3;
1790
+ case "asteroid":
1791
+ case "comet":
1792
+ return 4;
1793
+ case "structure":
1794
+ case "phenomenon":
1795
+ return 5;
1796
+ }
1797
+ }
1798
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1799
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1800
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1801
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1802
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1803
+ const rect = createLabelRect(object, placement, labelMultiplier);
1804
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1805
+ return placement;
1806
+ }
1807
+ }
1808
+ }
1809
+ return null;
1810
+ }
1811
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1812
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1813
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1814
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1815
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1816
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1817
+ 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";
1818
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1819
+ }
1820
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1821
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1822
+ return object.y >= parent.y ? "below" : "above";
1823
+ }
1824
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1825
+ }
1826
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1827
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1828
+ return object.x >= parent.x ? "right" : "left";
1829
+ }
1830
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1831
+ }
1832
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1833
+ const step = 14 * labelMultiplier;
1834
+ switch (direction) {
1835
+ case "above": {
1836
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1837
+ return {
1838
+ x: object.x,
1839
+ labelY,
1840
+ secondaryY: labelY - 16 * labelMultiplier,
1841
+ textAnchor: "middle",
1842
+ direction
1843
+ };
1844
+ }
1845
+ case "below": {
1846
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1847
+ return {
1848
+ x: object.x,
1849
+ labelY,
1850
+ secondaryY: labelY + 16 * labelMultiplier,
1851
+ textAnchor: "middle",
1852
+ direction
1853
+ };
1854
+ }
1855
+ case "left": {
1856
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1857
+ const labelY = object.y - 4 * labelMultiplier;
1858
+ return {
1859
+ x,
1860
+ labelY,
1861
+ secondaryY: labelY + 16 * labelMultiplier,
1862
+ textAnchor: "end",
1863
+ direction
1864
+ };
1865
+ }
1866
+ case "right": {
1867
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1868
+ const labelY = object.y - 4 * labelMultiplier;
1869
+ return {
1870
+ x,
1871
+ labelY,
1872
+ secondaryY: labelY + 16 * labelMultiplier,
1873
+ textAnchor: "start",
1874
+ direction
1875
+ };
1876
+ }
1877
+ }
1878
+ }
1879
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1655
1880
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1656
1881
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1657
1882
  return [
@@ -1666,6 +1891,10 @@ var WorldOrbit = (() => {
1666
1891
  id: "relations",
1667
1892
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1668
1893
  },
1894
+ {
1895
+ id: "events",
1896
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1897
+ },
1669
1898
  {
1670
1899
  id: "objects",
1671
1900
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1677,7 +1906,7 @@ var WorldOrbit = (() => {
1677
1906
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1678
1907
  ];
1679
1908
  }
1680
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1909
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1681
1910
  const groups = /* @__PURE__ */ new Map();
1682
1911
  const ensureGroup = (groupId) => {
1683
1912
  if (!groupId) {
@@ -1726,7 +1955,7 @@ var WorldOrbit = (() => {
1726
1955
  }
1727
1956
  }
1728
1957
  for (const group of groups.values()) {
1729
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1958
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1730
1959
  }
1731
1960
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1732
1961
  }
@@ -1760,6 +1989,29 @@ var WorldOrbit = (() => {
1760
1989
  };
1761
1990
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1762
1991
  }
1992
+ function createSceneEvents(events, objects, activeEventId) {
1993
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1994
+ return events.map((event) => {
1995
+ const objectIds = [.../* @__PURE__ */ new Set([
1996
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1997
+ ...event.participantObjectIds
1998
+ ])];
1999
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
2000
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
2001
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
2002
+ return {
2003
+ renderId: `${createRenderId(event.id)}-event`,
2004
+ eventId: event.id,
2005
+ event,
2006
+ objectIds,
2007
+ participantIds: [...event.participantObjectIds],
2008
+ targetObjectId: event.targetObjectId,
2009
+ x: centroidX,
2010
+ y: centroidY,
2011
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
2012
+ };
2013
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
2014
+ }
1763
2015
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1764
2016
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1765
2017
  const drafts = /* @__PURE__ */ new Map();
@@ -1807,13 +2059,18 @@ var WorldOrbit = (() => {
1807
2059
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1808
2060
  const title = document2.system?.title ?? document2.system?.properties.title;
1809
2061
  const label = title ? `${String(title)} Overview` : "Overview";
2062
+ const camera = normalizeViewCamera(null);
2063
+ const renderProjection = resolveRenderProjection(projection, camera);
1810
2064
  return {
1811
2065
  id: "overview",
1812
2066
  label,
1813
2067
  summary: "Fit the whole system with the current atlas defaults.",
1814
2068
  objectId: null,
1815
2069
  selectedObjectId: null,
2070
+ eventIds: [],
1816
2071
  projection,
2072
+ renderProjection,
2073
+ camera,
1817
2074
  preset,
1818
2075
  rotationDeg: 0,
1819
2076
  scale: null,
@@ -1849,6 +2106,9 @@ var WorldOrbit = (() => {
1849
2106
  draft.select = normalizedValue;
1850
2107
  }
1851
2108
  return;
2109
+ case "events":
2110
+ draft.eventIds = splitListValue(normalizedValue);
2111
+ return;
1852
2112
  case "projection":
1853
2113
  case "view":
1854
2114
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1860,6 +2120,30 @@ var WorldOrbit = (() => {
1860
2120
  case "angle":
1861
2121
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1862
2122
  return;
2123
+ case "camera.azimuth":
2124
+ draft.camera = {
2125
+ ...draft.camera ?? createEmptyViewCamera(),
2126
+ azimuth: parseFiniteNumber(normalizedValue)
2127
+ };
2128
+ return;
2129
+ case "camera.elevation":
2130
+ draft.camera = {
2131
+ ...draft.camera ?? createEmptyViewCamera(),
2132
+ elevation: parseFiniteNumber(normalizedValue)
2133
+ };
2134
+ return;
2135
+ case "camera.roll":
2136
+ draft.camera = {
2137
+ ...draft.camera ?? createEmptyViewCamera(),
2138
+ roll: parseFiniteNumber(normalizedValue)
2139
+ };
2140
+ return;
2141
+ case "camera.distance":
2142
+ draft.camera = {
2143
+ ...draft.camera ?? createEmptyViewCamera(),
2144
+ distance: parsePositiveNumber(normalizedValue)
2145
+ };
2146
+ return;
1863
2147
  case "zoom":
1864
2148
  case "scale":
1865
2149
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -1899,13 +2183,19 @@ var WorldOrbit = (() => {
1899
2183
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1900
2184
  const filter = normalizeViewpointFilter(draft.filter);
1901
2185
  const label = draft.label?.trim() || humanizeIdentifier(draft.id);
2186
+ const resolvedProjection = draft.projection ?? projection;
2187
+ const camera = normalizeViewCamera(draft.camera ?? null);
2188
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
1902
2189
  return {
1903
2190
  id: draft.id,
1904
2191
  label,
1905
2192
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1906
2193
  objectId,
1907
2194
  selectedObjectId,
1908
- projection: draft.projection ?? projection,
2195
+ eventIds: [...new Set(draft.eventIds ?? [])],
2196
+ projection: resolvedProjection,
2197
+ renderProjection,
2198
+ camera,
1909
2199
  preset: draft.preset ?? preset,
1910
2200
  rotationDeg: draft.rotationDeg ?? 0,
1911
2201
  scale: draft.scale ?? null,
@@ -1922,6 +2212,14 @@ var WorldOrbit = (() => {
1922
2212
  groupIds: []
1923
2213
  };
1924
2214
  }
2215
+ function createEmptyViewCamera() {
2216
+ return {
2217
+ azimuth: null,
2218
+ elevation: null,
2219
+ roll: null,
2220
+ distance: null
2221
+ };
2222
+ }
1925
2223
  function normalizeViewpointFilter(filter) {
1926
2224
  if (!filter) {
1927
2225
  return null;
@@ -1935,7 +2233,18 @@ var WorldOrbit = (() => {
1935
2233
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1936
2234
  }
1937
2235
  function parseViewProjection(value) {
1938
- return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
2236
+ switch (value.toLowerCase()) {
2237
+ case "topdown":
2238
+ return "topdown";
2239
+ case "isometric":
2240
+ return "isometric";
2241
+ case "orthographic":
2242
+ return "orthographic";
2243
+ case "perspective":
2244
+ return "perspective";
2245
+ default:
2246
+ return null;
2247
+ }
1939
2248
  }
1940
2249
  function parseRenderPreset(value) {
1941
2250
  const normalized = value.toLowerCase();
@@ -1962,7 +2271,7 @@ var WorldOrbit = (() => {
1962
2271
  next["orbits-front"] = enabled;
1963
2272
  continue;
1964
2273
  }
1965
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2274
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1966
2275
  next[rawLayer] = enabled;
1967
2276
  }
1968
2277
  }
@@ -1973,7 +2282,7 @@ var WorldOrbit = (() => {
1973
2282
  }
1974
2283
  function parseViewpointGroups(value, document2, relationships, objectMap) {
1975
2284
  return splitListValue(value).map((entry) => {
1976
- if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
2285
+ if (document2.schemaVersion === "2.1" || document2.schemaVersion === "2.5" || document2.groups.some((group) => group.id === entry)) {
1977
2286
  return entry;
1978
2287
  }
1979
2288
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -2010,7 +2319,7 @@ var WorldOrbit = (() => {
2010
2319
  }
2011
2320
  return parts.join(" - ");
2012
2321
  }
2013
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
2322
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2014
2323
  let minX = Number.POSITIVE_INFINITY;
2015
2324
  let minY = Number.POSITIVE_INFINITY;
2016
2325
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2040,7 +2349,7 @@ var WorldOrbit = (() => {
2040
2349
  for (const label of labels) {
2041
2350
  if (label.hidden)
2042
2351
  continue;
2043
- includeLabelBounds(label, include);
2352
+ includeLabelBounds(label, include, labelMultiplier);
2044
2353
  }
2045
2354
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
2046
2355
  return createBounds(0, 0, width, height);
@@ -2078,13 +2387,10 @@ var WorldOrbit = (() => {
2078
2387
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
2079
2388
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
2080
2389
  }
2081
- function includeLabelBounds(label, include) {
2082
- const labelScale = 1;
2083
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
2084
- include(label.x - labelHalfWidth, label.y - 18);
2085
- include(label.x + labelHalfWidth, label.y + 8);
2086
- include(label.x - labelHalfWidth, label.secondaryY - 14);
2087
- include(label.x + labelHalfWidth, label.secondaryY + 8);
2390
+ function includeLabelBounds(label, include, labelMultiplier) {
2391
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
2392
+ include(bounds.left, bounds.top);
2393
+ include(bounds.right, bounds.bottom);
2088
2394
  }
2089
2395
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
2090
2396
  if (positions.has(object.id)) {
@@ -2474,7 +2780,7 @@ var WorldOrbit = (() => {
2474
2780
  return null;
2475
2781
  }
2476
2782
  }
2477
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2783
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2478
2784
  let minX = Number.POSITIVE_INFINITY;
2479
2785
  let minY = Number.POSITIVE_INFINITY;
2480
2786
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2503,7 +2809,7 @@ var WorldOrbit = (() => {
2503
2809
  }
2504
2810
  for (const label of labels) {
2505
2811
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2506
- includeLabelBounds(label, include);
2812
+ includeLabelBounds(label, include, labelMultiplier);
2507
2813
  }
2508
2814
  }
2509
2815
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2528,12 +2834,28 @@ var WorldOrbit = (() => {
2528
2834
  }
2529
2835
  return current.id;
2530
2836
  }
2531
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2837
+ function createLabelRect(object, placement, labelMultiplier) {
2838
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2839
+ }
2840
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2841
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2842
+ const labelWidth = labelHalfWidth * 2;
2843
+ const topPadding = direction === "above" ? 18 : 12;
2844
+ const bottomPadding = direction === "above" ? 8 : 12;
2845
+ let left = x - labelHalfWidth;
2846
+ let right = x + labelHalfWidth;
2847
+ if (textAnchor === "start") {
2848
+ left = x;
2849
+ right = x + labelWidth;
2850
+ } else if (textAnchor === "end") {
2851
+ left = x - labelWidth;
2852
+ right = x;
2853
+ }
2532
2854
  return {
2533
- left: x - labelHalfWidth,
2534
- right: x + labelHalfWidth,
2535
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2536
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2855
+ left,
2856
+ right,
2857
+ top: Math.min(labelY, secondaryY) - topPadding,
2858
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2537
2859
  };
2538
2860
  }
2539
2861
  function rectsOverlap(left, right) {
@@ -2720,11 +3042,6 @@ var WorldOrbit = (() => {
2720
3042
  function customColorFor(value) {
2721
3043
  return typeof value === "string" && value.trim() ? value : void 0;
2722
3044
  }
2723
- function estimateLabelHalfWidth(object, labelMultiplier) {
2724
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2725
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2726
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2727
- }
2728
3045
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2729
3046
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2730
3047
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2741,7 +3058,7 @@ var WorldOrbit = (() => {
2741
3058
  }
2742
3059
 
2743
3060
  // packages/core/dist/draft.js
2744
- function materializeAtlasDocument(document2) {
3061
+ function materializeAtlasDocument(document2, options = {}) {
2745
3062
  const system = document2.system ? {
2746
3063
  type: "system",
2747
3064
  id: document2.system.id,
@@ -2752,6 +3069,8 @@ var WorldOrbit = (() => {
2752
3069
  properties: materializeDraftSystemProperties(document2.system),
2753
3070
  info: materializeDraftSystemInfo(document2.system)
2754
3071
  } : null;
3072
+ const objects = document2.objects.map(cloneWorldOrbitObject);
3073
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2755
3074
  return {
2756
3075
  format: "worldorbit",
2757
3076
  version: "1.0",
@@ -2759,7 +3078,8 @@ var WorldOrbit = (() => {
2759
3078
  system,
2760
3079
  groups: structuredClone(document2.groups ?? []),
2761
3080
  relations: structuredClone(document2.relations ?? []),
2762
- objects: document2.objects.map(cloneWorldOrbitObject)
3081
+ events: document2.events.map(cloneWorldOrbitEvent),
3082
+ objects
2763
3083
  };
2764
3084
  }
2765
3085
  function cloneWorldOrbitObject(object) {
@@ -2781,6 +3101,75 @@ var WorldOrbit = (() => {
2781
3101
  info: { ...object.info }
2782
3102
  };
2783
3103
  }
3104
+ function cloneWorldOrbitEvent(event) {
3105
+ return {
3106
+ ...event,
3107
+ participantObjectIds: [...event.participantObjectIds],
3108
+ tags: [...event.tags],
3109
+ positions: event.positions.map(cloneWorldOrbitEventPose)
3110
+ };
3111
+ }
3112
+ function cloneWorldOrbitEventPose(pose) {
3113
+ return {
3114
+ objectId: pose.objectId,
3115
+ placement: clonePlacement(pose.placement),
3116
+ inner: pose.inner ? { ...pose.inner } : void 0,
3117
+ outer: pose.outer ? { ...pose.outer } : void 0,
3118
+ epoch: pose.epoch ?? null,
3119
+ referencePlane: pose.referencePlane ?? null
3120
+ };
3121
+ }
3122
+ function clonePlacement(placement) {
3123
+ return placement ? structuredClone(placement) : null;
3124
+ }
3125
+ function applyEventPoseOverrides(objects, events, activeEventId) {
3126
+ if (!activeEventId) {
3127
+ return;
3128
+ }
3129
+ const event = events.find((entry) => entry.id === activeEventId);
3130
+ if (!event) {
3131
+ return;
3132
+ }
3133
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
3134
+ const referencedIds = /* @__PURE__ */ new Set([
3135
+ ...event.targetObjectId ? [event.targetObjectId] : [],
3136
+ ...event.participantObjectIds,
3137
+ ...event.positions.map((pose) => pose.objectId)
3138
+ ]);
3139
+ for (const objectId of referencedIds) {
3140
+ const object = objectMap.get(objectId);
3141
+ if (!object) {
3142
+ continue;
3143
+ }
3144
+ if (event.epoch) {
3145
+ object.epoch = event.epoch;
3146
+ }
3147
+ if (event.referencePlane) {
3148
+ object.referencePlane = event.referencePlane;
3149
+ }
3150
+ }
3151
+ for (const pose of event.positions) {
3152
+ const object = objectMap.get(pose.objectId);
3153
+ if (!object) {
3154
+ continue;
3155
+ }
3156
+ if (pose.placement) {
3157
+ object.placement = clonePlacement(pose.placement);
3158
+ }
3159
+ if (pose.inner) {
3160
+ object.properties.inner = { ...pose.inner };
3161
+ }
3162
+ if (pose.outer) {
3163
+ object.properties.outer = { ...pose.outer };
3164
+ }
3165
+ if (pose.epoch) {
3166
+ object.epoch = pose.epoch;
3167
+ }
3168
+ if (pose.referencePlane) {
3169
+ object.referencePlane = pose.referencePlane;
3170
+ }
3171
+ }
3172
+ }
2784
3173
  function cloneProperties(properties) {
2785
3174
  const next = {};
2786
3175
  for (const [key, value] of Object.entries(properties)) {
@@ -2853,6 +3242,18 @@ var WorldOrbit = (() => {
2853
3242
  if (viewpoint.rotationDeg !== 0) {
2854
3243
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2855
3244
  }
3245
+ if (viewpoint.camera?.azimuth !== null) {
3246
+ info2[`${prefix}.camera.azimuth`] = String(viewpoint.camera?.azimuth);
3247
+ }
3248
+ if (viewpoint.camera?.elevation !== null) {
3249
+ info2[`${prefix}.camera.elevation`] = String(viewpoint.camera?.elevation);
3250
+ }
3251
+ if (viewpoint.camera?.roll !== null) {
3252
+ info2[`${prefix}.camera.roll`] = String(viewpoint.camera?.roll);
3253
+ }
3254
+ if (viewpoint.camera?.distance !== null) {
3255
+ info2[`${prefix}.camera.distance`] = String(viewpoint.camera?.distance);
3256
+ }
2856
3257
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2857
3258
  if (serializedLayers) {
2858
3259
  info2[`${prefix}.layers`] = serializedLayers;
@@ -2869,6 +3270,9 @@ var WorldOrbit = (() => {
2869
3270
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2870
3271
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2871
3272
  }
3273
+ if (viewpoint.events.length > 0) {
3274
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
3275
+ }
2872
3276
  }
2873
3277
  for (const annotation of system.annotations) {
2874
3278
  const prefix = `annotation.${annotation.id}`;
@@ -2893,7 +3297,7 @@ var WorldOrbit = (() => {
2893
3297
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2894
3298
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2895
3299
  }
2896
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3300
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2897
3301
  if (layers[key] !== void 0) {
2898
3302
  tokens.push(layers[key] ? key : `-${key}`);
2899
3303
  }
@@ -3067,6 +3471,7 @@ var WorldOrbit = (() => {
3067
3471
  const diagnostics = [];
3068
3472
  const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
3069
3473
  const groupIds = new Set(document2.groups.map((group) => group.id));
3474
+ const eventIds = new Set(document2.events.map((event) => event.id));
3070
3475
  if (!document2.system) {
3071
3476
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3072
3477
  }
@@ -3076,6 +3481,7 @@ var WorldOrbit = (() => {
3076
3481
  ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3077
3482
  ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
3078
3483
  ["relation", document2.relations.map((relation) => relation.id)],
3484
+ ["event", document2.events.map((event) => event.id)],
3079
3485
  ["object", document2.objects.map((object) => object.id)]
3080
3486
  ]) {
3081
3487
  for (const id of ids) {
@@ -3091,11 +3497,14 @@ var WorldOrbit = (() => {
3091
3497
  validateRelation(relation, objectMap, diagnostics);
3092
3498
  }
3093
3499
  for (const viewpoint of document2.system?.viewpoints ?? []) {
3094
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3500
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
3095
3501
  }
3096
3502
  for (const object of document2.objects) {
3097
3503
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3098
3504
  }
3505
+ for (const event of document2.events) {
3506
+ validateEvent(event, document2.system, objectMap, diagnostics);
3507
+ }
3099
3508
  return diagnostics;
3100
3509
  }
3101
3510
  function validateRelation(relation, objectMap, diagnostics) {
@@ -3113,15 +3522,24 @@ var WorldOrbit = (() => {
3113
3522
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3114
3523
  }
3115
3524
  }
3116
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3117
- if (!filter || sourceSchemaVersion !== "2.1") {
3118
- return;
3119
- }
3120
- for (const groupId of filter.groupIds) {
3121
- if (!groupIds.has(groupId)) {
3122
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3525
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
3526
+ const filter = viewpoint.filter;
3527
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
3528
+ if (filter) {
3529
+ for (const groupId of filter.groupIds) {
3530
+ if (!groupIds.has(groupId)) {
3531
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
3532
+ }
3533
+ }
3534
+ }
3535
+ for (const eventId of viewpoint.events ?? []) {
3536
+ if (!eventIds.has(eventId)) {
3537
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
3538
+ }
3123
3539
  }
3124
3540
  }
3541
+ validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
3542
+ validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
3125
3543
  }
3126
3544
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
3127
3545
  const placement = object.placement;
@@ -3134,6 +3552,12 @@ var WorldOrbit = (() => {
3134
3552
  }
3135
3553
  }
3136
3554
  }
3555
+ if (typeof object.epoch === "string" && !object.epoch.trim()) {
3556
+ diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
3557
+ }
3558
+ if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
3559
+ diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
3560
+ }
3137
3561
  if (orbitPlacement) {
3138
3562
  if (!objectMap.has(orbitPlacement.target)) {
3139
3563
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -3205,6 +3629,122 @@ var WorldOrbit = (() => {
3205
3629
  }
3206
3630
  }
3207
3631
  }
3632
+ function validateEvent(event, system, objectMap, diagnostics) {
3633
+ const fieldPrefix = `event.${event.id}`;
3634
+ const referencedIds = /* @__PURE__ */ new Set();
3635
+ if (!event.kind.trim()) {
3636
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3637
+ }
3638
+ if (typeof event.epoch === "string" && !event.epoch.trim()) {
3639
+ diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, void 0, `${fieldPrefix}.epoch`));
3640
+ }
3641
+ if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
3642
+ diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, void 0, `${fieldPrefix}.referencePlane`));
3643
+ }
3644
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3645
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3646
+ }
3647
+ if (event.targetObjectId) {
3648
+ referencedIds.add(event.targetObjectId);
3649
+ if (!objectMap.has(event.targetObjectId)) {
3650
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3651
+ }
3652
+ }
3653
+ const seenParticipants = /* @__PURE__ */ new Set();
3654
+ for (const participantId of event.participantObjectIds) {
3655
+ referencedIds.add(participantId);
3656
+ if (seenParticipants.has(participantId)) {
3657
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3658
+ continue;
3659
+ }
3660
+ seenParticipants.add(participantId);
3661
+ if (!objectMap.has(participantId)) {
3662
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3663
+ }
3664
+ }
3665
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3666
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3667
+ }
3668
+ if (event.positions.length === 0) {
3669
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3670
+ }
3671
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3672
+ 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`));
3673
+ }
3674
+ const poseIds = /* @__PURE__ */ new Set();
3675
+ for (const pose of event.positions) {
3676
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3677
+ if (poseIds.has(pose.objectId)) {
3678
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3679
+ continue;
3680
+ }
3681
+ poseIds.add(pose.objectId);
3682
+ const object = objectMap.get(pose.objectId);
3683
+ if (!object) {
3684
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3685
+ continue;
3686
+ }
3687
+ if (!referencedIds.has(pose.objectId)) {
3688
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3689
+ }
3690
+ validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
3691
+ }
3692
+ const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
3693
+ if (event.positions.length > 0 && missingPoseIds.length > 0) {
3694
+ diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, void 0, `${fieldPrefix}.positions`));
3695
+ }
3696
+ }
3697
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
3698
+ const placement = pose.placement;
3699
+ if (!placement) {
3700
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3701
+ return;
3702
+ }
3703
+ if (placement.mode === "orbit") {
3704
+ if (!objectMap.has(placement.target)) {
3705
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3706
+ }
3707
+ if (placement.distance && placement.semiMajor) {
3708
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3709
+ }
3710
+ if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
3711
+ diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, void 0, `${fieldPrefix}.phase`));
3712
+ }
3713
+ if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
3714
+ diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, void 0, `${fieldPrefix}.inclination`));
3715
+ }
3716
+ if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
3717
+ 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`));
3718
+ }
3719
+ return;
3720
+ }
3721
+ if (placement.mode === "surface") {
3722
+ const target = objectMap.get(placement.target);
3723
+ if (!target) {
3724
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3725
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3726
+ 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`));
3727
+ }
3728
+ return;
3729
+ }
3730
+ if (placement.mode === "at") {
3731
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3732
+ 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`));
3733
+ }
3734
+ const reference = placement.reference;
3735
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3736
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3737
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3738
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3739
+ } else if (reference.kind === "lagrange") {
3740
+ if (!objectMap.has(reference.primary)) {
3741
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3742
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3743
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3744
+ }
3745
+ }
3746
+ }
3747
+ }
3208
3748
  function validateAtTarget(object, objectMap, diagnostics) {
3209
3749
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3210
3750
  if (!reference) {
@@ -3310,6 +3850,52 @@ var WorldOrbit = (() => {
3310
3850
  return null;
3311
3851
  }
3312
3852
  }
3853
+ function validateProjection(projection, diagnostics, field, viewpointId) {
3854
+ if (projection !== "topdown" && projection !== "isometric" && projection !== "orthographic" && projection !== "perspective") {
3855
+ diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, void 0, field));
3856
+ }
3857
+ }
3858
+ function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
3859
+ if (!camera) {
3860
+ return;
3861
+ }
3862
+ const prefix = `viewpoint.${viewpointId}.camera`;
3863
+ for (const [key, value] of [
3864
+ ["azimuth", camera.azimuth],
3865
+ ["elevation", camera.elevation],
3866
+ ["roll", camera.roll],
3867
+ ["distance", camera.distance]
3868
+ ]) {
3869
+ if (value !== null && (!Number.isFinite(value) || key === "distance" && value <= 0)) {
3870
+ diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, void 0, `${prefix}.${key}`));
3871
+ }
3872
+ }
3873
+ if (camera.distance !== null && projection !== "perspective") {
3874
+ 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`));
3875
+ }
3876
+ if (projection === "topdown" && (camera.elevation !== null || camera.roll !== null)) {
3877
+ 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));
3878
+ }
3879
+ if (projection === "isometric" && camera.elevation !== null) {
3880
+ diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, void 0, `${prefix}.elevation`));
3881
+ }
3882
+ if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
3883
+ 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`));
3884
+ }
3885
+ const hasAnchor = focusObjectId !== null && objectMap.has(focusObjectId) || selectedObjectId !== null && objectMap.has(selectedObjectId) || !!filter;
3886
+ if (!hasAnchor) {
3887
+ diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, void 0, prefix));
3888
+ }
3889
+ }
3890
+ function resolveEffectiveEpoch(system, object, event, pose) {
3891
+ return normalizeOptionalContextString(pose?.epoch) ?? normalizeOptionalContextString(event?.epoch) ?? normalizeOptionalContextString(object.epoch) ?? normalizeOptionalContextString(system?.epoch) ?? null;
3892
+ }
3893
+ function resolveEffectiveReferencePlane(system, object, event, pose) {
3894
+ return normalizeOptionalContextString(pose?.referencePlane) ?? normalizeOptionalContextString(event?.referencePlane) ?? normalizeOptionalContextString(object.referencePlane) ?? normalizeOptionalContextString(system?.referencePlane) ?? null;
3895
+ }
3896
+ function normalizeOptionalContextString(value) {
3897
+ return typeof value === "string" && value.trim() ? value.trim() : null;
3898
+ }
3313
3899
  function toleranceForField(object, field) {
3314
3900
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3315
3901
  if (typeof tolerance === "number") {
@@ -3405,6 +3991,23 @@ var WorldOrbit = (() => {
3405
3991
  });
3406
3992
  }
3407
3993
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3994
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3995
+ "orbit",
3996
+ "distance",
3997
+ "semiMajor",
3998
+ "eccentricity",
3999
+ "period",
4000
+ "angle",
4001
+ "inclination",
4002
+ "phase",
4003
+ "at",
4004
+ "surface",
4005
+ "free",
4006
+ "inner",
4007
+ "outer",
4008
+ "epoch",
4009
+ "referencePlane"
4010
+ ]);
3408
4011
  function parseWorldOrbitAtlas(source) {
3409
4012
  return parseAtlasSource(source);
3410
4013
  }
@@ -3419,12 +4022,15 @@ var WorldOrbit = (() => {
3419
4022
  const objectNodes = [];
3420
4023
  const groups = [];
3421
4024
  const relations = [];
4025
+ const events = [];
4026
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3422
4027
  let sawDefaults = false;
3423
4028
  let sawAtlas = false;
3424
4029
  const viewpointIds = /* @__PURE__ */ new Set();
3425
4030
  const annotationIds = /* @__PURE__ */ new Set();
3426
4031
  const groupIds = /* @__PURE__ */ new Set();
3427
4032
  const relationIds = /* @__PURE__ */ new Set();
4033
+ const eventIds = /* @__PURE__ */ new Set();
3428
4034
  for (let index = 0; index < lines.length; index++) {
3429
4035
  const rawLine = lines[index];
3430
4036
  const lineNumber = index + 1;
@@ -3442,7 +4048,7 @@ var WorldOrbit = (() => {
3442
4048
  if (!sawSchemaHeader) {
3443
4049
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3444
4050
  sawSchemaHeader = true;
3445
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
4051
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3446
4052
  diagnostics.push({
3447
4053
  code: "parse.schema21.commentCompatibility",
3448
4054
  severity: "warning",
@@ -3455,7 +4061,7 @@ var WorldOrbit = (() => {
3455
4061
  continue;
3456
4062
  }
3457
4063
  if (indent === 0) {
3458
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
4064
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3459
4065
  if (section.kind === "system") {
3460
4066
  system = section.system;
3461
4067
  } else if (section.kind === "defaults") {
@@ -3474,6 +4080,7 @@ var WorldOrbit = (() => {
3474
4080
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3475
4081
  }
3476
4082
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4083
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3477
4084
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3478
4085
  const baseDocument = {
3479
4086
  format: "worldorbit",
@@ -3481,6 +4088,7 @@ var WorldOrbit = (() => {
3481
4088
  system,
3482
4089
  groups,
3483
4090
  relations,
4091
+ events: normalizedEvents,
3484
4092
  objects,
3485
4093
  diagnostics
3486
4094
  };
@@ -3510,13 +4118,13 @@ var WorldOrbit = (() => {
3510
4118
  return document2;
3511
4119
  }
3512
4120
  function assertDraftSchemaHeader(tokens, line) {
3513
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3514
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4121
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
4122
+ 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);
3515
4123
  }
3516
4124
  const version = tokens[1].value.toLowerCase();
3517
- return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
4125
+ return version === "2.5" ? "2.5" : version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3518
4126
  }
3519
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
4127
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3520
4128
  const keyword = tokens[0]?.value.toLowerCase();
3521
4129
  switch (keyword) {
3522
4130
  case "system":
@@ -3534,6 +4142,8 @@ var WorldOrbit = (() => {
3534
4142
  return {
3535
4143
  kind: "defaults",
3536
4144
  system,
4145
+ sourceSchemaVersion,
4146
+ diagnostics,
3537
4147
  seenFields: /* @__PURE__ */ new Set()
3538
4148
  };
3539
4149
  case "atlas":
@@ -3553,7 +4163,7 @@ var WorldOrbit = (() => {
3553
4163
  if (!system) {
3554
4164
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3555
4165
  }
3556
- return startViewpointSection(tokens, line, system, viewpointIds);
4166
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3557
4167
  case "annotation":
3558
4168
  if (!system) {
3559
4169
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3565,6 +4175,9 @@ var WorldOrbit = (() => {
3565
4175
  case "relation":
3566
4176
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3567
4177
  return startRelationSection(tokens, line, relations, relationIds);
4178
+ case "event":
4179
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4180
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3568
4181
  case "object":
3569
4182
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3570
4183
  default:
@@ -3601,7 +4214,7 @@ var WorldOrbit = (() => {
3601
4214
  seenFields: /* @__PURE__ */ new Set()
3602
4215
  };
3603
4216
  }
3604
- function startViewpointSection(tokens, line, system, viewpointIds) {
4217
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3605
4218
  if (tokens.length !== 2) {
3606
4219
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3607
4220
  }
@@ -3618,10 +4231,12 @@ var WorldOrbit = (() => {
3618
4231
  summary: "",
3619
4232
  focusObjectId: null,
3620
4233
  selectedObjectId: null,
4234
+ events: [],
3621
4235
  projection: system.defaults.view,
3622
4236
  preset: system.defaults.preset,
3623
4237
  zoom: null,
3624
4238
  rotationDeg: 0,
4239
+ camera: null,
3625
4240
  layers: {},
3626
4241
  filter: null
3627
4242
  };
@@ -3630,10 +4245,15 @@ var WorldOrbit = (() => {
3630
4245
  return {
3631
4246
  kind: "viewpoint",
3632
4247
  viewpoint,
4248
+ sourceSchemaVersion,
4249
+ diagnostics,
3633
4250
  seenFields: /* @__PURE__ */ new Set(),
3634
4251
  inFilter: false,
3635
4252
  filterIndent: null,
3636
- seenFilterFields: /* @__PURE__ */ new Set()
4253
+ seenFilterFields: /* @__PURE__ */ new Set(),
4254
+ inCamera: false,
4255
+ cameraIndent: null,
4256
+ seenCameraFields: /* @__PURE__ */ new Set()
3637
4257
  };
3638
4258
  }
3639
4259
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -3720,6 +4340,51 @@ var WorldOrbit = (() => {
3720
4340
  seenFields: /* @__PURE__ */ new Set()
3721
4341
  };
3722
4342
  }
4343
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4344
+ if (tokens.length !== 2) {
4345
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4346
+ }
4347
+ const id = normalizeIdentifier(tokens[1].value);
4348
+ if (!id) {
4349
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4350
+ }
4351
+ if (eventIds.has(id)) {
4352
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4353
+ }
4354
+ const event = {
4355
+ id,
4356
+ kind: "",
4357
+ label: humanizeIdentifier2(id),
4358
+ summary: null,
4359
+ targetObjectId: null,
4360
+ participantObjectIds: [],
4361
+ timing: null,
4362
+ visibility: null,
4363
+ epoch: null,
4364
+ referencePlane: null,
4365
+ tags: [],
4366
+ color: null,
4367
+ hidden: false,
4368
+ positions: []
4369
+ };
4370
+ const rawPoses = [];
4371
+ events.push(event);
4372
+ eventPoseNodes.set(id, rawPoses);
4373
+ eventIds.add(id);
4374
+ return {
4375
+ kind: "event",
4376
+ event,
4377
+ sourceSchemaVersion,
4378
+ diagnostics,
4379
+ seenFields: /* @__PURE__ */ new Set(),
4380
+ rawPoses,
4381
+ inPositions: false,
4382
+ positionsIndent: null,
4383
+ activePose: null,
4384
+ poseIndent: null,
4385
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4386
+ };
4387
+ }
3723
4388
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3724
4389
  if (tokens.length < 3) {
3725
4390
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -3776,6 +4441,9 @@ var WorldOrbit = (() => {
3776
4441
  case "relation":
3777
4442
  applyRelationField(section, tokens, line);
3778
4443
  return;
4444
+ case "event":
4445
+ applyEventField(section, indent, tokens, line);
4446
+ return;
3779
4447
  case "object":
3780
4448
  applyObjectField(section, indent, tokens, line);
3781
4449
  return;
@@ -3818,6 +4486,12 @@ var WorldOrbit = (() => {
3818
4486
  const value = joinFieldValue(tokens, line);
3819
4487
  switch (key) {
3820
4488
  case "view":
4489
+ if (isSchema25Projection(value)) {
4490
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
4491
+ line,
4492
+ column: tokens[0].column
4493
+ });
4494
+ }
3821
4495
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
3822
4496
  return;
3823
4497
  case "scale":
@@ -3857,14 +4531,36 @@ var WorldOrbit = (() => {
3857
4531
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
3858
4532
  }
3859
4533
  function applyViewpointField2(section, indent, tokens, line) {
4534
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
4535
+ section.inCamera = false;
4536
+ section.cameraIndent = null;
4537
+ }
3860
4538
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
3861
4539
  section.inFilter = false;
3862
4540
  section.filterIndent = null;
3863
4541
  }
4542
+ if (section.inCamera) {
4543
+ applyViewpointCameraField(section, tokens, line);
4544
+ return;
4545
+ }
3864
4546
  if (section.inFilter) {
3865
4547
  applyViewpointFilterField(section, tokens, line);
3866
4548
  return;
3867
4549
  }
4550
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
4551
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4552
+ line,
4553
+ column: tokens[0].column
4554
+ });
4555
+ if (section.seenFields.has("camera")) {
4556
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
4557
+ }
4558
+ section.seenFields.add("camera");
4559
+ section.inCamera = true;
4560
+ section.cameraIndent = indent;
4561
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4562
+ return;
4563
+ }
3868
4564
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
3869
4565
  if (section.seenFields.has("filter")) {
3870
4566
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -3890,6 +4586,12 @@ var WorldOrbit = (() => {
3890
4586
  section.viewpoint.selectedObjectId = value;
3891
4587
  return;
3892
4588
  case "projection":
4589
+ if (isSchema25Projection(value)) {
4590
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
4591
+ line,
4592
+ column: tokens[0].column
4593
+ });
4594
+ }
3893
4595
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
3894
4596
  return;
3895
4597
  case "preset":
@@ -3901,13 +4603,49 @@ var WorldOrbit = (() => {
3901
4603
  case "rotation":
3902
4604
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3903
4605
  return;
4606
+ case "camera":
4607
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4608
+ line,
4609
+ column: tokens[0].column
4610
+ });
4611
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
4612
+ return;
3904
4613
  case "layers":
3905
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4614
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4615
+ return;
4616
+ case "events":
4617
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4618
+ line,
4619
+ column: tokens[0].column
4620
+ });
4621
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3906
4622
  return;
3907
4623
  default:
3908
4624
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
3909
4625
  }
3910
4626
  }
4627
+ function applyViewpointCameraField(section, tokens, line) {
4628
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
4629
+ const value = joinFieldValue(tokens, line);
4630
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4631
+ switch (key) {
4632
+ case "azimuth":
4633
+ camera.azimuth = parseFiniteNumber2(value, line, tokens[0].column, "camera.azimuth");
4634
+ break;
4635
+ case "elevation":
4636
+ camera.elevation = parseFiniteNumber2(value, line, tokens[0].column, "camera.elevation");
4637
+ break;
4638
+ case "roll":
4639
+ camera.roll = parseFiniteNumber2(value, line, tokens[0].column, "camera.roll");
4640
+ break;
4641
+ case "distance":
4642
+ camera.distance = parsePositiveNumber2(value, line, tokens[0].column, "camera.distance");
4643
+ break;
4644
+ default:
4645
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
4646
+ }
4647
+ section.viewpoint.camera = camera;
4648
+ }
3911
4649
  function applyViewpointFilterField(section, tokens, line) {
3912
4650
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
3913
4651
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -4007,6 +4745,126 @@ var WorldOrbit = (() => {
4007
4745
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4008
4746
  }
4009
4747
  }
4748
+ function applyEventField(section, indent, tokens, line) {
4749
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4750
+ section.activePose = null;
4751
+ section.poseIndent = null;
4752
+ section.activePoseSeenFields.clear();
4753
+ }
4754
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4755
+ section.inPositions = false;
4756
+ section.positionsIndent = null;
4757
+ }
4758
+ if (section.activePose) {
4759
+ if (tokens[0]?.value === "epoch" || tokens[0]?.value === "referencePlane") {
4760
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
4761
+ line,
4762
+ column: tokens[0]?.column ?? 1
4763
+ });
4764
+ }
4765
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4766
+ return;
4767
+ }
4768
+ if (section.inPositions) {
4769
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4770
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4771
+ }
4772
+ const objectId = tokens[1].value;
4773
+ if (!objectId.trim()) {
4774
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4775
+ }
4776
+ const rawPose = {
4777
+ objectId,
4778
+ fields: [],
4779
+ location: { line, column: tokens[0].column }
4780
+ };
4781
+ section.rawPoses.push(rawPose);
4782
+ section.activePose = rawPose;
4783
+ section.poseIndent = indent;
4784
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4785
+ return;
4786
+ }
4787
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4788
+ if (section.seenFields.has("positions")) {
4789
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4790
+ }
4791
+ section.seenFields.add("positions");
4792
+ section.inPositions = true;
4793
+ section.positionsIndent = indent;
4794
+ return;
4795
+ }
4796
+ const key = requireUniqueField(tokens, section.seenFields, line);
4797
+ switch (key) {
4798
+ case "kind":
4799
+ section.event.kind = joinFieldValue(tokens, line);
4800
+ return;
4801
+ case "label":
4802
+ section.event.label = joinFieldValue(tokens, line);
4803
+ return;
4804
+ case "summary":
4805
+ section.event.summary = joinFieldValue(tokens, line);
4806
+ return;
4807
+ case "target":
4808
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4809
+ return;
4810
+ case "participants":
4811
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4812
+ return;
4813
+ case "timing":
4814
+ section.event.timing = joinFieldValue(tokens, line);
4815
+ return;
4816
+ case "visibility":
4817
+ section.event.visibility = joinFieldValue(tokens, line);
4818
+ return;
4819
+ case "epoch":
4820
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
4821
+ line,
4822
+ column: tokens[0].column
4823
+ });
4824
+ section.event.epoch = joinFieldValue(tokens, line);
4825
+ return;
4826
+ case "referenceplane":
4827
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
4828
+ line,
4829
+ column: tokens[0].column
4830
+ });
4831
+ section.event.referencePlane = joinFieldValue(tokens, line);
4832
+ return;
4833
+ case "tags":
4834
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4835
+ return;
4836
+ case "color":
4837
+ section.event.color = joinFieldValue(tokens, line);
4838
+ return;
4839
+ case "hidden":
4840
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4841
+ line,
4842
+ column: tokens[0].column
4843
+ });
4844
+ return;
4845
+ default:
4846
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4847
+ }
4848
+ }
4849
+ function parseEventPoseField(tokens, line, seenFields) {
4850
+ if (tokens.length < 2) {
4851
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4852
+ }
4853
+ const key = tokens[0].value;
4854
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4855
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4856
+ }
4857
+ if (seenFields.has(key)) {
4858
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4859
+ }
4860
+ seenFields.add(key);
4861
+ return {
4862
+ type: "field",
4863
+ key,
4864
+ values: tokens.slice(1).map((token) => token.value),
4865
+ location: { line, column: tokens[0].column }
4866
+ };
4867
+ }
4010
4868
  function applyObjectField(section, indent, tokens, line) {
4011
4869
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4012
4870
  section.activeBlock = null;
@@ -4065,7 +4923,7 @@ var WorldOrbit = (() => {
4065
4923
  function parseObjectTypeTokens(tokens, line) {
4066
4924
  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");
4067
4925
  }
4068
- function parseLayerTokens(tokens, line) {
4926
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4069
4927
  const layers = {};
4070
4928
  for (const token of parseTokenList(tokens, line, "layers")) {
4071
4929
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -4075,7 +4933,13 @@ var WorldOrbit = (() => {
4075
4933
  layers["orbits-front"] = enabled;
4076
4934
  continue;
4077
4935
  }
4078
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4936
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4937
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4938
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4939
+ line,
4940
+ column: tokens[0]?.column ?? 1
4941
+ });
4942
+ }
4079
4943
  layers[raw] = enabled;
4080
4944
  }
4081
4945
  }
@@ -4093,11 +4957,15 @@ var WorldOrbit = (() => {
4093
4957
  }
4094
4958
  function parseProjectionValue(value, line, column) {
4095
4959
  const normalized = value.toLowerCase();
4096
- if (normalized !== "topdown" && normalized !== "isometric") {
4960
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
4097
4961
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4098
4962
  }
4099
4963
  return normalized;
4100
4964
  }
4965
+ function isSchema25Projection(value) {
4966
+ const normalized = value.toLowerCase();
4967
+ return normalized === "orthographic" || normalized === "perspective";
4968
+ }
4101
4969
  function parsePresetValue(value, line, column) {
4102
4970
  const normalized = value.toLowerCase();
4103
4971
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -4127,6 +4995,48 @@ var WorldOrbit = (() => {
4127
4995
  groupIds: []
4128
4996
  };
4129
4997
  }
4998
+ function createEmptyViewCamera2() {
4999
+ return {
5000
+ azimuth: null,
5001
+ elevation: null,
5002
+ roll: null,
5003
+ distance: null
5004
+ };
5005
+ }
5006
+ function parseInlineViewCamera(tokens, line, current) {
5007
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
5008
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
5009
+ }
5010
+ const camera = current ? { ...current } : createEmptyViewCamera2();
5011
+ const seen = /* @__PURE__ */ new Set();
5012
+ for (let index = 0; index < tokens.length; index += 2) {
5013
+ const fieldToken = tokens[index];
5014
+ const valueToken = tokens[index + 1];
5015
+ const key = fieldToken.value.toLowerCase();
5016
+ if (seen.has(key)) {
5017
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5018
+ }
5019
+ seen.add(key);
5020
+ const value = valueToken.value;
5021
+ switch (key) {
5022
+ case "azimuth":
5023
+ camera.azimuth = parseFiniteNumber2(value, line, fieldToken.column, "camera.azimuth");
5024
+ break;
5025
+ case "elevation":
5026
+ camera.elevation = parseFiniteNumber2(value, line, fieldToken.column, "camera.elevation");
5027
+ break;
5028
+ case "roll":
5029
+ camera.roll = parseFiniteNumber2(value, line, fieldToken.column, "camera.roll");
5030
+ break;
5031
+ case "distance":
5032
+ camera.distance = parsePositiveNumber2(value, line, fieldToken.column, "camera.distance");
5033
+ break;
5034
+ default:
5035
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5036
+ }
5037
+ }
5038
+ return camera;
5039
+ }
4130
5040
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
4131
5041
  const fields = [];
4132
5042
  let index = 0;
@@ -4214,7 +5124,7 @@ var WorldOrbit = (() => {
4214
5124
  }
4215
5125
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4216
5126
  const fieldMap = collectDraftFields(node.fields);
4217
- const placement = extractDraftPlacement(node.objectType, fieldMap);
5127
+ const placement = extractPlacementFromFieldMap(fieldMap);
4218
5128
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
4219
5129
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4220
5130
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -4259,21 +5169,41 @@ var WorldOrbit = (() => {
4259
5169
  object.tolerances = tolerances;
4260
5170
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
4261
5171
  object.typedBlocks = typedBlocks;
4262
- if (sourceSchemaVersion !== "2.1") {
5172
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4263
5173
  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) {
4264
5174
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4265
5175
  }
4266
5176
  }
4267
5177
  return object;
4268
5178
  }
4269
- function collectDraftFields(fields) {
5179
+ function normalizeDraftEvent(event, rawPoses) {
5180
+ return {
5181
+ ...event,
5182
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5183
+ tags: [...new Set(event.tags)],
5184
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5185
+ };
5186
+ }
5187
+ function normalizeDraftEventPose(rawPose) {
5188
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
5189
+ const placement = extractPlacementFromFieldMap(fieldMap);
5190
+ return {
5191
+ objectId: rawPose.objectId,
5192
+ placement,
5193
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5194
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
5195
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
5196
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
5197
+ };
5198
+ }
5199
+ function collectDraftFields(fields, _mode = "object") {
4270
5200
  const grouped = /* @__PURE__ */ new Map();
4271
5201
  for (const field of fields) {
4272
5202
  const spec = getDraftObjectFieldSpec(field.key);
4273
- if (!spec) {
5203
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
4274
5204
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4275
5205
  }
4276
- if (!spec.allowRepeat && grouped.has(field.key)) {
5206
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
4277
5207
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4278
5208
  }
4279
5209
  const existing = grouped.get(field.key) ?? [];
@@ -4282,7 +5212,7 @@ var WorldOrbit = (() => {
4282
5212
  }
4283
5213
  return grouped;
4284
5214
  }
4285
- function extractDraftPlacement(objectType, fieldMap) {
5215
+ function extractPlacementFromFieldMap(fieldMap) {
4286
5216
  const orbitField = fieldMap.get("orbit")?.[0];
4287
5217
  const atField = fieldMap.get("at")?.[0];
4288
5218
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4450,7 +5380,7 @@ var WorldOrbit = (() => {
4450
5380
  }
4451
5381
  }
4452
5382
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4453
- if (sourceSchemaVersion === "2.1") {
5383
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4454
5384
  return;
4455
5385
  }
4456
5386
  diagnostics.push({
@@ -4462,6 +5392,34 @@ var WorldOrbit = (() => {
4462
5392
  column: location.column
4463
5393
  });
4464
5394
  }
5395
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5396
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
5397
+ return;
5398
+ }
5399
+ diagnostics.push({
5400
+ code: "parse.schema25.featureCompatibility",
5401
+ severity: "warning",
5402
+ source: "parse",
5403
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5404
+ line: location.line,
5405
+ column: location.column
5406
+ });
5407
+ }
5408
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
5409
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
5410
+ }
5411
+ function schemaVersionRank(version) {
5412
+ switch (version) {
5413
+ case "2.0-draft":
5414
+ return 0;
5415
+ case "2.0":
5416
+ return 1;
5417
+ case "2.1":
5418
+ return 2;
5419
+ case "2.5":
5420
+ return 3;
5421
+ }
5422
+ }
4465
5423
  function preprocessAtlasSource(source) {
4466
5424
  const chars = [...source];
4467
5425
  const comments = [];
@@ -4549,8 +5507,9 @@ var WorldOrbit = (() => {
4549
5507
  }
4550
5508
 
4551
5509
  // packages/core/dist/load.js
4552
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5510
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
4553
5511
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
5512
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
4554
5513
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
4555
5514
  function detectWorldOrbitSchemaVersion(source) {
4556
5515
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -4564,6 +5523,9 @@ var WorldOrbit = (() => {
4564
5523
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
4565
5524
  return "2.1";
4566
5525
  }
5526
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
5527
+ return "2.5";
5528
+ }
4567
5529
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
4568
5530
  return "2.0";
4569
5531
  }
@@ -4624,7 +5586,7 @@ var WorldOrbit = (() => {
4624
5586
  }
4625
5587
  function loadWorldOrbitSourceWithDiagnostics(source) {
4626
5588
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
4627
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
5589
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1" || schemaVersion === "2.5") {
4628
5590
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
4629
5591
  }
4630
5592
  let ast;
@@ -4893,6 +5855,7 @@ var WorldOrbit = (() => {
4893
5855
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
4894
5856
  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("") : "";
4895
5857
  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("") : "";
5858
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
4896
5859
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
4897
5860
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
4898
5861
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -4928,6 +5891,9 @@ var WorldOrbit = (() => {
4928
5891
  .wo-orbit-front { opacity: 0.9; }
4929
5892
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
4930
5893
  .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5894
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5895
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5896
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
4931
5897
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
4932
5898
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
4933
5899
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -4962,6 +5928,7 @@ var WorldOrbit = (() => {
4962
5928
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
4963
5929
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
4964
5930
  ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5931
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
4965
5932
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
4966
5933
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
4967
5934
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -4969,6 +5936,20 @@ var WorldOrbit = (() => {
4969
5936
  </g>
4970
5937
  </g>
4971
5938
  </svg>`;
5939
+ }
5940
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5941
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5942
+ if (participants.length === 0) {
5943
+ return "";
5944
+ }
5945
+ const stroke = event.event.color || theme.accent;
5946
+ const label = event.event.label || event.event.id;
5947
+ 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("");
5948
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5949
+ ${lineMarkup}
5950
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5951
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5952
+ </g>`;
4972
5953
  }
4973
5954
  function renderDocumentToSvg(document2, options = {}) {
4974
5955
  return renderSceneToSvg(renderDocumentToScene(document2, options), options);
@@ -5568,6 +6549,13 @@ var WorldOrbit = (() => {
5568
6549
  value: `${details.object.resonance.targetObjectId} ${details.object.resonance.ratio}`
5569
6550
  });
5570
6551
  }
6552
+ if (details.relatedEvents.length > 0) {
6553
+ fields.set("events", {
6554
+ key: "events",
6555
+ label: "Events",
6556
+ value: details.relatedEvents.map((event) => event.event.label || event.event.id).join(", ")
6557
+ });
6558
+ }
5571
6559
  if (placement?.mode === "at") {
5572
6560
  fields.set("placement", {
5573
6561
  key: "placement",
@@ -5672,6 +6660,7 @@ var WorldOrbit = (() => {
5672
6660
  padding: options.padding,
5673
6661
  preset: options.preset,
5674
6662
  projection: options.projection,
6663
+ camera: options.camera ? { ...options.camera } : null,
5675
6664
  scaleModel: options.scaleModel ? { ...options.scaleModel } : void 0,
5676
6665
  theme: options.theme,
5677
6666
  layers: options.layers,
@@ -5960,6 +6949,11 @@ var WorldOrbit = (() => {
5960
6949
  if (currentInput.kind !== "scene" && viewpoint.projection !== scene.projection) {
5961
6950
  nextRenderOptions.projection = viewpoint.projection;
5962
6951
  }
6952
+ if (viewpoint.camera) {
6953
+ nextRenderOptions.camera = { ...viewpoint.camera };
6954
+ } else if (renderOptions.camera) {
6955
+ nextRenderOptions.camera = null;
6956
+ }
5963
6957
  if (viewpointLayers) {
5964
6958
  nextRenderOptions.layers = viewpointLayers;
5965
6959
  }
@@ -5982,6 +6976,12 @@ var WorldOrbit = (() => {
5982
6976
  emitAtlasStateChange();
5983
6977
  return true;
5984
6978
  },
6979
+ getActiveEventId() {
6980
+ return renderOptions.activeEventId ?? null;
6981
+ },
6982
+ setActiveEvent(id) {
6983
+ api.setRenderOptions({ activeEventId: id });
6984
+ },
5985
6985
  search(query, limit = 12) {
5986
6986
  return searchSceneObjects(scene, query, limit);
5987
6987
  },
@@ -6246,6 +7246,7 @@ var WorldOrbit = (() => {
6246
7246
  orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
6247
7247
  relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden && (orbit.objectId === renderObject.objectId || renderObject.ancestorIds.includes(orbit.objectId) || renderObject.childIds.includes(orbit.objectId))),
6248
7248
  relations: scene.relations.filter((relation) => !relation.hidden && (relation.fromObjectId === renderObject.objectId || relation.toObjectId === renderObject.objectId)),
7249
+ relatedEvents: scene.events.filter((event) => !event.hidden && (event.targetObjectId === renderObject.objectId || event.objectIds.includes(renderObject.objectId))),
6249
7250
  parent: getObjectById(renderObject.parentId),
6250
7251
  children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
6251
7252
  ancestors: renderObject.ancestorIds.map((ancestorId) => getObjectById(ancestorId)).filter(Boolean),
@@ -6559,16 +7560,19 @@ var WorldOrbit = (() => {
6559
7560
  function cloneRenderOptions(renderOptions) {
6560
7561
  return {
6561
7562
  ...renderOptions,
7563
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
6562
7564
  filter: renderOptions.filter ? { ...renderOptions.filter } : void 0,
6563
7565
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
6564
7566
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
6565
- theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme
7567
+ theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme,
7568
+ activeEventId: renderOptions.activeEventId ?? null
6566
7569
  };
6567
7570
  }
6568
7571
  function mergeRenderOptions(current, next) {
6569
7572
  return {
6570
7573
  ...current,
6571
7574
  ...next,
7575
+ camera: next.camera !== void 0 ? next.camera ? { ...next.camera } : null : current.camera ? { ...current.camera } : null,
6572
7576
  filter: next.filter !== void 0 ? normalizeViewerFilter(next.filter) : current.filter ? { ...current.filter } : void 0,
6573
7577
  scaleModel: next.scaleModel ? {
6574
7578
  ...current.scaleModel ?? {},
@@ -6582,7 +7586,7 @@ var WorldOrbit = (() => {
6582
7586
  };
6583
7587
  }
6584
7588
  function hasSceneAffectingRenderOptions(options) {
6585
- 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;
7589
+ 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;
6586
7590
  }
6587
7591
  function resolveSourceRenderOptions2(loaded, renderOptions) {
6588
7592
  const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
@@ -7179,6 +8183,8 @@ var WorldOrbit = (() => {
7179
8183
  }
7180
8184
  function buildInspectorSnapshot() {
7181
8185
  const activeViewer = requireViewer();
8186
+ const scene = activeViewer.getScene();
8187
+ const camera = scene.camera;
7182
8188
  return {
7183
8189
  selection: activeViewer.getSelectionDetails(),
7184
8190
  activeViewpoint: activeViewer.getActiveViewpoint(),
@@ -7186,13 +8192,21 @@ var WorldOrbit = (() => {
7186
8192
  atlasState: activeViewer.getAtlasState(),
7187
8193
  visibleObjectIds: activeViewer.getVisibleObjects().map((object) => object.objectId),
7188
8194
  scene: {
7189
- title: activeViewer.getScene().title,
7190
- projection: activeViewer.getScene().projection,
7191
- renderPreset: activeViewer.getScene().renderPreset,
7192
- groupCount: activeViewer.getScene().groups.length,
7193
- semanticGroupCount: activeViewer.getScene().semanticGroups.length,
7194
- relationCount: activeViewer.getScene().relations.length,
7195
- viewpointCount: activeViewer.getScene().viewpoints.length
8195
+ title: scene.title,
8196
+ projection: scene.projection,
8197
+ renderProjection: scene.renderProjection,
8198
+ camera: camera ? {
8199
+ azimuth: camera.azimuth,
8200
+ elevation: camera.elevation,
8201
+ roll: camera.roll,
8202
+ distance: camera.distance
8203
+ } : null,
8204
+ renderPreset: scene.renderPreset,
8205
+ groupCount: scene.groups.length,
8206
+ semanticGroupCount: scene.semanticGroups.length,
8207
+ relationCount: scene.relations.length,
8208
+ eventCount: scene.events.length,
8209
+ viewpointCount: scene.viewpoints.length
7196
8210
  }
7197
8211
  };
7198
8212
  }