worldorbit 2.5.16 → 2.5.17

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 (34) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/core/dist/index.js +750 -73
  3. package/dist/browser/editor/dist/index.js +1303 -135
  4. package/dist/browser/markdown/dist/index.js +631 -72
  5. package/dist/browser/viewer/dist/index.js +658 -77
  6. package/dist/unpkg/core/dist/index.js +750 -73
  7. package/dist/unpkg/editor/dist/index.js +1303 -135
  8. package/dist/unpkg/markdown/dist/index.js +631 -72
  9. package/dist/unpkg/viewer/dist/index.js +658 -77
  10. package/dist/unpkg/worldorbit-core.min.js +12 -12
  11. package/dist/unpkg/worldorbit-editor.min.js +284 -202
  12. package/dist/unpkg/worldorbit-markdown.min.js +66 -58
  13. package/dist/unpkg/worldorbit-viewer.min.js +76 -68
  14. package/dist/unpkg/worldorbit.js +797 -78
  15. package/dist/unpkg/worldorbit.min.js +80 -72
  16. package/package.json +1 -1
  17. package/packages/core/dist/atlas-edit.js +74 -0
  18. package/packages/core/dist/atlas-validate.js +122 -8
  19. package/packages/core/dist/draft-parse.js +212 -8
  20. package/packages/core/dist/draft.d.ts +5 -2
  21. package/packages/core/dist/draft.js +59 -3
  22. package/packages/core/dist/format.js +63 -1
  23. package/packages/core/dist/normalize.js +1 -0
  24. package/packages/core/dist/scene.js +248 -46
  25. package/packages/core/dist/types.d.ts +41 -2
  26. package/packages/editor/dist/editor.js +597 -61
  27. package/packages/editor/dist/types.d.ts +3 -1
  28. package/packages/viewer/dist/atlas-state.js +6 -0
  29. package/packages/viewer/dist/atlas-viewer.js +1 -0
  30. package/packages/viewer/dist/render.js +31 -2
  31. package/packages/viewer/dist/theme.js +1 -0
  32. package/packages/viewer/dist/tooltip.js +9 -0
  33. package/packages/viewer/dist/types.d.ts +8 -1
  34. package/packages/viewer/dist/viewer.js +12 -1
@@ -544,6 +544,7 @@
544
544
  system,
545
545
  groups: [],
546
546
  relations: [],
547
+ events: [],
547
548
  objects
548
549
  };
549
550
  }
@@ -1001,8 +1002,10 @@
1001
1002
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1002
1003
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1003
1004
  const systemId = document.system?.id ?? null;
1004
- const objectMap = new Map(document.objects.map((object) => [object.id, object]));
1005
- const relationships = buildSceneRelationships(document.objects, objectMap);
1005
+ const activeEventId = options.activeEventId ?? null;
1006
+ const effectiveObjects = createEffectiveObjects(document.objects, document.events ?? [], activeEventId);
1007
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
1008
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
1006
1009
  const positions = /* @__PURE__ */ new Map();
1007
1010
  const orbitDrafts = [];
1008
1011
  const leaderDrafts = [];
@@ -1011,7 +1014,7 @@
1011
1014
  const atObjects = [];
1012
1015
  const surfaceChildren = /* @__PURE__ */ new Map();
1013
1016
  const orbitChildren = /* @__PURE__ */ new Map();
1014
- for (const object of document.objects) {
1017
+ for (const object of effectiveObjects) {
1015
1018
  const placement = object.placement;
1016
1019
  if (!placement) {
1017
1020
  rootObjects.push(object);
@@ -1106,13 +1109,14 @@
1106
1109
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1107
1110
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1108
1111
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1109
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1112
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1110
1113
  const relations = createSceneRelations(document, objects);
1111
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1112
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1114
+ const events = createSceneEvents(document.events ?? [], objects, activeEventId);
1115
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1116
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1113
1117
  const semanticGroups = createSceneSemanticGroups(document, objects);
1114
1118
  const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
1115
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1119
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1116
1120
  return {
1117
1121
  width,
1118
1122
  height,
@@ -1138,6 +1142,8 @@
1138
1142
  groups,
1139
1143
  semanticGroups,
1140
1144
  viewpoints,
1145
+ events,
1146
+ activeEventId,
1141
1147
  objects,
1142
1148
  orbitVisuals,
1143
1149
  relations,
@@ -1156,6 +1162,35 @@
1156
1162
  y: center.y + dx * sin + dy * cos
1157
1163
  };
1158
1164
  }
1165
+ function createEffectiveObjects(objects, events, activeEventId) {
1166
+ const cloned = objects.map((object) => structuredClone(object));
1167
+ if (!activeEventId) {
1168
+ return cloned;
1169
+ }
1170
+ const activeEvent = events.find((event) => event.id === activeEventId);
1171
+ if (!activeEvent) {
1172
+ return cloned;
1173
+ }
1174
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1175
+ for (const pose of activeEvent.positions) {
1176
+ const object = objectMap.get(pose.objectId);
1177
+ if (!object) {
1178
+ continue;
1179
+ }
1180
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1181
+ if (pose.inner) {
1182
+ object.properties.inner = { ...pose.inner };
1183
+ } else {
1184
+ delete object.properties.inner;
1185
+ }
1186
+ if (pose.outer) {
1187
+ object.properties.outer = { ...pose.outer };
1188
+ } else {
1189
+ delete object.properties.outer;
1190
+ }
1191
+ }
1192
+ return cloned;
1193
+ }
1159
1194
  function resolveLayoutPreset(document) {
1160
1195
  const rawScale = String(document.system?.properties.scale ?? "balanced").toLowerCase();
1161
1196
  switch (rawScale) {
@@ -1311,24 +1346,14 @@
1311
1346
  hidden: draft.object.properties.hidden === true
1312
1347
  };
1313
1348
  }
1314
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1349
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1315
1350
  const labels = [];
1316
1351
  const occupied = [];
1317
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1352
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1353
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1318
1354
  for (const object of visibleObjects) {
1319
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1320
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1321
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1322
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1323
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1324
- let attempts = 0;
1325
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1326
- labelY += direction * 14 * labelMultiplier;
1327
- secondaryY += direction * 14 * labelMultiplier;
1328
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1329
- attempts += 1;
1330
- }
1331
- occupied.push(bounds);
1355
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1356
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1332
1357
  labels.push({
1333
1358
  renderId: `${object.renderId}-label`,
1334
1359
  objectId: object.objectId,
@@ -1337,17 +1362,128 @@
1337
1362
  semanticGroupIds: [...object.semanticGroupIds],
1338
1363
  label: object.label,
1339
1364
  secondaryLabel: object.secondaryLabel,
1340
- x: object.x,
1341
- y: labelY,
1342
- secondaryY,
1343
- textAnchor: "middle",
1344
- direction: direction < 0 ? "above" : "below",
1365
+ x: placement.x,
1366
+ y: placement.labelY,
1367
+ secondaryY: placement.secondaryY,
1368
+ textAnchor: placement.textAnchor,
1369
+ direction: placement.direction,
1345
1370
  hidden: object.hidden
1346
1371
  });
1347
1372
  }
1348
1373
  return labels;
1349
1374
  }
1350
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1375
+ function compareLabelPlacementOrder(left, right) {
1376
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1377
+ if (priorityDiff !== 0) {
1378
+ return priorityDiff;
1379
+ }
1380
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1381
+ if (renderPriorityDiff !== 0) {
1382
+ return renderPriorityDiff;
1383
+ }
1384
+ return left.sortKey - right.sortKey;
1385
+ }
1386
+ function labelPlacementPriority(object) {
1387
+ switch (object.object.type) {
1388
+ case "star":
1389
+ return 0;
1390
+ case "planet":
1391
+ return 1;
1392
+ case "moon":
1393
+ return 2;
1394
+ case "belt":
1395
+ case "ring":
1396
+ return 3;
1397
+ case "asteroid":
1398
+ case "comet":
1399
+ return 4;
1400
+ case "structure":
1401
+ case "phenomenon":
1402
+ return 5;
1403
+ }
1404
+ }
1405
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1406
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1407
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1408
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1409
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1410
+ const rect = createLabelRect(object, placement, labelMultiplier);
1411
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1412
+ return placement;
1413
+ }
1414
+ }
1415
+ }
1416
+ return null;
1417
+ }
1418
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1419
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1420
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1421
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1422
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1423
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1424
+ 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";
1425
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1426
+ }
1427
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1428
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1429
+ return object.y >= parent.y ? "below" : "above";
1430
+ }
1431
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1432
+ }
1433
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1434
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1435
+ return object.x >= parent.x ? "right" : "left";
1436
+ }
1437
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1438
+ }
1439
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1440
+ const step = 14 * labelMultiplier;
1441
+ switch (direction) {
1442
+ case "above": {
1443
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1444
+ return {
1445
+ x: object.x,
1446
+ labelY,
1447
+ secondaryY: labelY - 16 * labelMultiplier,
1448
+ textAnchor: "middle",
1449
+ direction
1450
+ };
1451
+ }
1452
+ case "below": {
1453
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1454
+ return {
1455
+ x: object.x,
1456
+ labelY,
1457
+ secondaryY: labelY + 16 * labelMultiplier,
1458
+ textAnchor: "middle",
1459
+ direction
1460
+ };
1461
+ }
1462
+ case "left": {
1463
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1464
+ const labelY = object.y - 4 * labelMultiplier;
1465
+ return {
1466
+ x,
1467
+ labelY,
1468
+ secondaryY: labelY + 16 * labelMultiplier,
1469
+ textAnchor: "end",
1470
+ direction
1471
+ };
1472
+ }
1473
+ case "right": {
1474
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1475
+ const labelY = object.y - 4 * labelMultiplier;
1476
+ return {
1477
+ x,
1478
+ labelY,
1479
+ secondaryY: labelY + 16 * labelMultiplier,
1480
+ textAnchor: "start",
1481
+ direction
1482
+ };
1483
+ }
1484
+ }
1485
+ }
1486
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1351
1487
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1352
1488
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1353
1489
  return [
@@ -1362,6 +1498,10 @@
1362
1498
  id: "relations",
1363
1499
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1364
1500
  },
1501
+ {
1502
+ id: "events",
1503
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1504
+ },
1365
1505
  {
1366
1506
  id: "objects",
1367
1507
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1373,7 +1513,7 @@
1373
1513
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1374
1514
  ];
1375
1515
  }
1376
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1516
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1377
1517
  const groups = /* @__PURE__ */ new Map();
1378
1518
  const ensureGroup = (groupId) => {
1379
1519
  if (!groupId) {
@@ -1422,7 +1562,7 @@
1422
1562
  }
1423
1563
  }
1424
1564
  for (const group of groups.values()) {
1425
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1565
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1426
1566
  }
1427
1567
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1428
1568
  }
@@ -1456,6 +1596,29 @@
1456
1596
  };
1457
1597
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1458
1598
  }
1599
+ function createSceneEvents(events, objects, activeEventId) {
1600
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1601
+ return events.map((event) => {
1602
+ const objectIds = [.../* @__PURE__ */ new Set([
1603
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1604
+ ...event.participantObjectIds
1605
+ ])];
1606
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1607
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1608
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1609
+ return {
1610
+ renderId: `${createRenderId(event.id)}-event`,
1611
+ eventId: event.id,
1612
+ event,
1613
+ objectIds,
1614
+ participantIds: [...event.participantObjectIds],
1615
+ targetObjectId: event.targetObjectId,
1616
+ x: centroidX,
1617
+ y: centroidY,
1618
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1619
+ };
1620
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1621
+ }
1459
1622
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
1460
1623
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
1461
1624
  const drafts = /* @__PURE__ */ new Map();
@@ -1509,6 +1672,7 @@
1509
1672
  summary: "Fit the whole system with the current atlas defaults.",
1510
1673
  objectId: null,
1511
1674
  selectedObjectId: null,
1675
+ eventIds: [],
1512
1676
  projection,
1513
1677
  preset,
1514
1678
  rotationDeg: 0,
@@ -1545,6 +1709,9 @@
1545
1709
  draft.select = normalizedValue;
1546
1710
  }
1547
1711
  return;
1712
+ case "events":
1713
+ draft.eventIds = splitListValue(normalizedValue);
1714
+ return;
1548
1715
  case "projection":
1549
1716
  case "view":
1550
1717
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1601,6 +1768,7 @@
1601
1768
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1602
1769
  objectId,
1603
1770
  selectedObjectId,
1771
+ eventIds: [...new Set(draft.eventIds ?? [])],
1604
1772
  projection: draft.projection ?? projection,
1605
1773
  preset: draft.preset ?? preset,
1606
1774
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1658,7 +1826,7 @@
1658
1826
  next["orbits-front"] = enabled;
1659
1827
  continue;
1660
1828
  }
1661
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1829
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1662
1830
  next[rawLayer] = enabled;
1663
1831
  }
1664
1832
  }
@@ -1706,7 +1874,7 @@
1706
1874
  }
1707
1875
  return parts.join(" - ");
1708
1876
  }
1709
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1877
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1710
1878
  let minX = Number.POSITIVE_INFINITY;
1711
1879
  let minY = Number.POSITIVE_INFINITY;
1712
1880
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1736,7 +1904,7 @@
1736
1904
  for (const label of labels) {
1737
1905
  if (label.hidden)
1738
1906
  continue;
1739
- includeLabelBounds(label, include);
1907
+ includeLabelBounds(label, include, labelMultiplier);
1740
1908
  }
1741
1909
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1742
1910
  return createBounds(0, 0, width, height);
@@ -1774,13 +1942,10 @@
1774
1942
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1775
1943
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1776
1944
  }
1777
- function includeLabelBounds(label, include) {
1778
- const labelScale = 1;
1779
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1780
- include(label.x - labelHalfWidth, label.y - 18);
1781
- include(label.x + labelHalfWidth, label.y + 8);
1782
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1783
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1945
+ function includeLabelBounds(label, include, labelMultiplier) {
1946
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1947
+ include(bounds.left, bounds.top);
1948
+ include(bounds.right, bounds.bottom);
1784
1949
  }
1785
1950
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1786
1951
  if (positions.has(object.id)) {
@@ -2170,7 +2335,7 @@
2170
2335
  return null;
2171
2336
  }
2172
2337
  }
2173
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2338
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2174
2339
  let minX = Number.POSITIVE_INFINITY;
2175
2340
  let minY = Number.POSITIVE_INFINITY;
2176
2341
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2199,7 +2364,7 @@
2199
2364
  }
2200
2365
  for (const label of labels) {
2201
2366
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2202
- includeLabelBounds(label, include);
2367
+ includeLabelBounds(label, include, labelMultiplier);
2203
2368
  }
2204
2369
  }
2205
2370
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2224,12 +2389,28 @@
2224
2389
  }
2225
2390
  return current.id;
2226
2391
  }
2227
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2392
+ function createLabelRect(object, placement, labelMultiplier) {
2393
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2394
+ }
2395
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2396
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2397
+ const labelWidth = labelHalfWidth * 2;
2398
+ const topPadding = direction === "above" ? 18 : 12;
2399
+ const bottomPadding = direction === "above" ? 8 : 12;
2400
+ let left = x - labelHalfWidth;
2401
+ let right = x + labelHalfWidth;
2402
+ if (textAnchor === "start") {
2403
+ left = x;
2404
+ right = x + labelWidth;
2405
+ } else if (textAnchor === "end") {
2406
+ left = x - labelWidth;
2407
+ right = x;
2408
+ }
2228
2409
  return {
2229
- left: x - labelHalfWidth,
2230
- right: x + labelHalfWidth,
2231
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2232
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2410
+ left,
2411
+ right,
2412
+ top: Math.min(labelY, secondaryY) - topPadding,
2413
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2233
2414
  };
2234
2415
  }
2235
2416
  function rectsOverlap(left, right) {
@@ -2416,11 +2597,6 @@
2416
2597
  function customColorFor(value) {
2417
2598
  return typeof value === "string" && value.trim() ? value : void 0;
2418
2599
  }
2419
- function estimateLabelHalfWidth(object, labelMultiplier) {
2420
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2421
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2422
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2423
- }
2424
2600
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2425
2601
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2426
2602
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2460,6 +2636,7 @@
2460
2636
  system,
2461
2637
  groups: structuredClone(document.groups ?? []),
2462
2638
  relations: structuredClone(document.relations ?? []),
2639
+ events: structuredClone(document.events ?? []),
2463
2640
  objects: document.objects.map(cloneWorldOrbitObject),
2464
2641
  diagnostics
2465
2642
  };
@@ -2467,7 +2644,7 @@
2467
2644
  function upgradeDocumentToDraftV2(document, options = {}) {
2468
2645
  return convertAtlasDocumentToLegacyDraft(upgradeDocumentToV2(document, options));
2469
2646
  }
2470
- function materializeAtlasDocument(document) {
2647
+ function materializeAtlasDocument(document, options = {}) {
2471
2648
  const system = document.system ? {
2472
2649
  type: "system",
2473
2650
  id: document.system.id,
@@ -2478,6 +2655,8 @@
2478
2655
  properties: materializeDraftSystemProperties(document.system),
2479
2656
  info: materializeDraftSystemInfo(document.system)
2480
2657
  } : null;
2658
+ const objects = document.objects.map(cloneWorldOrbitObject);
2659
+ applyEventPoseOverrides(objects, document.events ?? [], options.activeEventId ?? null);
2481
2660
  return {
2482
2661
  format: "worldorbit",
2483
2662
  version: "1.0",
@@ -2485,7 +2664,8 @@
2485
2664
  system,
2486
2665
  groups: structuredClone(document.groups ?? []),
2487
2666
  relations: structuredClone(document.relations ?? []),
2488
- objects: document.objects.map(cloneWorldOrbitObject)
2667
+ events: document.events.map(cloneWorldOrbitEvent),
2668
+ objects
2489
2669
  };
2490
2670
  }
2491
2671
  function materializeDraftDocument(document) {
@@ -2613,6 +2793,7 @@
2613
2793
  summary: viewpoint.summary,
2614
2794
  focusObjectId: viewpoint.objectId,
2615
2795
  selectedObjectId: viewpoint.selectedObjectId,
2796
+ events: [...viewpoint.eventIds],
2616
2797
  projection: viewpoint.projection,
2617
2798
  preset: viewpoint.preset,
2618
2799
  zoom: viewpoint.scale,
@@ -2645,6 +2826,52 @@
2645
2826
  info: { ...object.info }
2646
2827
  };
2647
2828
  }
2829
+ function cloneWorldOrbitEvent(event) {
2830
+ return {
2831
+ ...event,
2832
+ participantObjectIds: [...event.participantObjectIds],
2833
+ tags: [...event.tags],
2834
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2835
+ };
2836
+ }
2837
+ function cloneWorldOrbitEventPose(pose) {
2838
+ return {
2839
+ objectId: pose.objectId,
2840
+ placement: clonePlacement(pose.placement),
2841
+ inner: pose.inner ? { ...pose.inner } : void 0,
2842
+ outer: pose.outer ? { ...pose.outer } : void 0
2843
+ };
2844
+ }
2845
+ function clonePlacement(placement) {
2846
+ return placement ? structuredClone(placement) : null;
2847
+ }
2848
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2849
+ if (!activeEventId) {
2850
+ return;
2851
+ }
2852
+ const event = events.find((entry) => entry.id === activeEventId);
2853
+ if (!event) {
2854
+ return;
2855
+ }
2856
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2857
+ for (const pose of event.positions) {
2858
+ const object = objectMap.get(pose.objectId);
2859
+ if (!object) {
2860
+ continue;
2861
+ }
2862
+ object.placement = clonePlacement(pose.placement);
2863
+ if (pose.inner) {
2864
+ object.properties.inner = { ...pose.inner };
2865
+ } else {
2866
+ delete object.properties.inner;
2867
+ }
2868
+ if (pose.outer) {
2869
+ object.properties.outer = { ...pose.outer };
2870
+ } else {
2871
+ delete object.properties.outer;
2872
+ }
2873
+ }
2874
+ }
2648
2875
  function cloneProperties(properties) {
2649
2876
  const next = {};
2650
2877
  for (const [key, value] of Object.entries(properties)) {
@@ -2742,6 +2969,9 @@
2742
2969
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2743
2970
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2744
2971
  }
2972
+ if (viewpoint.events.length > 0) {
2973
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2974
+ }
2745
2975
  }
2746
2976
  for (const annotation of system.annotations) {
2747
2977
  const prefix = `annotation.${annotation.id}`;
@@ -2766,7 +2996,7 @@
2766
2996
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2767
2997
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2768
2998
  }
2769
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2999
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2770
3000
  if (layers[key] !== void 0) {
2771
3001
  tokens.push(layers[key] ? key : `-${key}`);
2772
3002
  }
@@ -2870,6 +3100,10 @@
2870
3100
  lines.push("");
2871
3101
  lines.push(...formatAtlasRelation(relation));
2872
3102
  }
3103
+ for (const event of [...document.events].sort(compareIdLike)) {
3104
+ lines.push("");
3105
+ lines.push(...formatAtlasEvent(event));
3106
+ }
2873
3107
  const sortedObjects = [...document.objects].sort(compareObjects);
2874
3108
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2875
3109
  lines.push("");
@@ -2900,6 +3134,10 @@
2900
3134
  lines.push("");
2901
3135
  lines.push(...formatAtlasRelation(relation));
2902
3136
  }
3137
+ for (const event of [...legacy.events].sort(compareIdLike)) {
3138
+ lines.push("");
3139
+ lines.push(...formatAtlasEvent(event));
3140
+ }
2903
3141
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2904
3142
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2905
3143
  lines.push("");
@@ -3111,6 +3349,9 @@
3111
3349
  if (layerTokens.length > 0) {
3112
3350
  lines.push(` layers ${layerTokens.join(" ")}`);
3113
3351
  }
3352
+ if (viewpoint.events.length > 0) {
3353
+ lines.push(` events ${viewpoint.events.join(" ")}`);
3354
+ }
3114
3355
  if (viewpoint.filter) {
3115
3356
  lines.push(" filter");
3116
3357
  if (viewpoint.filter.query) {
@@ -3183,6 +3424,54 @@
3183
3424
  }
3184
3425
  return lines;
3185
3426
  }
3427
+ function formatAtlasEvent(event) {
3428
+ const lines = [`event ${event.id}`, ` kind ${quoteIfNeeded(event.kind)}`];
3429
+ if (event.label) {
3430
+ lines.push(` label ${quoteIfNeeded(event.label)}`);
3431
+ }
3432
+ if (event.summary) {
3433
+ lines.push(` summary ${quoteIfNeeded(event.summary)}`);
3434
+ }
3435
+ if (event.targetObjectId) {
3436
+ lines.push(` target ${event.targetObjectId}`);
3437
+ }
3438
+ if (event.participantObjectIds.length > 0) {
3439
+ lines.push(` participants ${event.participantObjectIds.join(" ")}`);
3440
+ }
3441
+ if (event.timing) {
3442
+ lines.push(` timing ${quoteIfNeeded(event.timing)}`);
3443
+ }
3444
+ if (event.visibility) {
3445
+ lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
3446
+ }
3447
+ if (event.tags.length > 0) {
3448
+ lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3449
+ }
3450
+ if (event.color) {
3451
+ lines.push(` color ${quoteIfNeeded(event.color)}`);
3452
+ }
3453
+ if (event.hidden) {
3454
+ lines.push(" hidden true");
3455
+ }
3456
+ if (event.positions.length > 0) {
3457
+ lines.push("");
3458
+ lines.push(" positions");
3459
+ for (const pose of [...event.positions].sort(comparePoseObjectId)) {
3460
+ lines.push(` pose ${pose.objectId}`);
3461
+ for (const fieldLine of formatEventPoseFields(pose)) {
3462
+ lines.push(` ${fieldLine}`);
3463
+ }
3464
+ }
3465
+ }
3466
+ return lines;
3467
+ }
3468
+ function formatEventPoseFields(pose) {
3469
+ return [
3470
+ ...formatPlacement(pose.placement),
3471
+ ...formatOptionalUnit("inner", pose.inner),
3472
+ ...formatOptionalUnit("outer", pose.outer)
3473
+ ];
3474
+ }
3186
3475
  function formatValue(value) {
3187
3476
  if (Array.isArray(value)) {
3188
3477
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3224,7 +3513,7 @@
3224
3513
  if (orbitFront !== void 0 || orbitBack !== void 0) {
3225
3514
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
3226
3515
  }
3227
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3516
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
3228
3517
  if (layers[key] !== void 0) {
3229
3518
  tokens.push(layers[key] ? key : `-${key}`);
3230
3519
  }
@@ -3252,6 +3541,9 @@
3252
3541
  function compareIdLike(left, right) {
3253
3542
  return left.id.localeCompare(right.id);
3254
3543
  }
3544
+ function comparePoseObjectId(left, right) {
3545
+ return left.objectId.localeCompare(right.objectId);
3546
+ }
3255
3547
  function objectTypeIndex(objectType) {
3256
3548
  switch (objectType) {
3257
3549
  case "star":
@@ -3447,6 +3739,7 @@
3447
3739
  const diagnostics = [];
3448
3740
  const objectMap = new Map(document.objects.map((object) => [object.id, object]));
3449
3741
  const groupIds = new Set(document.groups.map((group) => group.id));
3742
+ const eventIds = new Set(document.events.map((event) => event.id));
3450
3743
  if (!document.system) {
3451
3744
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3452
3745
  }
@@ -3456,6 +3749,7 @@
3456
3749
  ["viewpoint", document.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3457
3750
  ["annotation", document.system?.annotations.map((annotation) => annotation.id) ?? []],
3458
3751
  ["relation", document.relations.map((relation) => relation.id)],
3752
+ ["event", document.events.map((event) => event.id)],
3459
3753
  ["object", document.objects.map((object) => object.id)]
3460
3754
  ]) {
3461
3755
  for (const id of ids) {
@@ -3471,11 +3765,14 @@
3471
3765
  validateRelation(relation, objectMap, diagnostics);
3472
3766
  }
3473
3767
  for (const viewpoint of document.system?.viewpoints ?? []) {
3474
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3768
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3475
3769
  }
3476
3770
  for (const object of document.objects) {
3477
3771
  validateObject(object, document.system, objectMap, groupIds, diagnostics);
3478
3772
  }
3773
+ for (const event of document.events) {
3774
+ validateEvent(event, objectMap, diagnostics);
3775
+ }
3479
3776
  return diagnostics;
3480
3777
  }
3481
3778
  function validateRelation(relation, objectMap, diagnostics) {
@@ -3493,13 +3790,19 @@
3493
3790
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3494
3791
  }
3495
3792
  }
3496
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3497
- if (!filter || sourceSchemaVersion !== "2.1") {
3498
- return;
3499
- }
3500
- for (const groupId of filter.groupIds) {
3501
- if (!groupIds.has(groupId)) {
3502
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3793
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3794
+ if (sourceSchemaVersion === "2.1") {
3795
+ if (filter) {
3796
+ for (const groupId of filter.groupIds) {
3797
+ if (!groupIds.has(groupId)) {
3798
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3799
+ }
3800
+ }
3801
+ }
3802
+ for (const eventId of eventRefs) {
3803
+ if (!eventIds.has(eventId)) {
3804
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3805
+ }
3503
3806
  }
3504
3807
  }
3505
3808
  }
@@ -3585,6 +3888,103 @@
3585
3888
  }
3586
3889
  }
3587
3890
  }
3891
+ function validateEvent(event, objectMap, diagnostics) {
3892
+ const fieldPrefix = `event.${event.id}`;
3893
+ const referencedIds = /* @__PURE__ */ new Set();
3894
+ if (!event.kind.trim()) {
3895
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3896
+ }
3897
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3898
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3899
+ }
3900
+ if (event.targetObjectId) {
3901
+ referencedIds.add(event.targetObjectId);
3902
+ if (!objectMap.has(event.targetObjectId)) {
3903
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3904
+ }
3905
+ }
3906
+ const seenParticipants = /* @__PURE__ */ new Set();
3907
+ for (const participantId of event.participantObjectIds) {
3908
+ referencedIds.add(participantId);
3909
+ if (seenParticipants.has(participantId)) {
3910
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3911
+ continue;
3912
+ }
3913
+ seenParticipants.add(participantId);
3914
+ if (!objectMap.has(participantId)) {
3915
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3916
+ }
3917
+ }
3918
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3919
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3920
+ }
3921
+ if (event.positions.length === 0) {
3922
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3923
+ }
3924
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3925
+ 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`));
3926
+ }
3927
+ const poseIds = /* @__PURE__ */ new Set();
3928
+ for (const pose of event.positions) {
3929
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3930
+ if (poseIds.has(pose.objectId)) {
3931
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3932
+ continue;
3933
+ }
3934
+ poseIds.add(pose.objectId);
3935
+ const object = objectMap.get(pose.objectId);
3936
+ if (!object) {
3937
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3938
+ continue;
3939
+ }
3940
+ if (!referencedIds.has(pose.objectId)) {
3941
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3942
+ }
3943
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3944
+ }
3945
+ }
3946
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3947
+ const placement = pose.placement;
3948
+ if (!placement) {
3949
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3950
+ return;
3951
+ }
3952
+ if (placement.mode === "orbit") {
3953
+ if (!objectMap.has(placement.target)) {
3954
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3955
+ }
3956
+ if (placement.distance && placement.semiMajor) {
3957
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3958
+ }
3959
+ return;
3960
+ }
3961
+ if (placement.mode === "surface") {
3962
+ const target = objectMap.get(placement.target);
3963
+ if (!target) {
3964
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3965
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3966
+ 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`));
3967
+ }
3968
+ return;
3969
+ }
3970
+ if (placement.mode === "at") {
3971
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3972
+ 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`));
3973
+ }
3974
+ const reference = placement.reference;
3975
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3976
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3977
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3978
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3979
+ } else if (reference.kind === "lagrange") {
3980
+ if (!objectMap.has(reference.primary)) {
3981
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3982
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3983
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3984
+ }
3985
+ }
3986
+ }
3987
+ }
3588
3988
  function validateAtTarget(object, objectMap, diagnostics) {
3589
3989
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3590
3990
  if (!reference) {
@@ -3785,6 +4185,21 @@
3785
4185
  });
3786
4186
  }
3787
4187
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
4188
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
4189
+ "orbit",
4190
+ "distance",
4191
+ "semiMajor",
4192
+ "eccentricity",
4193
+ "period",
4194
+ "angle",
4195
+ "inclination",
4196
+ "phase",
4197
+ "at",
4198
+ "surface",
4199
+ "free",
4200
+ "inner",
4201
+ "outer"
4202
+ ]);
3788
4203
  function parseWorldOrbitAtlas(source) {
3789
4204
  return parseAtlasSource(source);
3790
4205
  }
@@ -3802,12 +4217,15 @@
3802
4217
  const objectNodes = [];
3803
4218
  const groups = [];
3804
4219
  const relations = [];
4220
+ const events = [];
4221
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3805
4222
  let sawDefaults = false;
3806
4223
  let sawAtlas = false;
3807
4224
  const viewpointIds = /* @__PURE__ */ new Set();
3808
4225
  const annotationIds = /* @__PURE__ */ new Set();
3809
4226
  const groupIds = /* @__PURE__ */ new Set();
3810
4227
  const relationIds = /* @__PURE__ */ new Set();
4228
+ const eventIds = /* @__PURE__ */ new Set();
3811
4229
  for (let index = 0; index < lines.length; index++) {
3812
4230
  const rawLine = lines[index];
3813
4231
  const lineNumber = index + 1;
@@ -3838,7 +4256,7 @@
3838
4256
  continue;
3839
4257
  }
3840
4258
  if (indent === 0) {
3841
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
4259
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3842
4260
  if (section.kind === "system") {
3843
4261
  system = section.system;
3844
4262
  } else if (section.kind === "defaults") {
@@ -3857,6 +4275,7 @@
3857
4275
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3858
4276
  }
3859
4277
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4278
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3860
4279
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3861
4280
  const baseDocument = {
3862
4281
  format: "worldorbit",
@@ -3864,6 +4283,7 @@
3864
4283
  system,
3865
4284
  groups,
3866
4285
  relations,
4286
+ events: normalizedEvents,
3867
4287
  objects,
3868
4288
  diagnostics
3869
4289
  };
@@ -3899,7 +4319,7 @@
3899
4319
  const version = tokens[1].value.toLowerCase();
3900
4320
  return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3901
4321
  }
3902
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
4322
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3903
4323
  const keyword = tokens[0]?.value.toLowerCase();
3904
4324
  switch (keyword) {
3905
4325
  case "system":
@@ -3936,7 +4356,7 @@
3936
4356
  if (!system) {
3937
4357
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3938
4358
  }
3939
- return startViewpointSection(tokens, line, system, viewpointIds);
4359
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3940
4360
  case "annotation":
3941
4361
  if (!system) {
3942
4362
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3948,6 +4368,9 @@
3948
4368
  case "relation":
3949
4369
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3950
4370
  return startRelationSection(tokens, line, relations, relationIds);
4371
+ case "event":
4372
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4373
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3951
4374
  case "object":
3952
4375
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3953
4376
  default:
@@ -3984,7 +4407,7 @@
3984
4407
  seenFields: /* @__PURE__ */ new Set()
3985
4408
  };
3986
4409
  }
3987
- function startViewpointSection(tokens, line, system, viewpointIds) {
4410
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3988
4411
  if (tokens.length !== 2) {
3989
4412
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3990
4413
  }
@@ -4001,6 +4424,7 @@
4001
4424
  summary: "",
4002
4425
  focusObjectId: null,
4003
4426
  selectedObjectId: null,
4427
+ events: [],
4004
4428
  projection: system.defaults.view,
4005
4429
  preset: system.defaults.preset,
4006
4430
  zoom: null,
@@ -4013,6 +4437,8 @@
4013
4437
  return {
4014
4438
  kind: "viewpoint",
4015
4439
  viewpoint,
4440
+ sourceSchemaVersion,
4441
+ diagnostics,
4016
4442
  seenFields: /* @__PURE__ */ new Set(),
4017
4443
  inFilter: false,
4018
4444
  filterIndent: null,
@@ -4103,6 +4529,49 @@
4103
4529
  seenFields: /* @__PURE__ */ new Set()
4104
4530
  };
4105
4531
  }
4532
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4533
+ if (tokens.length !== 2) {
4534
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4535
+ }
4536
+ const id = normalizeIdentifier2(tokens[1].value);
4537
+ if (!id) {
4538
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4539
+ }
4540
+ if (eventIds.has(id)) {
4541
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4542
+ }
4543
+ const event = {
4544
+ id,
4545
+ kind: "",
4546
+ label: humanizeIdentifier3(id),
4547
+ summary: null,
4548
+ targetObjectId: null,
4549
+ participantObjectIds: [],
4550
+ timing: null,
4551
+ visibility: null,
4552
+ tags: [],
4553
+ color: null,
4554
+ hidden: false,
4555
+ positions: []
4556
+ };
4557
+ const rawPoses = [];
4558
+ events.push(event);
4559
+ eventPoseNodes.set(id, rawPoses);
4560
+ eventIds.add(id);
4561
+ return {
4562
+ kind: "event",
4563
+ event,
4564
+ sourceSchemaVersion,
4565
+ diagnostics,
4566
+ seenFields: /* @__PURE__ */ new Set(),
4567
+ rawPoses,
4568
+ inPositions: false,
4569
+ positionsIndent: null,
4570
+ activePose: null,
4571
+ poseIndent: null,
4572
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4573
+ };
4574
+ }
4106
4575
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
4107
4576
  if (tokens.length < 3) {
4108
4577
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -4159,6 +4628,9 @@
4159
4628
  case "relation":
4160
4629
  applyRelationField(section, tokens, line);
4161
4630
  return;
4631
+ case "event":
4632
+ applyEventField(section, indent, tokens, line);
4633
+ return;
4162
4634
  case "object":
4163
4635
  applyObjectField(section, indent, tokens, line);
4164
4636
  return;
@@ -4285,7 +4757,14 @@
4285
4757
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
4286
4758
  return;
4287
4759
  case "layers":
4288
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4760
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4761
+ return;
4762
+ case "events":
4763
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4764
+ line,
4765
+ column: tokens[0].column
4766
+ });
4767
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
4289
4768
  return;
4290
4769
  default:
4291
4770
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -4390,6 +4869,106 @@
4390
4869
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4391
4870
  }
4392
4871
  }
4872
+ function applyEventField(section, indent, tokens, line) {
4873
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4874
+ section.activePose = null;
4875
+ section.poseIndent = null;
4876
+ section.activePoseSeenFields.clear();
4877
+ }
4878
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4879
+ section.inPositions = false;
4880
+ section.positionsIndent = null;
4881
+ }
4882
+ if (section.activePose) {
4883
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4884
+ return;
4885
+ }
4886
+ if (section.inPositions) {
4887
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4888
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4889
+ }
4890
+ const objectId = tokens[1].value;
4891
+ if (!objectId.trim()) {
4892
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4893
+ }
4894
+ const rawPose = {
4895
+ objectId,
4896
+ fields: [],
4897
+ location: { line, column: tokens[0].column }
4898
+ };
4899
+ section.rawPoses.push(rawPose);
4900
+ section.activePose = rawPose;
4901
+ section.poseIndent = indent;
4902
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4903
+ return;
4904
+ }
4905
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4906
+ if (section.seenFields.has("positions")) {
4907
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4908
+ }
4909
+ section.seenFields.add("positions");
4910
+ section.inPositions = true;
4911
+ section.positionsIndent = indent;
4912
+ return;
4913
+ }
4914
+ const key = requireUniqueField(tokens, section.seenFields, line);
4915
+ switch (key) {
4916
+ case "kind":
4917
+ section.event.kind = joinFieldValue(tokens, line);
4918
+ return;
4919
+ case "label":
4920
+ section.event.label = joinFieldValue(tokens, line);
4921
+ return;
4922
+ case "summary":
4923
+ section.event.summary = joinFieldValue(tokens, line);
4924
+ return;
4925
+ case "target":
4926
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4927
+ return;
4928
+ case "participants":
4929
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4930
+ return;
4931
+ case "timing":
4932
+ section.event.timing = joinFieldValue(tokens, line);
4933
+ return;
4934
+ case "visibility":
4935
+ section.event.visibility = joinFieldValue(tokens, line);
4936
+ return;
4937
+ case "tags":
4938
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4939
+ return;
4940
+ case "color":
4941
+ section.event.color = joinFieldValue(tokens, line);
4942
+ return;
4943
+ case "hidden":
4944
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4945
+ line,
4946
+ column: tokens[0].column
4947
+ });
4948
+ return;
4949
+ default:
4950
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4951
+ }
4952
+ }
4953
+ function parseEventPoseField(tokens, line, seenFields) {
4954
+ if (tokens.length < 2) {
4955
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4956
+ }
4957
+ const key = tokens[0].value;
4958
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4959
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4960
+ }
4961
+ if (seenFields.has(key)) {
4962
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4963
+ }
4964
+ seenFields.add(key);
4965
+ return {
4966
+ type: "field",
4967
+ key,
4968
+ values: tokens.slice(1).map((token) => token.value),
4969
+ location: { line, column: tokens[0].column }
4970
+ };
4971
+ }
4393
4972
  function applyObjectField(section, indent, tokens, line) {
4394
4973
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4395
4974
  section.activeBlock = null;
@@ -4448,7 +5027,7 @@
4448
5027
  function parseObjectTypeTokens(tokens, line) {
4449
5028
  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");
4450
5029
  }
4451
- function parseLayerTokens(tokens, line) {
5030
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4452
5031
  const layers = {};
4453
5032
  for (const token of parseTokenList(tokens, line, "layers")) {
4454
5033
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -4458,7 +5037,13 @@
4458
5037
  layers["orbits-front"] = enabled;
4459
5038
  continue;
4460
5039
  }
4461
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
5040
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
5041
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
5042
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
5043
+ line,
5044
+ column: tokens[0]?.column ?? 1
5045
+ });
5046
+ }
4462
5047
  layers[raw] = enabled;
4463
5048
  }
4464
5049
  }
@@ -4597,7 +5182,7 @@
4597
5182
  }
4598
5183
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4599
5184
  const fieldMap = collectDraftFields(node.fields);
4600
- const placement = extractDraftPlacement(node.objectType, fieldMap);
5185
+ const placement = extractPlacementFromFieldMap(fieldMap);
4601
5186
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
4602
5187
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4603
5188
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -4649,6 +5234,24 @@
4649
5234
  }
4650
5235
  return object;
4651
5236
  }
5237
+ function normalizeDraftEvent(event, rawPoses) {
5238
+ return {
5239
+ ...event,
5240
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5241
+ tags: [...new Set(event.tags)],
5242
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5243
+ };
5244
+ }
5245
+ function normalizeDraftEventPose(rawPose) {
5246
+ const fieldMap = collectDraftFields(rawPose.fields);
5247
+ const placement = extractPlacementFromFieldMap(fieldMap);
5248
+ return {
5249
+ objectId: rawPose.objectId,
5250
+ placement,
5251
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5252
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
5253
+ };
5254
+ }
4652
5255
  function collectDraftFields(fields) {
4653
5256
  const grouped = /* @__PURE__ */ new Map();
4654
5257
  for (const field of fields) {
@@ -4665,7 +5268,7 @@
4665
5268
  }
4666
5269
  return grouped;
4667
5270
  }
4668
- function extractDraftPlacement(objectType, fieldMap) {
5271
+ function extractPlacementFromFieldMap(fieldMap) {
4669
5272
  const orbitField = fieldMap.get("orbit")?.[0];
4670
5273
  const atField = fieldMap.get("at")?.[0];
4671
5274
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4958,6 +5561,7 @@
4958
5561
  },
4959
5562
  groups: [],
4960
5563
  relations: [],
5564
+ events: [],
4961
5565
  objects: [],
4962
5566
  diagnostics: []
4963
5567
  };
@@ -4984,6 +5588,12 @@
4984
5588
  for (const relation of [...document.relations].sort(compareIdLike2)) {
4985
5589
  paths.push({ kind: "relation", id: relation.id });
4986
5590
  }
5591
+ for (const event of [...document.events].sort(compareIdLike2)) {
5592
+ paths.push({ kind: "event", id: event.id });
5593
+ for (const pose of [...event.positions].sort(comparePoseObjectId2)) {
5594
+ paths.push({ kind: "event-pose", id: event.id, key: pose.objectId });
5595
+ }
5596
+ }
4987
5597
  for (const object of [...document.objects].sort(compareIdLike2)) {
4988
5598
  paths.push({ kind: "object", id: object.id });
4989
5599
  }
@@ -4999,6 +5609,10 @@
4999
5609
  return path.key ? document.system?.atlasMetadata[path.key] ?? null : null;
5000
5610
  case "group":
5001
5611
  return path.id ? findGroup(document, path.id) : null;
5612
+ case "event":
5613
+ return path.id ? findEvent(document, path.id) : null;
5614
+ case "event-pose":
5615
+ return path.id && path.key ? findEventPose(document, path.id, path.key) : null;
5002
5616
  case "object":
5003
5617
  return path.id ? findObject(document, path.id) : null;
5004
5618
  case "viewpoint":
@@ -5038,6 +5652,18 @@
5038
5652
  }
5039
5653
  upsertById(next.groups, value);
5040
5654
  return next;
5655
+ case "event":
5656
+ if (!path.id) {
5657
+ throw new Error('Event updates require an "id" value.');
5658
+ }
5659
+ upsertById(next.events, value);
5660
+ return next;
5661
+ case "event-pose":
5662
+ if (!path.id || !path.key) {
5663
+ throw new Error('Event pose updates require an event "id" and pose "key" value.');
5664
+ }
5665
+ upsertEventPose(next.events, path.id, value);
5666
+ return next;
5041
5667
  case "object":
5042
5668
  if (!path.id) {
5043
5669
  throw new Error('Object updates require an "id" value.');
@@ -5086,6 +5712,19 @@
5086
5712
  next.groups = next.groups.filter((group) => group.id !== path.id);
5087
5713
  }
5088
5714
  return next;
5715
+ case "event":
5716
+ if (path.id) {
5717
+ next.events = next.events.filter((event) => event.id !== path.id);
5718
+ }
5719
+ return next;
5720
+ case "event-pose":
5721
+ if (path.id && path.key) {
5722
+ const event = findEvent(next, path.id);
5723
+ if (event) {
5724
+ event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
5725
+ }
5726
+ }
5727
+ return next;
5089
5728
  case "viewpoint":
5090
5729
  if (path.id) {
5091
5730
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -5154,6 +5793,22 @@
5154
5793
  };
5155
5794
  }
5156
5795
  }
5796
+ if (diagnostic.field?.startsWith("event.")) {
5797
+ const parts = diagnostic.field.split(".");
5798
+ if (parts[1] && findEvent(document, parts[1])) {
5799
+ if (parts[2] === "pose" && parts[3] && findEventPose(document, parts[1], parts[3])) {
5800
+ return {
5801
+ kind: "event-pose",
5802
+ id: parts[1],
5803
+ key: parts[3]
5804
+ };
5805
+ }
5806
+ return {
5807
+ kind: "event",
5808
+ id: parts[1]
5809
+ };
5810
+ }
5811
+ }
5157
5812
  if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
5158
5813
  return {
5159
5814
  kind: "metadata",
@@ -5185,6 +5840,12 @@
5185
5840
  function findRelation(document, relationId) {
5186
5841
  return document.relations.find((relation) => relation.id === relationId) ?? null;
5187
5842
  }
5843
+ function findEvent(document, eventId) {
5844
+ return document.events.find((event) => event.id === eventId) ?? null;
5845
+ }
5846
+ function findEventPose(document, eventId, objectId) {
5847
+ return findEvent(document, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
5848
+ }
5188
5849
  function findViewpoint(system, viewpointId) {
5189
5850
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
5190
5851
  }
@@ -5200,9 +5861,25 @@
5200
5861
  }
5201
5862
  items[index] = value;
5202
5863
  }
5864
+ function upsertEventPose(events, eventId, value) {
5865
+ const event = events.find((entry) => entry.id === eventId);
5866
+ if (!event) {
5867
+ throw new Error(`Unknown event "${eventId}" for pose update.`);
5868
+ }
5869
+ const index = event.positions.findIndex((entry) => entry.objectId === value.objectId);
5870
+ if (index === -1) {
5871
+ event.positions.push(value);
5872
+ event.positions.sort(comparePoseObjectId2);
5873
+ return;
5874
+ }
5875
+ event.positions[index] = value;
5876
+ }
5203
5877
  function compareIdLike2(left, right) {
5204
5878
  return left.id.localeCompare(right.id);
5205
5879
  }
5880
+ function comparePoseObjectId2(left, right) {
5881
+ return left.objectId.localeCompare(right.objectId);
5882
+ }
5206
5883
 
5207
5884
  // packages/core/dist/load.js
5208
5885
  var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;