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
@@ -541,6 +541,7 @@
541
541
  system,
542
542
  groups: [],
543
543
  relations: [],
544
+ events: [],
544
545
  objects
545
546
  };
546
547
  }
@@ -924,8 +925,10 @@
924
925
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
925
926
  const spacingFactor = layoutPresetSpacing(layoutPreset);
926
927
  const systemId = document2.system?.id ?? null;
927
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
928
- const relationships = buildSceneRelationships(document2.objects, objectMap);
928
+ const activeEventId = options.activeEventId ?? null;
929
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
930
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
931
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
929
932
  const positions = /* @__PURE__ */ new Map();
930
933
  const orbitDrafts = [];
931
934
  const leaderDrafts = [];
@@ -934,7 +937,7 @@
934
937
  const atObjects = [];
935
938
  const surfaceChildren = /* @__PURE__ */ new Map();
936
939
  const orbitChildren = /* @__PURE__ */ new Map();
937
- for (const object of document2.objects) {
940
+ for (const object of effectiveObjects) {
938
941
  const placement = object.placement;
939
942
  if (!placement) {
940
943
  rootObjects.push(object);
@@ -1029,13 +1032,14 @@
1029
1032
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1030
1033
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1031
1034
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1032
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1035
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1033
1036
  const relations = createSceneRelations(document2, objects);
1034
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1035
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1037
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1038
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1039
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1036
1040
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1037
1041
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1038
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1042
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1039
1043
  return {
1040
1044
  width,
1041
1045
  height,
@@ -1061,6 +1065,8 @@
1061
1065
  groups,
1062
1066
  semanticGroups,
1063
1067
  viewpoints,
1068
+ events,
1069
+ activeEventId,
1064
1070
  objects,
1065
1071
  orbitVisuals,
1066
1072
  relations,
@@ -1068,6 +1074,35 @@
1068
1074
  labels
1069
1075
  };
1070
1076
  }
1077
+ function createEffectiveObjects(objects, events, activeEventId) {
1078
+ const cloned = objects.map((object) => structuredClone(object));
1079
+ if (!activeEventId) {
1080
+ return cloned;
1081
+ }
1082
+ const activeEvent = events.find((event) => event.id === activeEventId);
1083
+ if (!activeEvent) {
1084
+ return cloned;
1085
+ }
1086
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1087
+ for (const pose of activeEvent.positions) {
1088
+ const object = objectMap.get(pose.objectId);
1089
+ if (!object) {
1090
+ continue;
1091
+ }
1092
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1093
+ if (pose.inner) {
1094
+ object.properties.inner = { ...pose.inner };
1095
+ } else {
1096
+ delete object.properties.inner;
1097
+ }
1098
+ if (pose.outer) {
1099
+ object.properties.outer = { ...pose.outer };
1100
+ } else {
1101
+ delete object.properties.outer;
1102
+ }
1103
+ }
1104
+ return cloned;
1105
+ }
1071
1106
  function resolveLayoutPreset(document2) {
1072
1107
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1073
1108
  switch (rawScale) {
@@ -1223,24 +1258,14 @@
1223
1258
  hidden: draft.object.properties.hidden === true
1224
1259
  };
1225
1260
  }
1226
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1261
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1227
1262
  const labels = [];
1228
1263
  const occupied = [];
1229
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1264
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1265
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1230
1266
  for (const object of visibleObjects) {
1231
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1232
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1233
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1234
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1235
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1236
- let attempts = 0;
1237
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1238
- labelY += direction * 14 * labelMultiplier;
1239
- secondaryY += direction * 14 * labelMultiplier;
1240
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1241
- attempts += 1;
1242
- }
1243
- occupied.push(bounds);
1267
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1268
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1244
1269
  labels.push({
1245
1270
  renderId: `${object.renderId}-label`,
1246
1271
  objectId: object.objectId,
@@ -1249,17 +1274,128 @@
1249
1274
  semanticGroupIds: [...object.semanticGroupIds],
1250
1275
  label: object.label,
1251
1276
  secondaryLabel: object.secondaryLabel,
1252
- x: object.x,
1253
- y: labelY,
1254
- secondaryY,
1255
- textAnchor: "middle",
1256
- direction: direction < 0 ? "above" : "below",
1277
+ x: placement.x,
1278
+ y: placement.labelY,
1279
+ secondaryY: placement.secondaryY,
1280
+ textAnchor: placement.textAnchor,
1281
+ direction: placement.direction,
1257
1282
  hidden: object.hidden
1258
1283
  });
1259
1284
  }
1260
1285
  return labels;
1261
1286
  }
1262
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1287
+ function compareLabelPlacementOrder(left, right) {
1288
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1289
+ if (priorityDiff !== 0) {
1290
+ return priorityDiff;
1291
+ }
1292
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1293
+ if (renderPriorityDiff !== 0) {
1294
+ return renderPriorityDiff;
1295
+ }
1296
+ return left.sortKey - right.sortKey;
1297
+ }
1298
+ function labelPlacementPriority(object) {
1299
+ switch (object.object.type) {
1300
+ case "star":
1301
+ return 0;
1302
+ case "planet":
1303
+ return 1;
1304
+ case "moon":
1305
+ return 2;
1306
+ case "belt":
1307
+ case "ring":
1308
+ return 3;
1309
+ case "asteroid":
1310
+ case "comet":
1311
+ return 4;
1312
+ case "structure":
1313
+ case "phenomenon":
1314
+ return 5;
1315
+ }
1316
+ }
1317
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1318
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1319
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1320
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1321
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1322
+ const rect = createLabelRect(object, placement, labelMultiplier);
1323
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1324
+ return placement;
1325
+ }
1326
+ }
1327
+ }
1328
+ return null;
1329
+ }
1330
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1331
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1332
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1333
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1334
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1335
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1336
+ 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";
1337
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1338
+ }
1339
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1340
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1341
+ return object.y >= parent.y ? "below" : "above";
1342
+ }
1343
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1344
+ }
1345
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1346
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1347
+ return object.x >= parent.x ? "right" : "left";
1348
+ }
1349
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1350
+ }
1351
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1352
+ const step = 14 * labelMultiplier;
1353
+ switch (direction) {
1354
+ case "above": {
1355
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1356
+ return {
1357
+ x: object.x,
1358
+ labelY,
1359
+ secondaryY: labelY - 16 * labelMultiplier,
1360
+ textAnchor: "middle",
1361
+ direction
1362
+ };
1363
+ }
1364
+ case "below": {
1365
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1366
+ return {
1367
+ x: object.x,
1368
+ labelY,
1369
+ secondaryY: labelY + 16 * labelMultiplier,
1370
+ textAnchor: "middle",
1371
+ direction
1372
+ };
1373
+ }
1374
+ case "left": {
1375
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1376
+ const labelY = object.y - 4 * labelMultiplier;
1377
+ return {
1378
+ x,
1379
+ labelY,
1380
+ secondaryY: labelY + 16 * labelMultiplier,
1381
+ textAnchor: "end",
1382
+ direction
1383
+ };
1384
+ }
1385
+ case "right": {
1386
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1387
+ const labelY = object.y - 4 * labelMultiplier;
1388
+ return {
1389
+ x,
1390
+ labelY,
1391
+ secondaryY: labelY + 16 * labelMultiplier,
1392
+ textAnchor: "start",
1393
+ direction
1394
+ };
1395
+ }
1396
+ }
1397
+ }
1398
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1263
1399
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1264
1400
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1265
1401
  return [
@@ -1274,6 +1410,10 @@
1274
1410
  id: "relations",
1275
1411
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1276
1412
  },
1413
+ {
1414
+ id: "events",
1415
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1416
+ },
1277
1417
  {
1278
1418
  id: "objects",
1279
1419
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1285,7 +1425,7 @@
1285
1425
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1286
1426
  ];
1287
1427
  }
1288
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1428
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1289
1429
  const groups = /* @__PURE__ */ new Map();
1290
1430
  const ensureGroup = (groupId) => {
1291
1431
  if (!groupId) {
@@ -1334,7 +1474,7 @@
1334
1474
  }
1335
1475
  }
1336
1476
  for (const group of groups.values()) {
1337
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1477
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1338
1478
  }
1339
1479
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1340
1480
  }
@@ -1368,6 +1508,29 @@
1368
1508
  };
1369
1509
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1370
1510
  }
1511
+ function createSceneEvents(events, objects, activeEventId) {
1512
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1513
+ return events.map((event) => {
1514
+ const objectIds = [.../* @__PURE__ */ new Set([
1515
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1516
+ ...event.participantObjectIds
1517
+ ])];
1518
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1519
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1520
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1521
+ return {
1522
+ renderId: `${createRenderId(event.id)}-event`,
1523
+ eventId: event.id,
1524
+ event,
1525
+ objectIds,
1526
+ participantIds: [...event.participantObjectIds],
1527
+ targetObjectId: event.targetObjectId,
1528
+ x: centroidX,
1529
+ y: centroidY,
1530
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1531
+ };
1532
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1533
+ }
1371
1534
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1372
1535
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1373
1536
  const drafts = /* @__PURE__ */ new Map();
@@ -1421,6 +1584,7 @@
1421
1584
  summary: "Fit the whole system with the current atlas defaults.",
1422
1585
  objectId: null,
1423
1586
  selectedObjectId: null,
1587
+ eventIds: [],
1424
1588
  projection,
1425
1589
  preset,
1426
1590
  rotationDeg: 0,
@@ -1457,6 +1621,9 @@
1457
1621
  draft.select = normalizedValue;
1458
1622
  }
1459
1623
  return;
1624
+ case "events":
1625
+ draft.eventIds = splitListValue(normalizedValue);
1626
+ return;
1460
1627
  case "projection":
1461
1628
  case "view":
1462
1629
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1513,6 +1680,7 @@
1513
1680
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1514
1681
  objectId,
1515
1682
  selectedObjectId,
1683
+ eventIds: [...new Set(draft.eventIds ?? [])],
1516
1684
  projection: draft.projection ?? projection,
1517
1685
  preset: draft.preset ?? preset,
1518
1686
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1570,7 +1738,7 @@
1570
1738
  next["orbits-front"] = enabled;
1571
1739
  continue;
1572
1740
  }
1573
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1741
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1574
1742
  next[rawLayer] = enabled;
1575
1743
  }
1576
1744
  }
@@ -1618,7 +1786,7 @@
1618
1786
  }
1619
1787
  return parts.join(" - ");
1620
1788
  }
1621
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1789
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1622
1790
  let minX = Number.POSITIVE_INFINITY;
1623
1791
  let minY = Number.POSITIVE_INFINITY;
1624
1792
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1648,7 +1816,7 @@
1648
1816
  for (const label of labels) {
1649
1817
  if (label.hidden)
1650
1818
  continue;
1651
- includeLabelBounds(label, include);
1819
+ includeLabelBounds(label, include, labelMultiplier);
1652
1820
  }
1653
1821
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1654
1822
  return createBounds(0, 0, width, height);
@@ -1686,13 +1854,10 @@
1686
1854
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1687
1855
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1688
1856
  }
1689
- function includeLabelBounds(label, include) {
1690
- const labelScale = 1;
1691
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1692
- include(label.x - labelHalfWidth, label.y - 18);
1693
- include(label.x + labelHalfWidth, label.y + 8);
1694
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1695
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1857
+ function includeLabelBounds(label, include, labelMultiplier) {
1858
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1859
+ include(bounds.left, bounds.top);
1860
+ include(bounds.right, bounds.bottom);
1696
1861
  }
1697
1862
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1698
1863
  if (positions.has(object.id)) {
@@ -2082,7 +2247,7 @@
2082
2247
  return null;
2083
2248
  }
2084
2249
  }
2085
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2250
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2086
2251
  let minX = Number.POSITIVE_INFINITY;
2087
2252
  let minY = Number.POSITIVE_INFINITY;
2088
2253
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2111,7 +2276,7 @@
2111
2276
  }
2112
2277
  for (const label of labels) {
2113
2278
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2114
- includeLabelBounds(label, include);
2279
+ includeLabelBounds(label, include, labelMultiplier);
2115
2280
  }
2116
2281
  }
2117
2282
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2136,12 +2301,28 @@
2136
2301
  }
2137
2302
  return current.id;
2138
2303
  }
2139
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2304
+ function createLabelRect(object, placement, labelMultiplier) {
2305
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2306
+ }
2307
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2308
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2309
+ const labelWidth = labelHalfWidth * 2;
2310
+ const topPadding = direction === "above" ? 18 : 12;
2311
+ const bottomPadding = direction === "above" ? 8 : 12;
2312
+ let left = x - labelHalfWidth;
2313
+ let right = x + labelHalfWidth;
2314
+ if (textAnchor === "start") {
2315
+ left = x;
2316
+ right = x + labelWidth;
2317
+ } else if (textAnchor === "end") {
2318
+ left = x - labelWidth;
2319
+ right = x;
2320
+ }
2140
2321
  return {
2141
- left: x - labelHalfWidth,
2142
- right: x + labelHalfWidth,
2143
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2144
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2322
+ left,
2323
+ right,
2324
+ top: Math.min(labelY, secondaryY) - topPadding,
2325
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2145
2326
  };
2146
2327
  }
2147
2328
  function rectsOverlap(left, right) {
@@ -2328,11 +2509,6 @@
2328
2509
  function customColorFor(value) {
2329
2510
  return typeof value === "string" && value.trim() ? value : void 0;
2330
2511
  }
2331
- function estimateLabelHalfWidth(object, labelMultiplier) {
2332
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2333
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2334
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2335
- }
2336
2512
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2337
2513
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2338
2514
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2349,7 +2525,7 @@
2349
2525
  }
2350
2526
 
2351
2527
  // packages/core/dist/draft.js
2352
- function materializeAtlasDocument(document2) {
2528
+ function materializeAtlasDocument(document2, options = {}) {
2353
2529
  const system = document2.system ? {
2354
2530
  type: "system",
2355
2531
  id: document2.system.id,
@@ -2360,6 +2536,8 @@
2360
2536
  properties: materializeDraftSystemProperties(document2.system),
2361
2537
  info: materializeDraftSystemInfo(document2.system)
2362
2538
  } : null;
2539
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2540
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2363
2541
  return {
2364
2542
  format: "worldorbit",
2365
2543
  version: "1.0",
@@ -2367,7 +2545,8 @@
2367
2545
  system,
2368
2546
  groups: structuredClone(document2.groups ?? []),
2369
2547
  relations: structuredClone(document2.relations ?? []),
2370
- objects: document2.objects.map(cloneWorldOrbitObject)
2548
+ events: document2.events.map(cloneWorldOrbitEvent),
2549
+ objects
2371
2550
  };
2372
2551
  }
2373
2552
  function cloneWorldOrbitObject(object) {
@@ -2389,6 +2568,52 @@
2389
2568
  info: { ...object.info }
2390
2569
  };
2391
2570
  }
2571
+ function cloneWorldOrbitEvent(event) {
2572
+ return {
2573
+ ...event,
2574
+ participantObjectIds: [...event.participantObjectIds],
2575
+ tags: [...event.tags],
2576
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2577
+ };
2578
+ }
2579
+ function cloneWorldOrbitEventPose(pose) {
2580
+ return {
2581
+ objectId: pose.objectId,
2582
+ placement: clonePlacement(pose.placement),
2583
+ inner: pose.inner ? { ...pose.inner } : void 0,
2584
+ outer: pose.outer ? { ...pose.outer } : void 0
2585
+ };
2586
+ }
2587
+ function clonePlacement(placement) {
2588
+ return placement ? structuredClone(placement) : null;
2589
+ }
2590
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2591
+ if (!activeEventId) {
2592
+ return;
2593
+ }
2594
+ const event = events.find((entry) => entry.id === activeEventId);
2595
+ if (!event) {
2596
+ return;
2597
+ }
2598
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2599
+ for (const pose of event.positions) {
2600
+ const object = objectMap.get(pose.objectId);
2601
+ if (!object) {
2602
+ continue;
2603
+ }
2604
+ object.placement = clonePlacement(pose.placement);
2605
+ if (pose.inner) {
2606
+ object.properties.inner = { ...pose.inner };
2607
+ } else {
2608
+ delete object.properties.inner;
2609
+ }
2610
+ if (pose.outer) {
2611
+ object.properties.outer = { ...pose.outer };
2612
+ } else {
2613
+ delete object.properties.outer;
2614
+ }
2615
+ }
2616
+ }
2392
2617
  function cloneProperties(properties) {
2393
2618
  const next = {};
2394
2619
  for (const [key, value] of Object.entries(properties)) {
@@ -2477,6 +2702,9 @@
2477
2702
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2478
2703
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2479
2704
  }
2705
+ if (viewpoint.events.length > 0) {
2706
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2707
+ }
2480
2708
  }
2481
2709
  for (const annotation of system.annotations) {
2482
2710
  const prefix = `annotation.${annotation.id}`;
@@ -2501,7 +2729,7 @@
2501
2729
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2502
2730
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2503
2731
  }
2504
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2732
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2505
2733
  if (layers[key] !== void 0) {
2506
2734
  tokens.push(layers[key] ? key : `-${key}`);
2507
2735
  }
@@ -2675,6 +2903,7 @@
2675
2903
  const diagnostics = [];
2676
2904
  const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
2677
2905
  const groupIds = new Set(document2.groups.map((group) => group.id));
2906
+ const eventIds = new Set(document2.events.map((event) => event.id));
2678
2907
  if (!document2.system) {
2679
2908
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
2680
2909
  }
@@ -2684,6 +2913,7 @@
2684
2913
  ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
2685
2914
  ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
2686
2915
  ["relation", document2.relations.map((relation) => relation.id)],
2916
+ ["event", document2.events.map((event) => event.id)],
2687
2917
  ["object", document2.objects.map((object) => object.id)]
2688
2918
  ]) {
2689
2919
  for (const id of ids) {
@@ -2699,11 +2929,14 @@
2699
2929
  validateRelation(relation, objectMap, diagnostics);
2700
2930
  }
2701
2931
  for (const viewpoint of document2.system?.viewpoints ?? []) {
2702
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
2932
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
2703
2933
  }
2704
2934
  for (const object of document2.objects) {
2705
2935
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
2706
2936
  }
2937
+ for (const event of document2.events) {
2938
+ validateEvent(event, objectMap, diagnostics);
2939
+ }
2707
2940
  return diagnostics;
2708
2941
  }
2709
2942
  function validateRelation(relation, objectMap, diagnostics) {
@@ -2721,13 +2954,19 @@
2721
2954
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
2722
2955
  }
2723
2956
  }
2724
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
2725
- if (!filter || sourceSchemaVersion !== "2.1") {
2726
- return;
2727
- }
2728
- for (const groupId of filter.groupIds) {
2729
- if (!groupIds.has(groupId)) {
2730
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
2957
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
2958
+ if (sourceSchemaVersion === "2.1") {
2959
+ if (filter) {
2960
+ for (const groupId of filter.groupIds) {
2961
+ if (!groupIds.has(groupId)) {
2962
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
2963
+ }
2964
+ }
2965
+ }
2966
+ for (const eventId of eventRefs) {
2967
+ if (!eventIds.has(eventId)) {
2968
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
2969
+ }
2731
2970
  }
2732
2971
  }
2733
2972
  }
@@ -2813,6 +3052,103 @@
2813
3052
  }
2814
3053
  }
2815
3054
  }
3055
+ function validateEvent(event, objectMap, diagnostics) {
3056
+ const fieldPrefix = `event.${event.id}`;
3057
+ const referencedIds = /* @__PURE__ */ new Set();
3058
+ if (!event.kind.trim()) {
3059
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3060
+ }
3061
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3062
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3063
+ }
3064
+ if (event.targetObjectId) {
3065
+ referencedIds.add(event.targetObjectId);
3066
+ if (!objectMap.has(event.targetObjectId)) {
3067
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3068
+ }
3069
+ }
3070
+ const seenParticipants = /* @__PURE__ */ new Set();
3071
+ for (const participantId of event.participantObjectIds) {
3072
+ referencedIds.add(participantId);
3073
+ if (seenParticipants.has(participantId)) {
3074
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3075
+ continue;
3076
+ }
3077
+ seenParticipants.add(participantId);
3078
+ if (!objectMap.has(participantId)) {
3079
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3080
+ }
3081
+ }
3082
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3083
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3084
+ }
3085
+ if (event.positions.length === 0) {
3086
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3087
+ }
3088
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3089
+ 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`));
3090
+ }
3091
+ const poseIds = /* @__PURE__ */ new Set();
3092
+ for (const pose of event.positions) {
3093
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3094
+ if (poseIds.has(pose.objectId)) {
3095
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3096
+ continue;
3097
+ }
3098
+ poseIds.add(pose.objectId);
3099
+ const object = objectMap.get(pose.objectId);
3100
+ if (!object) {
3101
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3102
+ continue;
3103
+ }
3104
+ if (!referencedIds.has(pose.objectId)) {
3105
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3106
+ }
3107
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3108
+ }
3109
+ }
3110
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3111
+ const placement = pose.placement;
3112
+ if (!placement) {
3113
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3114
+ return;
3115
+ }
3116
+ if (placement.mode === "orbit") {
3117
+ if (!objectMap.has(placement.target)) {
3118
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3119
+ }
3120
+ if (placement.distance && placement.semiMajor) {
3121
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3122
+ }
3123
+ return;
3124
+ }
3125
+ if (placement.mode === "surface") {
3126
+ const target = objectMap.get(placement.target);
3127
+ if (!target) {
3128
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3129
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3130
+ 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`));
3131
+ }
3132
+ return;
3133
+ }
3134
+ if (placement.mode === "at") {
3135
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3136
+ 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`));
3137
+ }
3138
+ const reference = placement.reference;
3139
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3140
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3141
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3142
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3143
+ } else if (reference.kind === "lagrange") {
3144
+ if (!objectMap.has(reference.primary)) {
3145
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3146
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3147
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3148
+ }
3149
+ }
3150
+ }
3151
+ }
2816
3152
  function validateAtTarget(object, objectMap, diagnostics) {
2817
3153
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
2818
3154
  if (!reference) {
@@ -3013,6 +3349,21 @@
3013
3349
  });
3014
3350
  }
3015
3351
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3352
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3353
+ "orbit",
3354
+ "distance",
3355
+ "semiMajor",
3356
+ "eccentricity",
3357
+ "period",
3358
+ "angle",
3359
+ "inclination",
3360
+ "phase",
3361
+ "at",
3362
+ "surface",
3363
+ "free",
3364
+ "inner",
3365
+ "outer"
3366
+ ]);
3016
3367
  function parseWorldOrbitAtlas(source) {
3017
3368
  return parseAtlasSource(source);
3018
3369
  }
@@ -3027,12 +3378,15 @@
3027
3378
  const objectNodes = [];
3028
3379
  const groups = [];
3029
3380
  const relations = [];
3381
+ const events = [];
3382
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3030
3383
  let sawDefaults = false;
3031
3384
  let sawAtlas = false;
3032
3385
  const viewpointIds = /* @__PURE__ */ new Set();
3033
3386
  const annotationIds = /* @__PURE__ */ new Set();
3034
3387
  const groupIds = /* @__PURE__ */ new Set();
3035
3388
  const relationIds = /* @__PURE__ */ new Set();
3389
+ const eventIds = /* @__PURE__ */ new Set();
3036
3390
  for (let index = 0; index < lines.length; index++) {
3037
3391
  const rawLine = lines[index];
3038
3392
  const lineNumber = index + 1;
@@ -3063,7 +3417,7 @@
3063
3417
  continue;
3064
3418
  }
3065
3419
  if (indent === 0) {
3066
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
3420
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3067
3421
  if (section.kind === "system") {
3068
3422
  system = section.system;
3069
3423
  } else if (section.kind === "defaults") {
@@ -3082,6 +3436,7 @@
3082
3436
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3083
3437
  }
3084
3438
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3439
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3085
3440
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3086
3441
  const baseDocument = {
3087
3442
  format: "worldorbit",
@@ -3089,6 +3444,7 @@
3089
3444
  system,
3090
3445
  groups,
3091
3446
  relations,
3447
+ events: normalizedEvents,
3092
3448
  objects,
3093
3449
  diagnostics
3094
3450
  };
@@ -3124,7 +3480,7 @@
3124
3480
  const version = tokens[1].value.toLowerCase();
3125
3481
  return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3126
3482
  }
3127
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
3483
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3128
3484
  const keyword = tokens[0]?.value.toLowerCase();
3129
3485
  switch (keyword) {
3130
3486
  case "system":
@@ -3161,7 +3517,7 @@
3161
3517
  if (!system) {
3162
3518
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3163
3519
  }
3164
- return startViewpointSection(tokens, line, system, viewpointIds);
3520
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3165
3521
  case "annotation":
3166
3522
  if (!system) {
3167
3523
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3173,6 +3529,9 @@
3173
3529
  case "relation":
3174
3530
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3175
3531
  return startRelationSection(tokens, line, relations, relationIds);
3532
+ case "event":
3533
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
3534
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3176
3535
  case "object":
3177
3536
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3178
3537
  default:
@@ -3209,7 +3568,7 @@
3209
3568
  seenFields: /* @__PURE__ */ new Set()
3210
3569
  };
3211
3570
  }
3212
- function startViewpointSection(tokens, line, system, viewpointIds) {
3571
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3213
3572
  if (tokens.length !== 2) {
3214
3573
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3215
3574
  }
@@ -3226,6 +3585,7 @@
3226
3585
  summary: "",
3227
3586
  focusObjectId: null,
3228
3587
  selectedObjectId: null,
3588
+ events: [],
3229
3589
  projection: system.defaults.view,
3230
3590
  preset: system.defaults.preset,
3231
3591
  zoom: null,
@@ -3238,6 +3598,8 @@
3238
3598
  return {
3239
3599
  kind: "viewpoint",
3240
3600
  viewpoint,
3601
+ sourceSchemaVersion,
3602
+ diagnostics,
3241
3603
  seenFields: /* @__PURE__ */ new Set(),
3242
3604
  inFilter: false,
3243
3605
  filterIndent: null,
@@ -3328,6 +3690,49 @@
3328
3690
  seenFields: /* @__PURE__ */ new Set()
3329
3691
  };
3330
3692
  }
3693
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
3694
+ if (tokens.length !== 2) {
3695
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
3696
+ }
3697
+ const id = normalizeIdentifier(tokens[1].value);
3698
+ if (!id) {
3699
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
3700
+ }
3701
+ if (eventIds.has(id)) {
3702
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
3703
+ }
3704
+ const event = {
3705
+ id,
3706
+ kind: "",
3707
+ label: humanizeIdentifier2(id),
3708
+ summary: null,
3709
+ targetObjectId: null,
3710
+ participantObjectIds: [],
3711
+ timing: null,
3712
+ visibility: null,
3713
+ tags: [],
3714
+ color: null,
3715
+ hidden: false,
3716
+ positions: []
3717
+ };
3718
+ const rawPoses = [];
3719
+ events.push(event);
3720
+ eventPoseNodes.set(id, rawPoses);
3721
+ eventIds.add(id);
3722
+ return {
3723
+ kind: "event",
3724
+ event,
3725
+ sourceSchemaVersion,
3726
+ diagnostics,
3727
+ seenFields: /* @__PURE__ */ new Set(),
3728
+ rawPoses,
3729
+ inPositions: false,
3730
+ positionsIndent: null,
3731
+ activePose: null,
3732
+ poseIndent: null,
3733
+ activePoseSeenFields: /* @__PURE__ */ new Set()
3734
+ };
3735
+ }
3331
3736
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3332
3737
  if (tokens.length < 3) {
3333
3738
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -3384,6 +3789,9 @@
3384
3789
  case "relation":
3385
3790
  applyRelationField(section, tokens, line);
3386
3791
  return;
3792
+ case "event":
3793
+ applyEventField(section, indent, tokens, line);
3794
+ return;
3387
3795
  case "object":
3388
3796
  applyObjectField(section, indent, tokens, line);
3389
3797
  return;
@@ -3510,7 +3918,14 @@
3510
3918
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3511
3919
  return;
3512
3920
  case "layers":
3513
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
3921
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
3922
+ return;
3923
+ case "events":
3924
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
3925
+ line,
3926
+ column: tokens[0].column
3927
+ });
3928
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3514
3929
  return;
3515
3930
  default:
3516
3931
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -3615,6 +4030,106 @@
3615
4030
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
3616
4031
  }
3617
4032
  }
4033
+ function applyEventField(section, indent, tokens, line) {
4034
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4035
+ section.activePose = null;
4036
+ section.poseIndent = null;
4037
+ section.activePoseSeenFields.clear();
4038
+ }
4039
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4040
+ section.inPositions = false;
4041
+ section.positionsIndent = null;
4042
+ }
4043
+ if (section.activePose) {
4044
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4045
+ return;
4046
+ }
4047
+ if (section.inPositions) {
4048
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4049
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4050
+ }
4051
+ const objectId = tokens[1].value;
4052
+ if (!objectId.trim()) {
4053
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4054
+ }
4055
+ const rawPose = {
4056
+ objectId,
4057
+ fields: [],
4058
+ location: { line, column: tokens[0].column }
4059
+ };
4060
+ section.rawPoses.push(rawPose);
4061
+ section.activePose = rawPose;
4062
+ section.poseIndent = indent;
4063
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4064
+ return;
4065
+ }
4066
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4067
+ if (section.seenFields.has("positions")) {
4068
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4069
+ }
4070
+ section.seenFields.add("positions");
4071
+ section.inPositions = true;
4072
+ section.positionsIndent = indent;
4073
+ return;
4074
+ }
4075
+ const key = requireUniqueField(tokens, section.seenFields, line);
4076
+ switch (key) {
4077
+ case "kind":
4078
+ section.event.kind = joinFieldValue(tokens, line);
4079
+ return;
4080
+ case "label":
4081
+ section.event.label = joinFieldValue(tokens, line);
4082
+ return;
4083
+ case "summary":
4084
+ section.event.summary = joinFieldValue(tokens, line);
4085
+ return;
4086
+ case "target":
4087
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4088
+ return;
4089
+ case "participants":
4090
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4091
+ return;
4092
+ case "timing":
4093
+ section.event.timing = joinFieldValue(tokens, line);
4094
+ return;
4095
+ case "visibility":
4096
+ section.event.visibility = joinFieldValue(tokens, line);
4097
+ return;
4098
+ case "tags":
4099
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4100
+ return;
4101
+ case "color":
4102
+ section.event.color = joinFieldValue(tokens, line);
4103
+ return;
4104
+ case "hidden":
4105
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4106
+ line,
4107
+ column: tokens[0].column
4108
+ });
4109
+ return;
4110
+ default:
4111
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4112
+ }
4113
+ }
4114
+ function parseEventPoseField(tokens, line, seenFields) {
4115
+ if (tokens.length < 2) {
4116
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4117
+ }
4118
+ const key = tokens[0].value;
4119
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4120
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4121
+ }
4122
+ if (seenFields.has(key)) {
4123
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4124
+ }
4125
+ seenFields.add(key);
4126
+ return {
4127
+ type: "field",
4128
+ key,
4129
+ values: tokens.slice(1).map((token) => token.value),
4130
+ location: { line, column: tokens[0].column }
4131
+ };
4132
+ }
3618
4133
  function applyObjectField(section, indent, tokens, line) {
3619
4134
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
3620
4135
  section.activeBlock = null;
@@ -3673,7 +4188,7 @@
3673
4188
  function parseObjectTypeTokens(tokens, line) {
3674
4189
  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");
3675
4190
  }
3676
- function parseLayerTokens(tokens, line) {
4191
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
3677
4192
  const layers = {};
3678
4193
  for (const token of parseTokenList(tokens, line, "layers")) {
3679
4194
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -3683,7 +4198,13 @@
3683
4198
  layers["orbits-front"] = enabled;
3684
4199
  continue;
3685
4200
  }
3686
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4201
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4202
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4203
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4204
+ line,
4205
+ column: tokens[0]?.column ?? 1
4206
+ });
4207
+ }
3687
4208
  layers[raw] = enabled;
3688
4209
  }
3689
4210
  }
@@ -3822,7 +4343,7 @@
3822
4343
  }
3823
4344
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
3824
4345
  const fieldMap = collectDraftFields(node.fields);
3825
- const placement = extractDraftPlacement(node.objectType, fieldMap);
4346
+ const placement = extractPlacementFromFieldMap(fieldMap);
3826
4347
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
3827
4348
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
3828
4349
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -3874,6 +4395,24 @@
3874
4395
  }
3875
4396
  return object;
3876
4397
  }
4398
+ function normalizeDraftEvent(event, rawPoses) {
4399
+ return {
4400
+ ...event,
4401
+ participantObjectIds: [...new Set(event.participantObjectIds)],
4402
+ tags: [...new Set(event.tags)],
4403
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
4404
+ };
4405
+ }
4406
+ function normalizeDraftEventPose(rawPose) {
4407
+ const fieldMap = collectDraftFields(rawPose.fields);
4408
+ const placement = extractPlacementFromFieldMap(fieldMap);
4409
+ return {
4410
+ objectId: rawPose.objectId,
4411
+ placement,
4412
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
4413
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
4414
+ };
4415
+ }
3877
4416
  function collectDraftFields(fields) {
3878
4417
  const grouped = /* @__PURE__ */ new Map();
3879
4418
  for (const field of fields) {
@@ -3890,7 +4429,7 @@
3890
4429
  }
3891
4430
  return grouped;
3892
4431
  }
3893
- function extractDraftPlacement(objectType, fieldMap) {
4432
+ function extractPlacementFromFieldMap(fieldMap) {
3894
4433
  const orbitField = fieldMap.get("orbit")?.[0];
3895
4434
  const atField = fieldMap.get("at")?.[0];
3896
4435
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4326,6 +4865,7 @@
4326
4865
  background: true,
4327
4866
  guides: true,
4328
4867
  relations: true,
4868
+ events: true,
4329
4869
  orbits: true,
4330
4870
  objects: true,
4331
4871
  labels: true,
@@ -4529,6 +5069,7 @@
4529
5069
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
4530
5070
  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("") : "";
4531
5071
  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("") : "";
5072
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
4532
5073
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
4533
5074
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
4534
5075
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -4564,6 +5105,9 @@
4564
5105
  .wo-orbit-front { opacity: 0.9; }
4565
5106
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
4566
5107
  .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5108
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5109
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5110
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
4567
5111
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
4568
5112
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
4569
5113
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -4598,6 +5142,7 @@
4598
5142
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
4599
5143
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
4600
5144
  ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5145
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
4601
5146
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
4602
5147
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
4603
5148
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -4605,6 +5150,20 @@
4605
5150
  </g>
4606
5151
  </g>
4607
5152
  </svg>`;
5153
+ }
5154
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5155
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5156
+ if (participants.length === 0) {
5157
+ return "";
5158
+ }
5159
+ const stroke = event.event.color || theme.accent;
5160
+ const label = event.event.label || event.event.id;
5161
+ 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("");
5162
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5163
+ ${lineMarkup}
5164
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5165
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5166
+ </g>`;
4608
5167
  }
4609
5168
  function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
4610
5169
  const backParts = [];