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
@@ -568,6 +568,7 @@ var WorldOrbit = (() => {
568
568
  system,
569
569
  groups: [],
570
570
  relations: [],
571
+ events: [],
571
572
  objects
572
573
  };
573
574
  }
@@ -951,8 +952,10 @@ var WorldOrbit = (() => {
951
952
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
952
953
  const spacingFactor = layoutPresetSpacing(layoutPreset);
953
954
  const systemId = document2.system?.id ?? null;
954
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
955
- const relationships = buildSceneRelationships(document2.objects, objectMap);
955
+ const activeEventId = options.activeEventId ?? null;
956
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
957
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
958
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
956
959
  const positions = /* @__PURE__ */ new Map();
957
960
  const orbitDrafts = [];
958
961
  const leaderDrafts = [];
@@ -961,7 +964,7 @@ var WorldOrbit = (() => {
961
964
  const atObjects = [];
962
965
  const surfaceChildren = /* @__PURE__ */ new Map();
963
966
  const orbitChildren = /* @__PURE__ */ new Map();
964
- for (const object of document2.objects) {
967
+ for (const object of effectiveObjects) {
965
968
  const placement = object.placement;
966
969
  if (!placement) {
967
970
  rootObjects.push(object);
@@ -1056,13 +1059,14 @@ var WorldOrbit = (() => {
1056
1059
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1057
1060
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1058
1061
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1059
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1062
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1060
1063
  const relations = createSceneRelations(document2, objects);
1061
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1062
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1064
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1065
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1066
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1063
1067
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1064
1068
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1065
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1069
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1066
1070
  return {
1067
1071
  width,
1068
1072
  height,
@@ -1088,6 +1092,8 @@ var WorldOrbit = (() => {
1088
1092
  groups,
1089
1093
  semanticGroups,
1090
1094
  viewpoints,
1095
+ events,
1096
+ activeEventId,
1091
1097
  objects,
1092
1098
  orbitVisuals,
1093
1099
  relations,
@@ -1095,6 +1101,35 @@ var WorldOrbit = (() => {
1095
1101
  labels
1096
1102
  };
1097
1103
  }
1104
+ function createEffectiveObjects(objects, events, activeEventId) {
1105
+ const cloned = objects.map((object) => structuredClone(object));
1106
+ if (!activeEventId) {
1107
+ return cloned;
1108
+ }
1109
+ const activeEvent = events.find((event) => event.id === activeEventId);
1110
+ if (!activeEvent) {
1111
+ return cloned;
1112
+ }
1113
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1114
+ for (const pose of activeEvent.positions) {
1115
+ const object = objectMap.get(pose.objectId);
1116
+ if (!object) {
1117
+ continue;
1118
+ }
1119
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1120
+ if (pose.inner) {
1121
+ object.properties.inner = { ...pose.inner };
1122
+ } else {
1123
+ delete object.properties.inner;
1124
+ }
1125
+ if (pose.outer) {
1126
+ object.properties.outer = { ...pose.outer };
1127
+ } else {
1128
+ delete object.properties.outer;
1129
+ }
1130
+ }
1131
+ return cloned;
1132
+ }
1098
1133
  function resolveLayoutPreset(document2) {
1099
1134
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1100
1135
  switch (rawScale) {
@@ -1250,24 +1285,14 @@ var WorldOrbit = (() => {
1250
1285
  hidden: draft.object.properties.hidden === true
1251
1286
  };
1252
1287
  }
1253
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1288
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1254
1289
  const labels = [];
1255
1290
  const occupied = [];
1256
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1291
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1292
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1257
1293
  for (const object of visibleObjects) {
1258
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1259
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1260
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1261
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1262
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1263
- let attempts = 0;
1264
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1265
- labelY += direction * 14 * labelMultiplier;
1266
- secondaryY += direction * 14 * labelMultiplier;
1267
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1268
- attempts += 1;
1269
- }
1270
- occupied.push(bounds);
1294
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1295
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1271
1296
  labels.push({
1272
1297
  renderId: `${object.renderId}-label`,
1273
1298
  objectId: object.objectId,
@@ -1276,17 +1301,128 @@ var WorldOrbit = (() => {
1276
1301
  semanticGroupIds: [...object.semanticGroupIds],
1277
1302
  label: object.label,
1278
1303
  secondaryLabel: object.secondaryLabel,
1279
- x: object.x,
1280
- y: labelY,
1281
- secondaryY,
1282
- textAnchor: "middle",
1283
- direction: direction < 0 ? "above" : "below",
1304
+ x: placement.x,
1305
+ y: placement.labelY,
1306
+ secondaryY: placement.secondaryY,
1307
+ textAnchor: placement.textAnchor,
1308
+ direction: placement.direction,
1284
1309
  hidden: object.hidden
1285
1310
  });
1286
1311
  }
1287
1312
  return labels;
1288
1313
  }
1289
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1314
+ function compareLabelPlacementOrder(left, right) {
1315
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1316
+ if (priorityDiff !== 0) {
1317
+ return priorityDiff;
1318
+ }
1319
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1320
+ if (renderPriorityDiff !== 0) {
1321
+ return renderPriorityDiff;
1322
+ }
1323
+ return left.sortKey - right.sortKey;
1324
+ }
1325
+ function labelPlacementPriority(object) {
1326
+ switch (object.object.type) {
1327
+ case "star":
1328
+ return 0;
1329
+ case "planet":
1330
+ return 1;
1331
+ case "moon":
1332
+ return 2;
1333
+ case "belt":
1334
+ case "ring":
1335
+ return 3;
1336
+ case "asteroid":
1337
+ case "comet":
1338
+ return 4;
1339
+ case "structure":
1340
+ case "phenomenon":
1341
+ return 5;
1342
+ }
1343
+ }
1344
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1345
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1346
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1347
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1348
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1349
+ const rect = createLabelRect(object, placement, labelMultiplier);
1350
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1351
+ return placement;
1352
+ }
1353
+ }
1354
+ }
1355
+ return null;
1356
+ }
1357
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1358
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1359
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1360
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1361
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1362
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1363
+ 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";
1364
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1365
+ }
1366
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1367
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1368
+ return object.y >= parent.y ? "below" : "above";
1369
+ }
1370
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1371
+ }
1372
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1373
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1374
+ return object.x >= parent.x ? "right" : "left";
1375
+ }
1376
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1377
+ }
1378
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1379
+ const step = 14 * labelMultiplier;
1380
+ switch (direction) {
1381
+ case "above": {
1382
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1383
+ return {
1384
+ x: object.x,
1385
+ labelY,
1386
+ secondaryY: labelY - 16 * labelMultiplier,
1387
+ textAnchor: "middle",
1388
+ direction
1389
+ };
1390
+ }
1391
+ case "below": {
1392
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1393
+ return {
1394
+ x: object.x,
1395
+ labelY,
1396
+ secondaryY: labelY + 16 * labelMultiplier,
1397
+ textAnchor: "middle",
1398
+ direction
1399
+ };
1400
+ }
1401
+ case "left": {
1402
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1403
+ const labelY = object.y - 4 * labelMultiplier;
1404
+ return {
1405
+ x,
1406
+ labelY,
1407
+ secondaryY: labelY + 16 * labelMultiplier,
1408
+ textAnchor: "end",
1409
+ direction
1410
+ };
1411
+ }
1412
+ case "right": {
1413
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1414
+ const labelY = object.y - 4 * labelMultiplier;
1415
+ return {
1416
+ x,
1417
+ labelY,
1418
+ secondaryY: labelY + 16 * labelMultiplier,
1419
+ textAnchor: "start",
1420
+ direction
1421
+ };
1422
+ }
1423
+ }
1424
+ }
1425
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1290
1426
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1291
1427
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1292
1428
  return [
@@ -1301,6 +1437,10 @@ var WorldOrbit = (() => {
1301
1437
  id: "relations",
1302
1438
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1303
1439
  },
1440
+ {
1441
+ id: "events",
1442
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1443
+ },
1304
1444
  {
1305
1445
  id: "objects",
1306
1446
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1312,7 +1452,7 @@ var WorldOrbit = (() => {
1312
1452
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1313
1453
  ];
1314
1454
  }
1315
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1455
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1316
1456
  const groups = /* @__PURE__ */ new Map();
1317
1457
  const ensureGroup = (groupId) => {
1318
1458
  if (!groupId) {
@@ -1361,7 +1501,7 @@ var WorldOrbit = (() => {
1361
1501
  }
1362
1502
  }
1363
1503
  for (const group of groups.values()) {
1364
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1504
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1365
1505
  }
1366
1506
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1367
1507
  }
@@ -1395,6 +1535,29 @@ var WorldOrbit = (() => {
1395
1535
  };
1396
1536
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1397
1537
  }
1538
+ function createSceneEvents(events, objects, activeEventId) {
1539
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1540
+ return events.map((event) => {
1541
+ const objectIds = [.../* @__PURE__ */ new Set([
1542
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1543
+ ...event.participantObjectIds
1544
+ ])];
1545
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1546
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1547
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1548
+ return {
1549
+ renderId: `${createRenderId(event.id)}-event`,
1550
+ eventId: event.id,
1551
+ event,
1552
+ objectIds,
1553
+ participantIds: [...event.participantObjectIds],
1554
+ targetObjectId: event.targetObjectId,
1555
+ x: centroidX,
1556
+ y: centroidY,
1557
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1558
+ };
1559
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1560
+ }
1398
1561
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1399
1562
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1400
1563
  const drafts = /* @__PURE__ */ new Map();
@@ -1448,6 +1611,7 @@ var WorldOrbit = (() => {
1448
1611
  summary: "Fit the whole system with the current atlas defaults.",
1449
1612
  objectId: null,
1450
1613
  selectedObjectId: null,
1614
+ eventIds: [],
1451
1615
  projection,
1452
1616
  preset,
1453
1617
  rotationDeg: 0,
@@ -1484,6 +1648,9 @@ var WorldOrbit = (() => {
1484
1648
  draft.select = normalizedValue;
1485
1649
  }
1486
1650
  return;
1651
+ case "events":
1652
+ draft.eventIds = splitListValue(normalizedValue);
1653
+ return;
1487
1654
  case "projection":
1488
1655
  case "view":
1489
1656
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1540,6 +1707,7 @@ var WorldOrbit = (() => {
1540
1707
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1541
1708
  objectId,
1542
1709
  selectedObjectId,
1710
+ eventIds: [...new Set(draft.eventIds ?? [])],
1543
1711
  projection: draft.projection ?? projection,
1544
1712
  preset: draft.preset ?? preset,
1545
1713
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1597,7 +1765,7 @@ var WorldOrbit = (() => {
1597
1765
  next["orbits-front"] = enabled;
1598
1766
  continue;
1599
1767
  }
1600
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1768
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1601
1769
  next[rawLayer] = enabled;
1602
1770
  }
1603
1771
  }
@@ -1645,7 +1813,7 @@ var WorldOrbit = (() => {
1645
1813
  }
1646
1814
  return parts.join(" - ");
1647
1815
  }
1648
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1816
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1649
1817
  let minX = Number.POSITIVE_INFINITY;
1650
1818
  let minY = Number.POSITIVE_INFINITY;
1651
1819
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1675,7 +1843,7 @@ var WorldOrbit = (() => {
1675
1843
  for (const label of labels) {
1676
1844
  if (label.hidden)
1677
1845
  continue;
1678
- includeLabelBounds(label, include);
1846
+ includeLabelBounds(label, include, labelMultiplier);
1679
1847
  }
1680
1848
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1681
1849
  return createBounds(0, 0, width, height);
@@ -1713,13 +1881,10 @@ var WorldOrbit = (() => {
1713
1881
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1714
1882
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1715
1883
  }
1716
- function includeLabelBounds(label, include) {
1717
- const labelScale = 1;
1718
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1719
- include(label.x - labelHalfWidth, label.y - 18);
1720
- include(label.x + labelHalfWidth, label.y + 8);
1721
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1722
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1884
+ function includeLabelBounds(label, include, labelMultiplier) {
1885
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1886
+ include(bounds.left, bounds.top);
1887
+ include(bounds.right, bounds.bottom);
1723
1888
  }
1724
1889
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1725
1890
  if (positions.has(object.id)) {
@@ -2109,7 +2274,7 @@ var WorldOrbit = (() => {
2109
2274
  return null;
2110
2275
  }
2111
2276
  }
2112
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2277
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2113
2278
  let minX = Number.POSITIVE_INFINITY;
2114
2279
  let minY = Number.POSITIVE_INFINITY;
2115
2280
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2138,7 +2303,7 @@ var WorldOrbit = (() => {
2138
2303
  }
2139
2304
  for (const label of labels) {
2140
2305
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2141
- includeLabelBounds(label, include);
2306
+ includeLabelBounds(label, include, labelMultiplier);
2142
2307
  }
2143
2308
  }
2144
2309
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2163,12 +2328,28 @@ var WorldOrbit = (() => {
2163
2328
  }
2164
2329
  return current.id;
2165
2330
  }
2166
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2331
+ function createLabelRect(object, placement, labelMultiplier) {
2332
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2333
+ }
2334
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2335
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2336
+ const labelWidth = labelHalfWidth * 2;
2337
+ const topPadding = direction === "above" ? 18 : 12;
2338
+ const bottomPadding = direction === "above" ? 8 : 12;
2339
+ let left = x - labelHalfWidth;
2340
+ let right = x + labelHalfWidth;
2341
+ if (textAnchor === "start") {
2342
+ left = x;
2343
+ right = x + labelWidth;
2344
+ } else if (textAnchor === "end") {
2345
+ left = x - labelWidth;
2346
+ right = x;
2347
+ }
2167
2348
  return {
2168
- left: x - labelHalfWidth,
2169
- right: x + labelHalfWidth,
2170
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2171
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2349
+ left,
2350
+ right,
2351
+ top: Math.min(labelY, secondaryY) - topPadding,
2352
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2172
2353
  };
2173
2354
  }
2174
2355
  function rectsOverlap(left, right) {
@@ -2355,11 +2536,6 @@ var WorldOrbit = (() => {
2355
2536
  function customColorFor(value) {
2356
2537
  return typeof value === "string" && value.trim() ? value : void 0;
2357
2538
  }
2358
- function estimateLabelHalfWidth(object, labelMultiplier) {
2359
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2360
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2361
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2362
- }
2363
2539
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2364
2540
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2365
2541
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2376,7 +2552,7 @@ var WorldOrbit = (() => {
2376
2552
  }
2377
2553
 
2378
2554
  // packages/core/dist/draft.js
2379
- function materializeAtlasDocument(document2) {
2555
+ function materializeAtlasDocument(document2, options = {}) {
2380
2556
  const system = document2.system ? {
2381
2557
  type: "system",
2382
2558
  id: document2.system.id,
@@ -2387,6 +2563,8 @@ var WorldOrbit = (() => {
2387
2563
  properties: materializeDraftSystemProperties(document2.system),
2388
2564
  info: materializeDraftSystemInfo(document2.system)
2389
2565
  } : null;
2566
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2567
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2390
2568
  return {
2391
2569
  format: "worldorbit",
2392
2570
  version: "1.0",
@@ -2394,7 +2572,8 @@ var WorldOrbit = (() => {
2394
2572
  system,
2395
2573
  groups: structuredClone(document2.groups ?? []),
2396
2574
  relations: structuredClone(document2.relations ?? []),
2397
- objects: document2.objects.map(cloneWorldOrbitObject)
2575
+ events: document2.events.map(cloneWorldOrbitEvent),
2576
+ objects
2398
2577
  };
2399
2578
  }
2400
2579
  function cloneWorldOrbitObject(object) {
@@ -2416,6 +2595,52 @@ var WorldOrbit = (() => {
2416
2595
  info: { ...object.info }
2417
2596
  };
2418
2597
  }
2598
+ function cloneWorldOrbitEvent(event) {
2599
+ return {
2600
+ ...event,
2601
+ participantObjectIds: [...event.participantObjectIds],
2602
+ tags: [...event.tags],
2603
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2604
+ };
2605
+ }
2606
+ function cloneWorldOrbitEventPose(pose) {
2607
+ return {
2608
+ objectId: pose.objectId,
2609
+ placement: clonePlacement(pose.placement),
2610
+ inner: pose.inner ? { ...pose.inner } : void 0,
2611
+ outer: pose.outer ? { ...pose.outer } : void 0
2612
+ };
2613
+ }
2614
+ function clonePlacement(placement) {
2615
+ return placement ? structuredClone(placement) : null;
2616
+ }
2617
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2618
+ if (!activeEventId) {
2619
+ return;
2620
+ }
2621
+ const event = events.find((entry) => entry.id === activeEventId);
2622
+ if (!event) {
2623
+ return;
2624
+ }
2625
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2626
+ for (const pose of event.positions) {
2627
+ const object = objectMap.get(pose.objectId);
2628
+ if (!object) {
2629
+ continue;
2630
+ }
2631
+ object.placement = clonePlacement(pose.placement);
2632
+ if (pose.inner) {
2633
+ object.properties.inner = { ...pose.inner };
2634
+ } else {
2635
+ delete object.properties.inner;
2636
+ }
2637
+ if (pose.outer) {
2638
+ object.properties.outer = { ...pose.outer };
2639
+ } else {
2640
+ delete object.properties.outer;
2641
+ }
2642
+ }
2643
+ }
2419
2644
  function cloneProperties(properties) {
2420
2645
  const next = {};
2421
2646
  for (const [key, value] of Object.entries(properties)) {
@@ -2504,6 +2729,9 @@ var WorldOrbit = (() => {
2504
2729
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2505
2730
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2506
2731
  }
2732
+ if (viewpoint.events.length > 0) {
2733
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2734
+ }
2507
2735
  }
2508
2736
  for (const annotation of system.annotations) {
2509
2737
  const prefix = `annotation.${annotation.id}`;
@@ -2528,7 +2756,7 @@ var WorldOrbit = (() => {
2528
2756
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2529
2757
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2530
2758
  }
2531
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2759
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2532
2760
  if (layers[key] !== void 0) {
2533
2761
  tokens.push(layers[key] ? key : `-${key}`);
2534
2762
  }
@@ -2702,6 +2930,7 @@ var WorldOrbit = (() => {
2702
2930
  const diagnostics = [];
2703
2931
  const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
2704
2932
  const groupIds = new Set(document2.groups.map((group) => group.id));
2933
+ const eventIds = new Set(document2.events.map((event) => event.id));
2705
2934
  if (!document2.system) {
2706
2935
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
2707
2936
  }
@@ -2711,6 +2940,7 @@ var WorldOrbit = (() => {
2711
2940
  ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
2712
2941
  ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
2713
2942
  ["relation", document2.relations.map((relation) => relation.id)],
2943
+ ["event", document2.events.map((event) => event.id)],
2714
2944
  ["object", document2.objects.map((object) => object.id)]
2715
2945
  ]) {
2716
2946
  for (const id of ids) {
@@ -2726,11 +2956,14 @@ var WorldOrbit = (() => {
2726
2956
  validateRelation(relation, objectMap, diagnostics);
2727
2957
  }
2728
2958
  for (const viewpoint of document2.system?.viewpoints ?? []) {
2729
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
2959
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
2730
2960
  }
2731
2961
  for (const object of document2.objects) {
2732
2962
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
2733
2963
  }
2964
+ for (const event of document2.events) {
2965
+ validateEvent(event, objectMap, diagnostics);
2966
+ }
2734
2967
  return diagnostics;
2735
2968
  }
2736
2969
  function validateRelation(relation, objectMap, diagnostics) {
@@ -2748,13 +2981,19 @@ var WorldOrbit = (() => {
2748
2981
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
2749
2982
  }
2750
2983
  }
2751
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
2752
- if (!filter || sourceSchemaVersion !== "2.1") {
2753
- return;
2754
- }
2755
- for (const groupId of filter.groupIds) {
2756
- if (!groupIds.has(groupId)) {
2757
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
2984
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
2985
+ if (sourceSchemaVersion === "2.1") {
2986
+ if (filter) {
2987
+ for (const groupId of filter.groupIds) {
2988
+ if (!groupIds.has(groupId)) {
2989
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
2990
+ }
2991
+ }
2992
+ }
2993
+ for (const eventId of eventRefs) {
2994
+ if (!eventIds.has(eventId)) {
2995
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
2996
+ }
2758
2997
  }
2759
2998
  }
2760
2999
  }
@@ -2840,6 +3079,103 @@ var WorldOrbit = (() => {
2840
3079
  }
2841
3080
  }
2842
3081
  }
3082
+ function validateEvent(event, objectMap, diagnostics) {
3083
+ const fieldPrefix = `event.${event.id}`;
3084
+ const referencedIds = /* @__PURE__ */ new Set();
3085
+ if (!event.kind.trim()) {
3086
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3087
+ }
3088
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3089
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3090
+ }
3091
+ if (event.targetObjectId) {
3092
+ referencedIds.add(event.targetObjectId);
3093
+ if (!objectMap.has(event.targetObjectId)) {
3094
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3095
+ }
3096
+ }
3097
+ const seenParticipants = /* @__PURE__ */ new Set();
3098
+ for (const participantId of event.participantObjectIds) {
3099
+ referencedIds.add(participantId);
3100
+ if (seenParticipants.has(participantId)) {
3101
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3102
+ continue;
3103
+ }
3104
+ seenParticipants.add(participantId);
3105
+ if (!objectMap.has(participantId)) {
3106
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3107
+ }
3108
+ }
3109
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3110
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3111
+ }
3112
+ if (event.positions.length === 0) {
3113
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3114
+ }
3115
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3116
+ 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`));
3117
+ }
3118
+ const poseIds = /* @__PURE__ */ new Set();
3119
+ for (const pose of event.positions) {
3120
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3121
+ if (poseIds.has(pose.objectId)) {
3122
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3123
+ continue;
3124
+ }
3125
+ poseIds.add(pose.objectId);
3126
+ const object = objectMap.get(pose.objectId);
3127
+ if (!object) {
3128
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3129
+ continue;
3130
+ }
3131
+ if (!referencedIds.has(pose.objectId)) {
3132
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3133
+ }
3134
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3135
+ }
3136
+ }
3137
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3138
+ const placement = pose.placement;
3139
+ if (!placement) {
3140
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3141
+ return;
3142
+ }
3143
+ if (placement.mode === "orbit") {
3144
+ if (!objectMap.has(placement.target)) {
3145
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3146
+ }
3147
+ if (placement.distance && placement.semiMajor) {
3148
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3149
+ }
3150
+ return;
3151
+ }
3152
+ if (placement.mode === "surface") {
3153
+ const target = objectMap.get(placement.target);
3154
+ if (!target) {
3155
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3156
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3157
+ 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`));
3158
+ }
3159
+ return;
3160
+ }
3161
+ if (placement.mode === "at") {
3162
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3163
+ 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`));
3164
+ }
3165
+ const reference = placement.reference;
3166
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3167
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3168
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3169
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3170
+ } else if (reference.kind === "lagrange") {
3171
+ if (!objectMap.has(reference.primary)) {
3172
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3173
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3174
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3175
+ }
3176
+ }
3177
+ }
3178
+ }
2843
3179
  function validateAtTarget(object, objectMap, diagnostics) {
2844
3180
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
2845
3181
  if (!reference) {
@@ -3040,6 +3376,21 @@ var WorldOrbit = (() => {
3040
3376
  });
3041
3377
  }
3042
3378
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3379
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3380
+ "orbit",
3381
+ "distance",
3382
+ "semiMajor",
3383
+ "eccentricity",
3384
+ "period",
3385
+ "angle",
3386
+ "inclination",
3387
+ "phase",
3388
+ "at",
3389
+ "surface",
3390
+ "free",
3391
+ "inner",
3392
+ "outer"
3393
+ ]);
3043
3394
  function parseWorldOrbitAtlas(source) {
3044
3395
  return parseAtlasSource(source);
3045
3396
  }
@@ -3054,12 +3405,15 @@ var WorldOrbit = (() => {
3054
3405
  const objectNodes = [];
3055
3406
  const groups = [];
3056
3407
  const relations = [];
3408
+ const events = [];
3409
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3057
3410
  let sawDefaults = false;
3058
3411
  let sawAtlas = false;
3059
3412
  const viewpointIds = /* @__PURE__ */ new Set();
3060
3413
  const annotationIds = /* @__PURE__ */ new Set();
3061
3414
  const groupIds = /* @__PURE__ */ new Set();
3062
3415
  const relationIds = /* @__PURE__ */ new Set();
3416
+ const eventIds = /* @__PURE__ */ new Set();
3063
3417
  for (let index = 0; index < lines.length; index++) {
3064
3418
  const rawLine = lines[index];
3065
3419
  const lineNumber = index + 1;
@@ -3090,7 +3444,7 @@ var WorldOrbit = (() => {
3090
3444
  continue;
3091
3445
  }
3092
3446
  if (indent === 0) {
3093
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
3447
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3094
3448
  if (section.kind === "system") {
3095
3449
  system = section.system;
3096
3450
  } else if (section.kind === "defaults") {
@@ -3109,6 +3463,7 @@ var WorldOrbit = (() => {
3109
3463
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3110
3464
  }
3111
3465
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3466
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3112
3467
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3113
3468
  const baseDocument = {
3114
3469
  format: "worldorbit",
@@ -3116,6 +3471,7 @@ var WorldOrbit = (() => {
3116
3471
  system,
3117
3472
  groups,
3118
3473
  relations,
3474
+ events: normalizedEvents,
3119
3475
  objects,
3120
3476
  diagnostics
3121
3477
  };
@@ -3151,7 +3507,7 @@ var WorldOrbit = (() => {
3151
3507
  const version = tokens[1].value.toLowerCase();
3152
3508
  return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3153
3509
  }
3154
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
3510
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3155
3511
  const keyword = tokens[0]?.value.toLowerCase();
3156
3512
  switch (keyword) {
3157
3513
  case "system":
@@ -3188,7 +3544,7 @@ var WorldOrbit = (() => {
3188
3544
  if (!system) {
3189
3545
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3190
3546
  }
3191
- return startViewpointSection(tokens, line, system, viewpointIds);
3547
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3192
3548
  case "annotation":
3193
3549
  if (!system) {
3194
3550
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3200,6 +3556,9 @@ var WorldOrbit = (() => {
3200
3556
  case "relation":
3201
3557
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3202
3558
  return startRelationSection(tokens, line, relations, relationIds);
3559
+ case "event":
3560
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
3561
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3203
3562
  case "object":
3204
3563
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3205
3564
  default:
@@ -3236,7 +3595,7 @@ var WorldOrbit = (() => {
3236
3595
  seenFields: /* @__PURE__ */ new Set()
3237
3596
  };
3238
3597
  }
3239
- function startViewpointSection(tokens, line, system, viewpointIds) {
3598
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3240
3599
  if (tokens.length !== 2) {
3241
3600
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3242
3601
  }
@@ -3253,6 +3612,7 @@ var WorldOrbit = (() => {
3253
3612
  summary: "",
3254
3613
  focusObjectId: null,
3255
3614
  selectedObjectId: null,
3615
+ events: [],
3256
3616
  projection: system.defaults.view,
3257
3617
  preset: system.defaults.preset,
3258
3618
  zoom: null,
@@ -3265,6 +3625,8 @@ var WorldOrbit = (() => {
3265
3625
  return {
3266
3626
  kind: "viewpoint",
3267
3627
  viewpoint,
3628
+ sourceSchemaVersion,
3629
+ diagnostics,
3268
3630
  seenFields: /* @__PURE__ */ new Set(),
3269
3631
  inFilter: false,
3270
3632
  filterIndent: null,
@@ -3355,6 +3717,49 @@ var WorldOrbit = (() => {
3355
3717
  seenFields: /* @__PURE__ */ new Set()
3356
3718
  };
3357
3719
  }
3720
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
3721
+ if (tokens.length !== 2) {
3722
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
3723
+ }
3724
+ const id = normalizeIdentifier(tokens[1].value);
3725
+ if (!id) {
3726
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
3727
+ }
3728
+ if (eventIds.has(id)) {
3729
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
3730
+ }
3731
+ const event = {
3732
+ id,
3733
+ kind: "",
3734
+ label: humanizeIdentifier2(id),
3735
+ summary: null,
3736
+ targetObjectId: null,
3737
+ participantObjectIds: [],
3738
+ timing: null,
3739
+ visibility: null,
3740
+ tags: [],
3741
+ color: null,
3742
+ hidden: false,
3743
+ positions: []
3744
+ };
3745
+ const rawPoses = [];
3746
+ events.push(event);
3747
+ eventPoseNodes.set(id, rawPoses);
3748
+ eventIds.add(id);
3749
+ return {
3750
+ kind: "event",
3751
+ event,
3752
+ sourceSchemaVersion,
3753
+ diagnostics,
3754
+ seenFields: /* @__PURE__ */ new Set(),
3755
+ rawPoses,
3756
+ inPositions: false,
3757
+ positionsIndent: null,
3758
+ activePose: null,
3759
+ poseIndent: null,
3760
+ activePoseSeenFields: /* @__PURE__ */ new Set()
3761
+ };
3762
+ }
3358
3763
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3359
3764
  if (tokens.length < 3) {
3360
3765
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -3411,6 +3816,9 @@ var WorldOrbit = (() => {
3411
3816
  case "relation":
3412
3817
  applyRelationField(section, tokens, line);
3413
3818
  return;
3819
+ case "event":
3820
+ applyEventField(section, indent, tokens, line);
3821
+ return;
3414
3822
  case "object":
3415
3823
  applyObjectField(section, indent, tokens, line);
3416
3824
  return;
@@ -3537,7 +3945,14 @@ var WorldOrbit = (() => {
3537
3945
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3538
3946
  return;
3539
3947
  case "layers":
3540
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
3948
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
3949
+ return;
3950
+ case "events":
3951
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
3952
+ line,
3953
+ column: tokens[0].column
3954
+ });
3955
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3541
3956
  return;
3542
3957
  default:
3543
3958
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -3642,6 +4057,106 @@ var WorldOrbit = (() => {
3642
4057
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
3643
4058
  }
3644
4059
  }
4060
+ function applyEventField(section, indent, tokens, line) {
4061
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4062
+ section.activePose = null;
4063
+ section.poseIndent = null;
4064
+ section.activePoseSeenFields.clear();
4065
+ }
4066
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4067
+ section.inPositions = false;
4068
+ section.positionsIndent = null;
4069
+ }
4070
+ if (section.activePose) {
4071
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4072
+ return;
4073
+ }
4074
+ if (section.inPositions) {
4075
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4076
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4077
+ }
4078
+ const objectId = tokens[1].value;
4079
+ if (!objectId.trim()) {
4080
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4081
+ }
4082
+ const rawPose = {
4083
+ objectId,
4084
+ fields: [],
4085
+ location: { line, column: tokens[0].column }
4086
+ };
4087
+ section.rawPoses.push(rawPose);
4088
+ section.activePose = rawPose;
4089
+ section.poseIndent = indent;
4090
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4091
+ return;
4092
+ }
4093
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4094
+ if (section.seenFields.has("positions")) {
4095
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4096
+ }
4097
+ section.seenFields.add("positions");
4098
+ section.inPositions = true;
4099
+ section.positionsIndent = indent;
4100
+ return;
4101
+ }
4102
+ const key = requireUniqueField(tokens, section.seenFields, line);
4103
+ switch (key) {
4104
+ case "kind":
4105
+ section.event.kind = joinFieldValue(tokens, line);
4106
+ return;
4107
+ case "label":
4108
+ section.event.label = joinFieldValue(tokens, line);
4109
+ return;
4110
+ case "summary":
4111
+ section.event.summary = joinFieldValue(tokens, line);
4112
+ return;
4113
+ case "target":
4114
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4115
+ return;
4116
+ case "participants":
4117
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4118
+ return;
4119
+ case "timing":
4120
+ section.event.timing = joinFieldValue(tokens, line);
4121
+ return;
4122
+ case "visibility":
4123
+ section.event.visibility = joinFieldValue(tokens, line);
4124
+ return;
4125
+ case "tags":
4126
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4127
+ return;
4128
+ case "color":
4129
+ section.event.color = joinFieldValue(tokens, line);
4130
+ return;
4131
+ case "hidden":
4132
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4133
+ line,
4134
+ column: tokens[0].column
4135
+ });
4136
+ return;
4137
+ default:
4138
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4139
+ }
4140
+ }
4141
+ function parseEventPoseField(tokens, line, seenFields) {
4142
+ if (tokens.length < 2) {
4143
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4144
+ }
4145
+ const key = tokens[0].value;
4146
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4147
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4148
+ }
4149
+ if (seenFields.has(key)) {
4150
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4151
+ }
4152
+ seenFields.add(key);
4153
+ return {
4154
+ type: "field",
4155
+ key,
4156
+ values: tokens.slice(1).map((token) => token.value),
4157
+ location: { line, column: tokens[0].column }
4158
+ };
4159
+ }
3645
4160
  function applyObjectField(section, indent, tokens, line) {
3646
4161
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
3647
4162
  section.activeBlock = null;
@@ -3700,7 +4215,7 @@ var WorldOrbit = (() => {
3700
4215
  function parseObjectTypeTokens(tokens, line) {
3701
4216
  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");
3702
4217
  }
3703
- function parseLayerTokens(tokens, line) {
4218
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
3704
4219
  const layers = {};
3705
4220
  for (const token of parseTokenList(tokens, line, "layers")) {
3706
4221
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -3710,7 +4225,13 @@ var WorldOrbit = (() => {
3710
4225
  layers["orbits-front"] = enabled;
3711
4226
  continue;
3712
4227
  }
3713
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4228
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4229
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4230
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4231
+ line,
4232
+ column: tokens[0]?.column ?? 1
4233
+ });
4234
+ }
3714
4235
  layers[raw] = enabled;
3715
4236
  }
3716
4237
  }
@@ -3849,7 +4370,7 @@ var WorldOrbit = (() => {
3849
4370
  }
3850
4371
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
3851
4372
  const fieldMap = collectDraftFields(node.fields);
3852
- const placement = extractDraftPlacement(node.objectType, fieldMap);
4373
+ const placement = extractPlacementFromFieldMap(fieldMap);
3853
4374
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
3854
4375
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
3855
4376
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -3901,6 +4422,24 @@ var WorldOrbit = (() => {
3901
4422
  }
3902
4423
  return object;
3903
4424
  }
4425
+ function normalizeDraftEvent(event, rawPoses) {
4426
+ return {
4427
+ ...event,
4428
+ participantObjectIds: [...new Set(event.participantObjectIds)],
4429
+ tags: [...new Set(event.tags)],
4430
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
4431
+ };
4432
+ }
4433
+ function normalizeDraftEventPose(rawPose) {
4434
+ const fieldMap = collectDraftFields(rawPose.fields);
4435
+ const placement = extractPlacementFromFieldMap(fieldMap);
4436
+ return {
4437
+ objectId: rawPose.objectId,
4438
+ placement,
4439
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
4440
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
4441
+ };
4442
+ }
3904
4443
  function collectDraftFields(fields) {
3905
4444
  const grouped = /* @__PURE__ */ new Map();
3906
4445
  for (const field of fields) {
@@ -3917,7 +4456,7 @@ var WorldOrbit = (() => {
3917
4456
  }
3918
4457
  return grouped;
3919
4458
  }
3920
- function extractDraftPlacement(objectType, fieldMap) {
4459
+ function extractPlacementFromFieldMap(fieldMap) {
3921
4460
  const orbitField = fieldMap.get("orbit")?.[0];
3922
4461
  const atField = fieldMap.get("at")?.[0];
3923
4462
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4353,6 +4892,7 @@ var WorldOrbit = (() => {
4353
4892
  background: true,
4354
4893
  guides: true,
4355
4894
  relations: true,
4895
+ events: true,
4356
4896
  orbits: true,
4357
4897
  objects: true,
4358
4898
  labels: true,
@@ -4556,6 +5096,7 @@ var WorldOrbit = (() => {
4556
5096
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
4557
5097
  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("") : "";
4558
5098
  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("") : "";
5099
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
4559
5100
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
4560
5101
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
4561
5102
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -4591,6 +5132,9 @@ var WorldOrbit = (() => {
4591
5132
  .wo-orbit-front { opacity: 0.9; }
4592
5133
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
4593
5134
  .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5135
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5136
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5137
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
4594
5138
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
4595
5139
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
4596
5140
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -4625,6 +5169,7 @@ var WorldOrbit = (() => {
4625
5169
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
4626
5170
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
4627
5171
  ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5172
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
4628
5173
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
4629
5174
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
4630
5175
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -4632,6 +5177,20 @@ var WorldOrbit = (() => {
4632
5177
  </g>
4633
5178
  </g>
4634
5179
  </svg>`;
5180
+ }
5181
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5182
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5183
+ if (participants.length === 0) {
5184
+ return "";
5185
+ }
5186
+ const stroke = event.event.color || theme.accent;
5187
+ const label = event.event.label || event.event.id;
5188
+ 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("");
5189
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5190
+ ${lineMarkup}
5191
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5192
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5193
+ </g>`;
4635
5194
  }
4636
5195
  function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
4637
5196
  const backParts = [];