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,
@@ -1079,6 +1085,35 @@
1079
1085
  y: center.y + dx * sin + dy * cos
1080
1086
  };
1081
1087
  }
1088
+ function createEffectiveObjects(objects, events, activeEventId) {
1089
+ const cloned = objects.map((object) => structuredClone(object));
1090
+ if (!activeEventId) {
1091
+ return cloned;
1092
+ }
1093
+ const activeEvent = events.find((event) => event.id === activeEventId);
1094
+ if (!activeEvent) {
1095
+ return cloned;
1096
+ }
1097
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1098
+ for (const pose of activeEvent.positions) {
1099
+ const object = objectMap.get(pose.objectId);
1100
+ if (!object) {
1101
+ continue;
1102
+ }
1103
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1104
+ if (pose.inner) {
1105
+ object.properties.inner = { ...pose.inner };
1106
+ } else {
1107
+ delete object.properties.inner;
1108
+ }
1109
+ if (pose.outer) {
1110
+ object.properties.outer = { ...pose.outer };
1111
+ } else {
1112
+ delete object.properties.outer;
1113
+ }
1114
+ }
1115
+ return cloned;
1116
+ }
1082
1117
  function resolveLayoutPreset(document2) {
1083
1118
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1084
1119
  switch (rawScale) {
@@ -1234,24 +1269,14 @@
1234
1269
  hidden: draft.object.properties.hidden === true
1235
1270
  };
1236
1271
  }
1237
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1272
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1238
1273
  const labels = [];
1239
1274
  const occupied = [];
1240
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1275
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1276
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1241
1277
  for (const object of visibleObjects) {
1242
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1243
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1244
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1245
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1246
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1247
- let attempts = 0;
1248
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1249
- labelY += direction * 14 * labelMultiplier;
1250
- secondaryY += direction * 14 * labelMultiplier;
1251
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1252
- attempts += 1;
1253
- }
1254
- occupied.push(bounds);
1278
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1279
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1255
1280
  labels.push({
1256
1281
  renderId: `${object.renderId}-label`,
1257
1282
  objectId: object.objectId,
@@ -1260,17 +1285,128 @@
1260
1285
  semanticGroupIds: [...object.semanticGroupIds],
1261
1286
  label: object.label,
1262
1287
  secondaryLabel: object.secondaryLabel,
1263
- x: object.x,
1264
- y: labelY,
1265
- secondaryY,
1266
- textAnchor: "middle",
1267
- direction: direction < 0 ? "above" : "below",
1288
+ x: placement.x,
1289
+ y: placement.labelY,
1290
+ secondaryY: placement.secondaryY,
1291
+ textAnchor: placement.textAnchor,
1292
+ direction: placement.direction,
1268
1293
  hidden: object.hidden
1269
1294
  });
1270
1295
  }
1271
1296
  return labels;
1272
1297
  }
1273
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1298
+ function compareLabelPlacementOrder(left, right) {
1299
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1300
+ if (priorityDiff !== 0) {
1301
+ return priorityDiff;
1302
+ }
1303
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1304
+ if (renderPriorityDiff !== 0) {
1305
+ return renderPriorityDiff;
1306
+ }
1307
+ return left.sortKey - right.sortKey;
1308
+ }
1309
+ function labelPlacementPriority(object) {
1310
+ switch (object.object.type) {
1311
+ case "star":
1312
+ return 0;
1313
+ case "planet":
1314
+ return 1;
1315
+ case "moon":
1316
+ return 2;
1317
+ case "belt":
1318
+ case "ring":
1319
+ return 3;
1320
+ case "asteroid":
1321
+ case "comet":
1322
+ return 4;
1323
+ case "structure":
1324
+ case "phenomenon":
1325
+ return 5;
1326
+ }
1327
+ }
1328
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1329
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1330
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1331
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1332
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1333
+ const rect = createLabelRect(object, placement, labelMultiplier);
1334
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1335
+ return placement;
1336
+ }
1337
+ }
1338
+ }
1339
+ return null;
1340
+ }
1341
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1342
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1343
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1344
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1345
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1346
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1347
+ 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";
1348
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1349
+ }
1350
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1351
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1352
+ return object.y >= parent.y ? "below" : "above";
1353
+ }
1354
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1355
+ }
1356
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1357
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1358
+ return object.x >= parent.x ? "right" : "left";
1359
+ }
1360
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1361
+ }
1362
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1363
+ const step = 14 * labelMultiplier;
1364
+ switch (direction) {
1365
+ case "above": {
1366
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1367
+ return {
1368
+ x: object.x,
1369
+ labelY,
1370
+ secondaryY: labelY - 16 * labelMultiplier,
1371
+ textAnchor: "middle",
1372
+ direction
1373
+ };
1374
+ }
1375
+ case "below": {
1376
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1377
+ return {
1378
+ x: object.x,
1379
+ labelY,
1380
+ secondaryY: labelY + 16 * labelMultiplier,
1381
+ textAnchor: "middle",
1382
+ direction
1383
+ };
1384
+ }
1385
+ case "left": {
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: "end",
1393
+ direction
1394
+ };
1395
+ }
1396
+ case "right": {
1397
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1398
+ const labelY = object.y - 4 * labelMultiplier;
1399
+ return {
1400
+ x,
1401
+ labelY,
1402
+ secondaryY: labelY + 16 * labelMultiplier,
1403
+ textAnchor: "start",
1404
+ direction
1405
+ };
1406
+ }
1407
+ }
1408
+ }
1409
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1274
1410
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1275
1411
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1276
1412
  return [
@@ -1285,6 +1421,10 @@
1285
1421
  id: "relations",
1286
1422
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1287
1423
  },
1424
+ {
1425
+ id: "events",
1426
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1427
+ },
1288
1428
  {
1289
1429
  id: "objects",
1290
1430
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1296,7 +1436,7 @@
1296
1436
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1297
1437
  ];
1298
1438
  }
1299
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1439
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1300
1440
  const groups = /* @__PURE__ */ new Map();
1301
1441
  const ensureGroup = (groupId) => {
1302
1442
  if (!groupId) {
@@ -1345,7 +1485,7 @@
1345
1485
  }
1346
1486
  }
1347
1487
  for (const group of groups.values()) {
1348
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1488
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1349
1489
  }
1350
1490
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1351
1491
  }
@@ -1379,6 +1519,29 @@
1379
1519
  };
1380
1520
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1381
1521
  }
1522
+ function createSceneEvents(events, objects, activeEventId) {
1523
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1524
+ return events.map((event) => {
1525
+ const objectIds = [.../* @__PURE__ */ new Set([
1526
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1527
+ ...event.participantObjectIds
1528
+ ])];
1529
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1530
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1531
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1532
+ return {
1533
+ renderId: `${createRenderId(event.id)}-event`,
1534
+ eventId: event.id,
1535
+ event,
1536
+ objectIds,
1537
+ participantIds: [...event.participantObjectIds],
1538
+ targetObjectId: event.targetObjectId,
1539
+ x: centroidX,
1540
+ y: centroidY,
1541
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1542
+ };
1543
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1544
+ }
1382
1545
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1383
1546
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1384
1547
  const drafts = /* @__PURE__ */ new Map();
@@ -1432,6 +1595,7 @@
1432
1595
  summary: "Fit the whole system with the current atlas defaults.",
1433
1596
  objectId: null,
1434
1597
  selectedObjectId: null,
1598
+ eventIds: [],
1435
1599
  projection,
1436
1600
  preset,
1437
1601
  rotationDeg: 0,
@@ -1468,6 +1632,9 @@
1468
1632
  draft.select = normalizedValue;
1469
1633
  }
1470
1634
  return;
1635
+ case "events":
1636
+ draft.eventIds = splitListValue(normalizedValue);
1637
+ return;
1471
1638
  case "projection":
1472
1639
  case "view":
1473
1640
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1524,6 +1691,7 @@
1524
1691
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1525
1692
  objectId,
1526
1693
  selectedObjectId,
1694
+ eventIds: [...new Set(draft.eventIds ?? [])],
1527
1695
  projection: draft.projection ?? projection,
1528
1696
  preset: draft.preset ?? preset,
1529
1697
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1581,7 +1749,7 @@
1581
1749
  next["orbits-front"] = enabled;
1582
1750
  continue;
1583
1751
  }
1584
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1752
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1585
1753
  next[rawLayer] = enabled;
1586
1754
  }
1587
1755
  }
@@ -1629,7 +1797,7 @@
1629
1797
  }
1630
1798
  return parts.join(" - ");
1631
1799
  }
1632
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1800
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1633
1801
  let minX = Number.POSITIVE_INFINITY;
1634
1802
  let minY = Number.POSITIVE_INFINITY;
1635
1803
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1659,7 +1827,7 @@
1659
1827
  for (const label of labels) {
1660
1828
  if (label.hidden)
1661
1829
  continue;
1662
- includeLabelBounds(label, include);
1830
+ includeLabelBounds(label, include, labelMultiplier);
1663
1831
  }
1664
1832
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1665
1833
  return createBounds(0, 0, width, height);
@@ -1697,13 +1865,10 @@
1697
1865
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1698
1866
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1699
1867
  }
1700
- function includeLabelBounds(label, include) {
1701
- const labelScale = 1;
1702
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1703
- include(label.x - labelHalfWidth, label.y - 18);
1704
- include(label.x + labelHalfWidth, label.y + 8);
1705
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1706
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1868
+ function includeLabelBounds(label, include, labelMultiplier) {
1869
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1870
+ include(bounds.left, bounds.top);
1871
+ include(bounds.right, bounds.bottom);
1707
1872
  }
1708
1873
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1709
1874
  if (positions.has(object.id)) {
@@ -2093,7 +2258,7 @@
2093
2258
  return null;
2094
2259
  }
2095
2260
  }
2096
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2261
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2097
2262
  let minX = Number.POSITIVE_INFINITY;
2098
2263
  let minY = Number.POSITIVE_INFINITY;
2099
2264
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2122,7 +2287,7 @@
2122
2287
  }
2123
2288
  for (const label of labels) {
2124
2289
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2125
- includeLabelBounds(label, include);
2290
+ includeLabelBounds(label, include, labelMultiplier);
2126
2291
  }
2127
2292
  }
2128
2293
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2147,12 +2312,28 @@
2147
2312
  }
2148
2313
  return current.id;
2149
2314
  }
2150
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2315
+ function createLabelRect(object, placement, labelMultiplier) {
2316
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2317
+ }
2318
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2319
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2320
+ const labelWidth = labelHalfWidth * 2;
2321
+ const topPadding = direction === "above" ? 18 : 12;
2322
+ const bottomPadding = direction === "above" ? 8 : 12;
2323
+ let left = x - labelHalfWidth;
2324
+ let right = x + labelHalfWidth;
2325
+ if (textAnchor === "start") {
2326
+ left = x;
2327
+ right = x + labelWidth;
2328
+ } else if (textAnchor === "end") {
2329
+ left = x - labelWidth;
2330
+ right = x;
2331
+ }
2151
2332
  return {
2152
- left: x - labelHalfWidth,
2153
- right: x + labelHalfWidth,
2154
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2155
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2333
+ left,
2334
+ right,
2335
+ top: Math.min(labelY, secondaryY) - topPadding,
2336
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2156
2337
  };
2157
2338
  }
2158
2339
  function rectsOverlap(left, right) {
@@ -2339,11 +2520,6 @@
2339
2520
  function customColorFor(value) {
2340
2521
  return typeof value === "string" && value.trim() ? value : void 0;
2341
2522
  }
2342
- function estimateLabelHalfWidth(object, labelMultiplier) {
2343
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2344
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2345
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2346
- }
2347
2523
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2348
2524
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2349
2525
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2383,6 +2559,7 @@
2383
2559
  system,
2384
2560
  groups: structuredClone(document2.groups ?? []),
2385
2561
  relations: structuredClone(document2.relations ?? []),
2562
+ events: structuredClone(document2.events ?? []),
2386
2563
  objects: document2.objects.map(cloneWorldOrbitObject),
2387
2564
  diagnostics
2388
2565
  };
@@ -2390,7 +2567,7 @@
2390
2567
  function upgradeDocumentToDraftV2(document2, options = {}) {
2391
2568
  return convertAtlasDocumentToLegacyDraft(upgradeDocumentToV2(document2, options));
2392
2569
  }
2393
- function materializeAtlasDocument(document2) {
2570
+ function materializeAtlasDocument(document2, options = {}) {
2394
2571
  const system = document2.system ? {
2395
2572
  type: "system",
2396
2573
  id: document2.system.id,
@@ -2401,6 +2578,8 @@
2401
2578
  properties: materializeDraftSystemProperties(document2.system),
2402
2579
  info: materializeDraftSystemInfo(document2.system)
2403
2580
  } : null;
2581
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2582
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2404
2583
  return {
2405
2584
  format: "worldorbit",
2406
2585
  version: "1.0",
@@ -2408,7 +2587,8 @@
2408
2587
  system,
2409
2588
  groups: structuredClone(document2.groups ?? []),
2410
2589
  relations: structuredClone(document2.relations ?? []),
2411
- objects: document2.objects.map(cloneWorldOrbitObject)
2590
+ events: document2.events.map(cloneWorldOrbitEvent),
2591
+ objects
2412
2592
  };
2413
2593
  }
2414
2594
  function createDraftSystem(document2, defaults, atlasMetadata, annotations, diagnostics, preset) {
@@ -2533,6 +2713,7 @@
2533
2713
  summary: viewpoint.summary,
2534
2714
  focusObjectId: viewpoint.objectId,
2535
2715
  selectedObjectId: viewpoint.selectedObjectId,
2716
+ events: [...viewpoint.eventIds],
2536
2717
  projection: viewpoint.projection,
2537
2718
  preset: viewpoint.preset,
2538
2719
  zoom: viewpoint.scale,
@@ -2565,6 +2746,52 @@
2565
2746
  info: { ...object.info }
2566
2747
  };
2567
2748
  }
2749
+ function cloneWorldOrbitEvent(event) {
2750
+ return {
2751
+ ...event,
2752
+ participantObjectIds: [...event.participantObjectIds],
2753
+ tags: [...event.tags],
2754
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2755
+ };
2756
+ }
2757
+ function cloneWorldOrbitEventPose(pose) {
2758
+ return {
2759
+ objectId: pose.objectId,
2760
+ placement: clonePlacement(pose.placement),
2761
+ inner: pose.inner ? { ...pose.inner } : void 0,
2762
+ outer: pose.outer ? { ...pose.outer } : void 0
2763
+ };
2764
+ }
2765
+ function clonePlacement(placement) {
2766
+ return placement ? structuredClone(placement) : null;
2767
+ }
2768
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2769
+ if (!activeEventId) {
2770
+ return;
2771
+ }
2772
+ const event = events.find((entry) => entry.id === activeEventId);
2773
+ if (!event) {
2774
+ return;
2775
+ }
2776
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2777
+ for (const pose of event.positions) {
2778
+ const object = objectMap.get(pose.objectId);
2779
+ if (!object) {
2780
+ continue;
2781
+ }
2782
+ object.placement = clonePlacement(pose.placement);
2783
+ if (pose.inner) {
2784
+ object.properties.inner = { ...pose.inner };
2785
+ } else {
2786
+ delete object.properties.inner;
2787
+ }
2788
+ if (pose.outer) {
2789
+ object.properties.outer = { ...pose.outer };
2790
+ } else {
2791
+ delete object.properties.outer;
2792
+ }
2793
+ }
2794
+ }
2568
2795
  function cloneProperties(properties) {
2569
2796
  const next = {};
2570
2797
  for (const [key, value] of Object.entries(properties)) {
@@ -2662,6 +2889,9 @@
2662
2889
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2663
2890
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2664
2891
  }
2892
+ if (viewpoint.events.length > 0) {
2893
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2894
+ }
2665
2895
  }
2666
2896
  for (const annotation of system.annotations) {
2667
2897
  const prefix = `annotation.${annotation.id}`;
@@ -2686,7 +2916,7 @@
2686
2916
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2687
2917
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2688
2918
  }
2689
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2919
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2690
2920
  if (layers[key] !== void 0) {
2691
2921
  tokens.push(layers[key] ? key : `-${key}`);
2692
2922
  }
@@ -2790,6 +3020,10 @@
2790
3020
  lines.push("");
2791
3021
  lines.push(...formatAtlasRelation(relation));
2792
3022
  }
3023
+ for (const event of [...document2.events].sort(compareIdLike)) {
3024
+ lines.push("");
3025
+ lines.push(...formatAtlasEvent(event));
3026
+ }
2793
3027
  const sortedObjects = [...document2.objects].sort(compareObjects);
2794
3028
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2795
3029
  lines.push("");
@@ -2820,6 +3054,10 @@
2820
3054
  lines.push("");
2821
3055
  lines.push(...formatAtlasRelation(relation));
2822
3056
  }
3057
+ for (const event of [...legacy.events].sort(compareIdLike)) {
3058
+ lines.push("");
3059
+ lines.push(...formatAtlasEvent(event));
3060
+ }
2823
3061
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2824
3062
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2825
3063
  lines.push("");
@@ -3031,6 +3269,9 @@
3031
3269
  if (layerTokens.length > 0) {
3032
3270
  lines.push(` layers ${layerTokens.join(" ")}`);
3033
3271
  }
3272
+ if (viewpoint.events.length > 0) {
3273
+ lines.push(` events ${viewpoint.events.join(" ")}`);
3274
+ }
3034
3275
  if (viewpoint.filter) {
3035
3276
  lines.push(" filter");
3036
3277
  if (viewpoint.filter.query) {
@@ -3103,6 +3344,54 @@
3103
3344
  }
3104
3345
  return lines;
3105
3346
  }
3347
+ function formatAtlasEvent(event) {
3348
+ const lines = [`event ${event.id}`, ` kind ${quoteIfNeeded(event.kind)}`];
3349
+ if (event.label) {
3350
+ lines.push(` label ${quoteIfNeeded(event.label)}`);
3351
+ }
3352
+ if (event.summary) {
3353
+ lines.push(` summary ${quoteIfNeeded(event.summary)}`);
3354
+ }
3355
+ if (event.targetObjectId) {
3356
+ lines.push(` target ${event.targetObjectId}`);
3357
+ }
3358
+ if (event.participantObjectIds.length > 0) {
3359
+ lines.push(` participants ${event.participantObjectIds.join(" ")}`);
3360
+ }
3361
+ if (event.timing) {
3362
+ lines.push(` timing ${quoteIfNeeded(event.timing)}`);
3363
+ }
3364
+ if (event.visibility) {
3365
+ lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
3366
+ }
3367
+ if (event.tags.length > 0) {
3368
+ lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3369
+ }
3370
+ if (event.color) {
3371
+ lines.push(` color ${quoteIfNeeded(event.color)}`);
3372
+ }
3373
+ if (event.hidden) {
3374
+ lines.push(" hidden true");
3375
+ }
3376
+ if (event.positions.length > 0) {
3377
+ lines.push("");
3378
+ lines.push(" positions");
3379
+ for (const pose of [...event.positions].sort(comparePoseObjectId)) {
3380
+ lines.push(` pose ${pose.objectId}`);
3381
+ for (const fieldLine of formatEventPoseFields(pose)) {
3382
+ lines.push(` ${fieldLine}`);
3383
+ }
3384
+ }
3385
+ }
3386
+ return lines;
3387
+ }
3388
+ function formatEventPoseFields(pose) {
3389
+ return [
3390
+ ...formatPlacement(pose.placement),
3391
+ ...formatOptionalUnit("inner", pose.inner),
3392
+ ...formatOptionalUnit("outer", pose.outer)
3393
+ ];
3394
+ }
3106
3395
  function formatValue(value) {
3107
3396
  if (Array.isArray(value)) {
3108
3397
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3144,7 +3433,7 @@
3144
3433
  if (orbitFront !== void 0 || orbitBack !== void 0) {
3145
3434
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
3146
3435
  }
3147
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3436
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
3148
3437
  if (layers[key] !== void 0) {
3149
3438
  tokens.push(layers[key] ? key : `-${key}`);
3150
3439
  }
@@ -3172,6 +3461,9 @@
3172
3461
  function compareIdLike(left, right) {
3173
3462
  return left.id.localeCompare(right.id);
3174
3463
  }
3464
+ function comparePoseObjectId(left, right) {
3465
+ return left.objectId.localeCompare(right.objectId);
3466
+ }
3175
3467
  function objectTypeIndex(objectType) {
3176
3468
  switch (objectType) {
3177
3469
  case "star":
@@ -3367,6 +3659,7 @@
3367
3659
  const diagnostics = [];
3368
3660
  const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
3369
3661
  const groupIds = new Set(document2.groups.map((group) => group.id));
3662
+ const eventIds = new Set(document2.events.map((event) => event.id));
3370
3663
  if (!document2.system) {
3371
3664
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3372
3665
  }
@@ -3376,6 +3669,7 @@
3376
3669
  ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3377
3670
  ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
3378
3671
  ["relation", document2.relations.map((relation) => relation.id)],
3672
+ ["event", document2.events.map((event) => event.id)],
3379
3673
  ["object", document2.objects.map((object) => object.id)]
3380
3674
  ]) {
3381
3675
  for (const id of ids) {
@@ -3391,11 +3685,14 @@
3391
3685
  validateRelation(relation, objectMap, diagnostics);
3392
3686
  }
3393
3687
  for (const viewpoint of document2.system?.viewpoints ?? []) {
3394
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3688
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3395
3689
  }
3396
3690
  for (const object of document2.objects) {
3397
3691
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3398
3692
  }
3693
+ for (const event of document2.events) {
3694
+ validateEvent(event, objectMap, diagnostics);
3695
+ }
3399
3696
  return diagnostics;
3400
3697
  }
3401
3698
  function validateRelation(relation, objectMap, diagnostics) {
@@ -3413,13 +3710,19 @@
3413
3710
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3414
3711
  }
3415
3712
  }
3416
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3417
- if (!filter || sourceSchemaVersion !== "2.1") {
3418
- return;
3419
- }
3420
- for (const groupId of filter.groupIds) {
3421
- if (!groupIds.has(groupId)) {
3422
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3713
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3714
+ if (sourceSchemaVersion === "2.1") {
3715
+ if (filter) {
3716
+ for (const groupId of filter.groupIds) {
3717
+ if (!groupIds.has(groupId)) {
3718
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3719
+ }
3720
+ }
3721
+ }
3722
+ for (const eventId of eventRefs) {
3723
+ if (!eventIds.has(eventId)) {
3724
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3725
+ }
3423
3726
  }
3424
3727
  }
3425
3728
  }
@@ -3505,6 +3808,103 @@
3505
3808
  }
3506
3809
  }
3507
3810
  }
3811
+ function validateEvent(event, objectMap, diagnostics) {
3812
+ const fieldPrefix = `event.${event.id}`;
3813
+ const referencedIds = /* @__PURE__ */ new Set();
3814
+ if (!event.kind.trim()) {
3815
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3816
+ }
3817
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3818
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3819
+ }
3820
+ if (event.targetObjectId) {
3821
+ referencedIds.add(event.targetObjectId);
3822
+ if (!objectMap.has(event.targetObjectId)) {
3823
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3824
+ }
3825
+ }
3826
+ const seenParticipants = /* @__PURE__ */ new Set();
3827
+ for (const participantId of event.participantObjectIds) {
3828
+ referencedIds.add(participantId);
3829
+ if (seenParticipants.has(participantId)) {
3830
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3831
+ continue;
3832
+ }
3833
+ seenParticipants.add(participantId);
3834
+ if (!objectMap.has(participantId)) {
3835
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3836
+ }
3837
+ }
3838
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3839
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3840
+ }
3841
+ if (event.positions.length === 0) {
3842
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3843
+ }
3844
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3845
+ 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`));
3846
+ }
3847
+ const poseIds = /* @__PURE__ */ new Set();
3848
+ for (const pose of event.positions) {
3849
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3850
+ if (poseIds.has(pose.objectId)) {
3851
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3852
+ continue;
3853
+ }
3854
+ poseIds.add(pose.objectId);
3855
+ const object = objectMap.get(pose.objectId);
3856
+ if (!object) {
3857
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3858
+ continue;
3859
+ }
3860
+ if (!referencedIds.has(pose.objectId)) {
3861
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3862
+ }
3863
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3864
+ }
3865
+ }
3866
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3867
+ const placement = pose.placement;
3868
+ if (!placement) {
3869
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3870
+ return;
3871
+ }
3872
+ if (placement.mode === "orbit") {
3873
+ if (!objectMap.has(placement.target)) {
3874
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3875
+ }
3876
+ if (placement.distance && placement.semiMajor) {
3877
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3878
+ }
3879
+ return;
3880
+ }
3881
+ if (placement.mode === "surface") {
3882
+ const target = objectMap.get(placement.target);
3883
+ if (!target) {
3884
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3885
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3886
+ 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`));
3887
+ }
3888
+ return;
3889
+ }
3890
+ if (placement.mode === "at") {
3891
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3892
+ 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`));
3893
+ }
3894
+ const reference = placement.reference;
3895
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3896
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3897
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3898
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3899
+ } else if (reference.kind === "lagrange") {
3900
+ if (!objectMap.has(reference.primary)) {
3901
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3902
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3903
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3904
+ }
3905
+ }
3906
+ }
3907
+ }
3508
3908
  function validateAtTarget(object, objectMap, diagnostics) {
3509
3909
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3510
3910
  if (!reference) {
@@ -3705,6 +4105,21 @@
3705
4105
  });
3706
4106
  }
3707
4107
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
4108
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
4109
+ "orbit",
4110
+ "distance",
4111
+ "semiMajor",
4112
+ "eccentricity",
4113
+ "period",
4114
+ "angle",
4115
+ "inclination",
4116
+ "phase",
4117
+ "at",
4118
+ "surface",
4119
+ "free",
4120
+ "inner",
4121
+ "outer"
4122
+ ]);
3708
4123
  function parseWorldOrbitAtlas(source) {
3709
4124
  return parseAtlasSource(source);
3710
4125
  }
@@ -3719,12 +4134,15 @@
3719
4134
  const objectNodes = [];
3720
4135
  const groups = [];
3721
4136
  const relations = [];
4137
+ const events = [];
4138
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3722
4139
  let sawDefaults = false;
3723
4140
  let sawAtlas = false;
3724
4141
  const viewpointIds = /* @__PURE__ */ new Set();
3725
4142
  const annotationIds = /* @__PURE__ */ new Set();
3726
4143
  const groupIds = /* @__PURE__ */ new Set();
3727
4144
  const relationIds = /* @__PURE__ */ new Set();
4145
+ const eventIds = /* @__PURE__ */ new Set();
3728
4146
  for (let index = 0; index < lines.length; index++) {
3729
4147
  const rawLine = lines[index];
3730
4148
  const lineNumber = index + 1;
@@ -3755,7 +4173,7 @@
3755
4173
  continue;
3756
4174
  }
3757
4175
  if (indent === 0) {
3758
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
4176
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3759
4177
  if (section.kind === "system") {
3760
4178
  system = section.system;
3761
4179
  } else if (section.kind === "defaults") {
@@ -3774,6 +4192,7 @@
3774
4192
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3775
4193
  }
3776
4194
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4195
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3777
4196
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3778
4197
  const baseDocument = {
3779
4198
  format: "worldorbit",
@@ -3781,6 +4200,7 @@
3781
4200
  system,
3782
4201
  groups,
3783
4202
  relations,
4203
+ events: normalizedEvents,
3784
4204
  objects,
3785
4205
  diagnostics
3786
4206
  };
@@ -3816,7 +4236,7 @@
3816
4236
  const version = tokens[1].value.toLowerCase();
3817
4237
  return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3818
4238
  }
3819
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
4239
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3820
4240
  const keyword = tokens[0]?.value.toLowerCase();
3821
4241
  switch (keyword) {
3822
4242
  case "system":
@@ -3853,7 +4273,7 @@
3853
4273
  if (!system) {
3854
4274
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3855
4275
  }
3856
- return startViewpointSection(tokens, line, system, viewpointIds);
4276
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3857
4277
  case "annotation":
3858
4278
  if (!system) {
3859
4279
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3865,6 +4285,9 @@
3865
4285
  case "relation":
3866
4286
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3867
4287
  return startRelationSection(tokens, line, relations, relationIds);
4288
+ case "event":
4289
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4290
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3868
4291
  case "object":
3869
4292
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3870
4293
  default:
@@ -3901,7 +4324,7 @@
3901
4324
  seenFields: /* @__PURE__ */ new Set()
3902
4325
  };
3903
4326
  }
3904
- function startViewpointSection(tokens, line, system, viewpointIds) {
4327
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3905
4328
  if (tokens.length !== 2) {
3906
4329
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3907
4330
  }
@@ -3918,6 +4341,7 @@
3918
4341
  summary: "",
3919
4342
  focusObjectId: null,
3920
4343
  selectedObjectId: null,
4344
+ events: [],
3921
4345
  projection: system.defaults.view,
3922
4346
  preset: system.defaults.preset,
3923
4347
  zoom: null,
@@ -3930,6 +4354,8 @@
3930
4354
  return {
3931
4355
  kind: "viewpoint",
3932
4356
  viewpoint,
4357
+ sourceSchemaVersion,
4358
+ diagnostics,
3933
4359
  seenFields: /* @__PURE__ */ new Set(),
3934
4360
  inFilter: false,
3935
4361
  filterIndent: null,
@@ -4020,6 +4446,49 @@
4020
4446
  seenFields: /* @__PURE__ */ new Set()
4021
4447
  };
4022
4448
  }
4449
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4450
+ if (tokens.length !== 2) {
4451
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4452
+ }
4453
+ const id = normalizeIdentifier2(tokens[1].value);
4454
+ if (!id) {
4455
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4456
+ }
4457
+ if (eventIds.has(id)) {
4458
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4459
+ }
4460
+ const event = {
4461
+ id,
4462
+ kind: "",
4463
+ label: humanizeIdentifier3(id),
4464
+ summary: null,
4465
+ targetObjectId: null,
4466
+ participantObjectIds: [],
4467
+ timing: null,
4468
+ visibility: null,
4469
+ tags: [],
4470
+ color: null,
4471
+ hidden: false,
4472
+ positions: []
4473
+ };
4474
+ const rawPoses = [];
4475
+ events.push(event);
4476
+ eventPoseNodes.set(id, rawPoses);
4477
+ eventIds.add(id);
4478
+ return {
4479
+ kind: "event",
4480
+ event,
4481
+ sourceSchemaVersion,
4482
+ diagnostics,
4483
+ seenFields: /* @__PURE__ */ new Set(),
4484
+ rawPoses,
4485
+ inPositions: false,
4486
+ positionsIndent: null,
4487
+ activePose: null,
4488
+ poseIndent: null,
4489
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4490
+ };
4491
+ }
4023
4492
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
4024
4493
  if (tokens.length < 3) {
4025
4494
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -4076,6 +4545,9 @@
4076
4545
  case "relation":
4077
4546
  applyRelationField(section, tokens, line);
4078
4547
  return;
4548
+ case "event":
4549
+ applyEventField(section, indent, tokens, line);
4550
+ return;
4079
4551
  case "object":
4080
4552
  applyObjectField(section, indent, tokens, line);
4081
4553
  return;
@@ -4202,7 +4674,14 @@
4202
4674
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
4203
4675
  return;
4204
4676
  case "layers":
4205
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4677
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4678
+ return;
4679
+ case "events":
4680
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4681
+ line,
4682
+ column: tokens[0].column
4683
+ });
4684
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
4206
4685
  return;
4207
4686
  default:
4208
4687
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -4307,6 +4786,106 @@
4307
4786
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4308
4787
  }
4309
4788
  }
4789
+ function applyEventField(section, indent, tokens, line) {
4790
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4791
+ section.activePose = null;
4792
+ section.poseIndent = null;
4793
+ section.activePoseSeenFields.clear();
4794
+ }
4795
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4796
+ section.inPositions = false;
4797
+ section.positionsIndent = null;
4798
+ }
4799
+ if (section.activePose) {
4800
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4801
+ return;
4802
+ }
4803
+ if (section.inPositions) {
4804
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4805
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4806
+ }
4807
+ const objectId = tokens[1].value;
4808
+ if (!objectId.trim()) {
4809
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4810
+ }
4811
+ const rawPose = {
4812
+ objectId,
4813
+ fields: [],
4814
+ location: { line, column: tokens[0].column }
4815
+ };
4816
+ section.rawPoses.push(rawPose);
4817
+ section.activePose = rawPose;
4818
+ section.poseIndent = indent;
4819
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4820
+ return;
4821
+ }
4822
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4823
+ if (section.seenFields.has("positions")) {
4824
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4825
+ }
4826
+ section.seenFields.add("positions");
4827
+ section.inPositions = true;
4828
+ section.positionsIndent = indent;
4829
+ return;
4830
+ }
4831
+ const key = requireUniqueField(tokens, section.seenFields, line);
4832
+ switch (key) {
4833
+ case "kind":
4834
+ section.event.kind = joinFieldValue(tokens, line);
4835
+ return;
4836
+ case "label":
4837
+ section.event.label = joinFieldValue(tokens, line);
4838
+ return;
4839
+ case "summary":
4840
+ section.event.summary = joinFieldValue(tokens, line);
4841
+ return;
4842
+ case "target":
4843
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4844
+ return;
4845
+ case "participants":
4846
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4847
+ return;
4848
+ case "timing":
4849
+ section.event.timing = joinFieldValue(tokens, line);
4850
+ return;
4851
+ case "visibility":
4852
+ section.event.visibility = joinFieldValue(tokens, line);
4853
+ return;
4854
+ case "tags":
4855
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4856
+ return;
4857
+ case "color":
4858
+ section.event.color = joinFieldValue(tokens, line);
4859
+ return;
4860
+ case "hidden":
4861
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4862
+ line,
4863
+ column: tokens[0].column
4864
+ });
4865
+ return;
4866
+ default:
4867
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4868
+ }
4869
+ }
4870
+ function parseEventPoseField(tokens, line, seenFields) {
4871
+ if (tokens.length < 2) {
4872
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4873
+ }
4874
+ const key = tokens[0].value;
4875
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4876
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4877
+ }
4878
+ if (seenFields.has(key)) {
4879
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4880
+ }
4881
+ seenFields.add(key);
4882
+ return {
4883
+ type: "field",
4884
+ key,
4885
+ values: tokens.slice(1).map((token) => token.value),
4886
+ location: { line, column: tokens[0].column }
4887
+ };
4888
+ }
4310
4889
  function applyObjectField(section, indent, tokens, line) {
4311
4890
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4312
4891
  section.activeBlock = null;
@@ -4365,7 +4944,7 @@
4365
4944
  function parseObjectTypeTokens(tokens, line) {
4366
4945
  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");
4367
4946
  }
4368
- function parseLayerTokens(tokens, line) {
4947
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4369
4948
  const layers = {};
4370
4949
  for (const token of parseTokenList(tokens, line, "layers")) {
4371
4950
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -4375,7 +4954,13 @@
4375
4954
  layers["orbits-front"] = enabled;
4376
4955
  continue;
4377
4956
  }
4378
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4957
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4958
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4959
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4960
+ line,
4961
+ column: tokens[0]?.column ?? 1
4962
+ });
4963
+ }
4379
4964
  layers[raw] = enabled;
4380
4965
  }
4381
4966
  }
@@ -4514,7 +5099,7 @@
4514
5099
  }
4515
5100
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4516
5101
  const fieldMap = collectDraftFields(node.fields);
4517
- const placement = extractDraftPlacement(node.objectType, fieldMap);
5102
+ const placement = extractPlacementFromFieldMap(fieldMap);
4518
5103
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
4519
5104
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4520
5105
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -4566,6 +5151,24 @@
4566
5151
  }
4567
5152
  return object;
4568
5153
  }
5154
+ function normalizeDraftEvent(event, rawPoses) {
5155
+ return {
5156
+ ...event,
5157
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5158
+ tags: [...new Set(event.tags)],
5159
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5160
+ };
5161
+ }
5162
+ function normalizeDraftEventPose(rawPose) {
5163
+ const fieldMap = collectDraftFields(rawPose.fields);
5164
+ const placement = extractPlacementFromFieldMap(fieldMap);
5165
+ return {
5166
+ objectId: rawPose.objectId,
5167
+ placement,
5168
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5169
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
5170
+ };
5171
+ }
4569
5172
  function collectDraftFields(fields) {
4570
5173
  const grouped = /* @__PURE__ */ new Map();
4571
5174
  for (const field of fields) {
@@ -4582,7 +5185,7 @@
4582
5185
  }
4583
5186
  return grouped;
4584
5187
  }
4585
- function extractDraftPlacement(objectType, fieldMap) {
5188
+ function extractPlacementFromFieldMap(fieldMap) {
4586
5189
  const orbitField = fieldMap.get("orbit")?.[0];
4587
5190
  const atField = fieldMap.get("at")?.[0];
4588
5191
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4875,6 +5478,7 @@
4875
5478
  },
4876
5479
  groups: [],
4877
5480
  relations: [],
5481
+ events: [],
4878
5482
  objects: [],
4879
5483
  diagnostics: []
4880
5484
  };
@@ -4892,6 +5496,10 @@
4892
5496
  return path.key ? document2.system?.atlasMetadata[path.key] ?? null : null;
4893
5497
  case "group":
4894
5498
  return path.id ? findGroup(document2, path.id) : null;
5499
+ case "event":
5500
+ return path.id ? findEvent(document2, path.id) : null;
5501
+ case "event-pose":
5502
+ return path.id && path.key ? findEventPose(document2, path.id, path.key) : null;
4895
5503
  case "object":
4896
5504
  return path.id ? findObject(document2, path.id) : null;
4897
5505
  case "viewpoint":
@@ -4921,6 +5529,19 @@
4921
5529
  next.groups = next.groups.filter((group) => group.id !== path.id);
4922
5530
  }
4923
5531
  return next;
5532
+ case "event":
5533
+ if (path.id) {
5534
+ next.events = next.events.filter((event) => event.id !== path.id);
5535
+ }
5536
+ return next;
5537
+ case "event-pose":
5538
+ if (path.id && path.key) {
5539
+ const event = findEvent(next, path.id);
5540
+ if (event) {
5541
+ event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
5542
+ }
5543
+ }
5544
+ return next;
4924
5545
  case "viewpoint":
4925
5546
  if (path.id) {
4926
5547
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -4989,6 +5610,22 @@
4989
5610
  };
4990
5611
  }
4991
5612
  }
5613
+ if (diagnostic.field?.startsWith("event.")) {
5614
+ const parts = diagnostic.field.split(".");
5615
+ if (parts[1] && findEvent(document2, parts[1])) {
5616
+ if (parts[2] === "pose" && parts[3] && findEventPose(document2, parts[1], parts[3])) {
5617
+ return {
5618
+ kind: "event-pose",
5619
+ id: parts[1],
5620
+ key: parts[3]
5621
+ };
5622
+ }
5623
+ return {
5624
+ kind: "event",
5625
+ id: parts[1]
5626
+ };
5627
+ }
5628
+ }
4992
5629
  if (diagnostic.field && diagnostic.field in ensureSystem(document2).atlasMetadata) {
4993
5630
  return {
4994
5631
  kind: "metadata",
@@ -5020,6 +5657,12 @@
5020
5657
  function findRelation(document2, relationId) {
5021
5658
  return document2.relations.find((relation) => relation.id === relationId) ?? null;
5022
5659
  }
5660
+ function findEvent(document2, eventId) {
5661
+ return document2.events.find((event) => event.id === eventId) ?? null;
5662
+ }
5663
+ function findEventPose(document2, eventId, objectId) {
5664
+ return findEvent(document2, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
5665
+ }
5023
5666
  function findViewpoint(system, viewpointId) {
5024
5667
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
5025
5668
  }
@@ -5197,6 +5840,7 @@
5197
5840
  background: true,
5198
5841
  guides: true,
5199
5842
  relations: true,
5843
+ events: true,
5200
5844
  orbits: true,
5201
5845
  objects: true,
5202
5846
  labels: true,
@@ -5345,12 +5989,14 @@
5345
5989
  return {
5346
5990
  version: "2.0",
5347
5991
  viewpointId,
5992
+ activeEventId: renderOptions.activeEventId ?? null,
5348
5993
  viewerState: { ...viewerState },
5349
5994
  renderOptions: {
5350
5995
  preset: renderOptions.preset,
5351
5996
  projection: renderOptions.projection,
5352
5997
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
5353
- scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0
5998
+ scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
5999
+ activeEventId: renderOptions.activeEventId ?? null
5354
6000
  },
5355
6001
  filter: normalizeViewerFilter(filter)
5356
6002
  };
@@ -5363,6 +6009,7 @@
5363
6009
  return {
5364
6010
  version: "2.0",
5365
6011
  viewpointId: raw.viewpointId ?? null,
6012
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
5366
6013
  viewerState: {
5367
6014
  scale: raw.viewerState?.scale ?? 1,
5368
6015
  rotationDeg: raw.viewerState?.rotationDeg ?? 0,
@@ -5374,7 +6021,8 @@
5374
6021
  preset: raw.renderOptions?.preset,
5375
6022
  projection: raw.renderOptions?.projection,
5376
6023
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : void 0,
5377
- scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0
6024
+ scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0,
6025
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null
5378
6026
  },
5379
6027
  filter: normalizeViewerFilter(raw.filter ?? null)
5380
6028
  };
@@ -5390,7 +6038,8 @@
5390
6038
  renderOptions: {
5391
6039
  ...atlasState.renderOptions,
5392
6040
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : void 0,
5393
- scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0
6041
+ scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0,
6042
+ activeEventId: atlasState.renderOptions.activeEventId ?? null
5394
6043
  },
5395
6044
  filter: atlasState.filter ? { ...atlasState.filter } : null
5396
6045
  }
@@ -5408,6 +6057,7 @@
5408
6057
  background: viewpoint.layers.background,
5409
6058
  guides: viewpoint.layers.guides,
5410
6059
  relations: viewpoint.layers.relations,
6060
+ events: viewpoint.layers.events,
5411
6061
  orbits: viewpoint.layers["orbits-front"] === void 0 && viewpoint.layers["orbits-back"] === void 0 ? void 0 : viewpoint.layers["orbits-front"] !== false || viewpoint.layers["orbits-back"] !== false,
5412
6062
  objects: viewpoint.layers.objects,
5413
6063
  labels: viewpoint.layers.labels,
@@ -5694,6 +6344,7 @@
5694
6344
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
5695
6345
  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("") : "";
5696
6346
  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("") : "";
6347
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
5697
6348
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
5698
6349
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
5699
6350
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -5729,6 +6380,9 @@
5729
6380
  .wo-orbit-front { opacity: 0.9; }
5730
6381
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
5731
6382
  .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
6383
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
6384
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
6385
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
5732
6386
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
5733
6387
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
5734
6388
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -5763,6 +6417,7 @@
5763
6417
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
5764
6418
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
5765
6419
  ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
6420
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
5766
6421
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
5767
6422
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
5768
6423
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -5770,6 +6425,20 @@
5770
6425
  </g>
5771
6426
  </g>
5772
6427
  </svg>`;
6428
+ }
6429
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
6430
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
6431
+ if (participants.length === 0) {
6432
+ return "";
6433
+ }
6434
+ const stroke = event.event.color || theme.accent;
6435
+ const label = event.event.label || event.event.id;
6436
+ 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("");
6437
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
6438
+ ${lineMarkup}
6439
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
6440
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
6441
+ </g>`;
5773
6442
  }
5774
6443
  function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
5775
6444
  const backParts = [];
@@ -6352,6 +7021,13 @@
6352
7021
  value: `${details.object.resonance.targetObjectId} ${details.object.resonance.ratio}`
6353
7022
  });
6354
7023
  }
7024
+ if (details.relatedEvents.length > 0) {
7025
+ fields.set("events", {
7026
+ key: "events",
7027
+ label: "Events",
7028
+ value: details.relatedEvents.map((event) => event.event.label || event.event.id).join(", ")
7029
+ });
7030
+ }
6355
7031
  if (placement?.mode === "at") {
6356
7032
  fields.set("placement", {
6357
7033
  key: "placement",
@@ -6766,6 +7442,12 @@
6766
7442
  emitAtlasStateChange();
6767
7443
  return true;
6768
7444
  },
7445
+ getActiveEventId() {
7446
+ return renderOptions.activeEventId ?? null;
7447
+ },
7448
+ setActiveEvent(id) {
7449
+ api.setRenderOptions({ activeEventId: id });
7450
+ },
6769
7451
  search(query, limit = 12) {
6770
7452
  return searchSceneObjects(scene, query, limit);
6771
7453
  },
@@ -7030,6 +7712,7 @@
7030
7712
  orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
7031
7713
  relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden && (orbit.objectId === renderObject.objectId || renderObject.ancestorIds.includes(orbit.objectId) || renderObject.childIds.includes(orbit.objectId))),
7032
7714
  relations: scene.relations.filter((relation) => !relation.hidden && (relation.fromObjectId === renderObject.objectId || relation.toObjectId === renderObject.objectId)),
7715
+ relatedEvents: scene.events.filter((event) => !event.hidden && (event.targetObjectId === renderObject.objectId || event.objectIds.includes(renderObject.objectId))),
7033
7716
  parent: getObjectById(renderObject.parentId),
7034
7717
  children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
7035
7718
  ancestors: renderObject.ancestorIds.map((ancestorId) => getObjectById(ancestorId)).filter(Boolean),
@@ -7346,7 +8029,8 @@
7346
8029
  filter: renderOptions.filter ? { ...renderOptions.filter } : void 0,
7347
8030
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
7348
8031
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
7349
- theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme
8032
+ theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme,
8033
+ activeEventId: renderOptions.activeEventId ?? null
7350
8034
  };
7351
8035
  }
7352
8036
  function mergeRenderOptions(current, next) {
@@ -7366,7 +8050,7 @@
7366
8050
  };
7367
8051
  }
7368
8052
  function hasSceneAffectingRenderOptions(options) {
7369
- return options.width !== void 0 || options.height !== void 0 || options.padding !== void 0 || options.preset !== void 0 || options.projection !== void 0 || options.scaleModel !== void 0;
8053
+ return options.width !== void 0 || options.height !== void 0 || options.padding !== void 0 || options.preset !== void 0 || options.projection !== void 0 || options.scaleModel !== void 0 || options.activeEventId !== void 0;
7370
8054
  }
7371
8055
  function resolveSourceRenderOptions(loaded, renderOptions) {
7372
8056
  const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
@@ -7699,6 +8383,34 @@
7699
8383
  description: "Rotates the saved camera angle in degrees.",
7700
8384
  references: ["90deg = quarter turn", "180deg = flip"]
7701
8385
  },
8386
+ "viewpoint-events": {
8387
+ description: "Lists event IDs that this viewpoint should feature in its detail panel.",
8388
+ references: ["solar-eclipse-naar", "transit-window conjunction"]
8389
+ },
8390
+ "event-kind": {
8391
+ description: "Short semantic event type for tooling and viewer overlays.",
8392
+ references: ["solar-eclipse", "lunar-eclipse", "transit"]
8393
+ },
8394
+ "event-target": {
8395
+ description: "Primary object this event is centered on.",
8396
+ references: ["Naar", "Seyra"]
8397
+ },
8398
+ "event-participants": {
8399
+ description: "Objects that participate in the event snapshot or description.",
8400
+ references: ["Iyath Naar Seyra", "Naar Seyra Orun"]
8401
+ },
8402
+ "event-timing": {
8403
+ description: "Free-text timing note for the event.",
8404
+ references: ['"Every late bloom season"', '"At local midyear"']
8405
+ },
8406
+ "event-visibility": {
8407
+ description: "Notes where or how the event is visible.",
8408
+ references: ['"Visible from Naar"', '"Southern hemisphere only"']
8409
+ },
8410
+ "event-viewpoints": {
8411
+ description: "Viewpoint IDs that should list this event prominently.",
8412
+ references: ["naar-system", "overview inner-system"]
8413
+ },
7702
8414
  "placement-target": {
7703
8415
  description: "Names the body or reference this object is attached to.",
7704
8416
  references: ["orbit Primary", "surface Homeworld", "at Naar:L4"]
@@ -7817,12 +8529,23 @@
7817
8529
  minimap: true,
7818
8530
  tooltipMode: "hover",
7819
8531
  onSelectionChange(selectedObject) {
8532
+ const activeEventId = selection ? selectionEventId(selection) : null;
7820
8533
  if (ignoreViewerSelection || !selectedObject) {
7821
- if (!ignoreViewerSelection && selection?.kind === "object") {
7822
- setSelection(null, false, true);
8534
+ if (!ignoreViewerSelection) {
8535
+ if (selection?.kind === "event-pose" && selection.id) {
8536
+ setSelection({ kind: "event", id: selection.id }, false, true);
8537
+ } else if (selection?.kind === "object") {
8538
+ setSelection(activeEventId ? { kind: "event", id: activeEventId } : null, false, true);
8539
+ } else if (selection?.kind === "event" && selection.id) {
8540
+ setSelection({ kind: "event", id: selection.id }, false, true);
8541
+ }
7823
8542
  }
7824
8543
  return;
7825
8544
  }
8545
+ if (activeEventId && findEventPose2(atlasDocument, activeEventId, selectedObject.objectId)) {
8546
+ setSelection({ kind: "event-pose", id: activeEventId, key: selectedObject.objectId }, false, true);
8547
+ return;
8548
+ }
7826
8549
  setSelection({ kind: "object", id: selectedObject.objectId }, false, true);
7827
8550
  },
7828
8551
  onViewChange() {
@@ -7832,6 +8555,7 @@
7832
8555
  toolbar.addEventListener("click", handleToolbarClick);
7833
8556
  outline.addEventListener("click", handleOutlineClick);
7834
8557
  overlay.addEventListener("pointerdown", handleOverlayPointerDown);
8558
+ inspector?.addEventListener("click", handleInspectorClick);
7835
8559
  inspector?.addEventListener("input", handleInspectorInput);
7836
8560
  inspector?.addEventListener("change", handleInspectorChange);
7837
8561
  sourcePane?.addEventListener("input", handleSourceInput);
@@ -7906,6 +8630,28 @@
7906
8630
  replaceAtlasDocument(nextDocument, true, { kind: "object", id });
7907
8631
  return id;
7908
8632
  },
8633
+ addEvent() {
8634
+ const id = createUniqueId("event", atlasDocument.events.map((event) => event.id));
8635
+ const created = {
8636
+ id,
8637
+ kind: "",
8638
+ label: humanizeIdentifier4(id),
8639
+ summary: null,
8640
+ targetObjectId: null,
8641
+ participantObjectIds: [],
8642
+ timing: null,
8643
+ visibility: null,
8644
+ tags: [],
8645
+ color: null,
8646
+ hidden: false,
8647
+ positions: []
8648
+ };
8649
+ const nextDocument = cloneAtlasDocument(atlasDocument);
8650
+ nextDocument.events.push(created);
8651
+ nextDocument.events.sort(compareEvents);
8652
+ replaceAtlasDocument(nextDocument, true, { kind: "event", id });
8653
+ return id;
8654
+ },
7909
8655
  addViewpoint() {
7910
8656
  const id = createUniqueId("viewpoint", atlasDocument.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []);
7911
8657
  const created = {
@@ -7914,6 +8660,7 @@
7914
8660
  summary: "",
7915
8661
  focusObjectId: null,
7916
8662
  selectedObjectId: null,
8663
+ events: [],
7917
8664
  projection: atlasDocument.system?.defaults.view ?? "topdown",
7918
8665
  preset: atlasDocument.system?.defaults.preset ?? null,
7919
8666
  zoom: null,
@@ -7974,6 +8721,7 @@
7974
8721
  toolbar.removeEventListener("click", handleToolbarClick);
7975
8722
  outline.removeEventListener("click", handleOutlineClick);
7976
8723
  overlay.removeEventListener("pointerdown", handleOverlayPointerDown);
8724
+ inspector?.removeEventListener("click", handleInspectorClick);
7977
8725
  inspector?.removeEventListener("input", handleInspectorInput);
7978
8726
  inspector?.removeEventListener("change", handleInspectorChange);
7979
8727
  sourcePane?.removeEventListener("input", handleSourceInput);
@@ -8019,7 +8767,7 @@
8019
8767
  }
8020
8768
  function getCurrentSourceForExport() {
8021
8769
  if (dragState?.changed) {
8022
- return formatDocument(atlasDocument, { schema: "2.0" });
8770
+ return formatAtlasSource(atlasDocument);
8023
8771
  }
8024
8772
  return canonicalSource;
8025
8773
  }
@@ -8058,7 +8806,7 @@
8058
8806
  }
8059
8807
  clearSourceInputTimer();
8060
8808
  atlasDocument = cloneAtlasDocument(nextDocument);
8061
- canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
8809
+ canonicalSource = formatAtlasSource(atlasDocument);
8062
8810
  if (!preserveSourceText) {
8063
8811
  sourceText = canonicalSource;
8064
8812
  }
@@ -8099,10 +8847,10 @@
8099
8847
  if (commitHistory) {
8100
8848
  history.push(createHistoryEntry());
8101
8849
  future.length = 0;
8102
- sourceText = formatDocument(nextDocument, { schema: "2.0" });
8850
+ sourceText = formatAtlasSource(nextDocument);
8103
8851
  }
8104
8852
  atlasDocument = cloneAtlasDocument(nextDocument);
8105
- canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
8853
+ canonicalSource = formatAtlasSource(atlasDocument);
8106
8854
  diagnostics = mergeDiagnostics(loadedDiagnostics, collectDocumentDiagnostics(atlasDocument));
8107
8855
  selection = normalizeSelection(selection);
8108
8856
  syncViewer({
@@ -8121,11 +8869,13 @@
8121
8869
  const previousState = viewer.getState();
8122
8870
  const currentRenderOptions = viewer.getRenderOptions();
8123
8871
  const nextPreset = atlasDocument.system?.defaults.preset ?? "atlas-card";
8872
+ const nextActiveEventId = selection ? selectionEventId(selection) : null;
8124
8873
  ignoreViewerSelection = true;
8125
- if (currentRenderOptions.preset !== nextPreset || currentRenderOptions.projection !== "document") {
8874
+ if (currentRenderOptions.preset !== nextPreset || currentRenderOptions.projection !== "document" || (currentRenderOptions.activeEventId ?? null) !== nextActiveEventId) {
8126
8875
  viewer.setRenderOptions({
8127
8876
  preset: nextPreset,
8128
- projection: "document"
8877
+ projection: "document",
8878
+ activeEventId: nextActiveEventId
8129
8879
  });
8130
8880
  }
8131
8881
  viewer.setDocument(materializeAtlasDocument(atlasDocument));
@@ -8134,10 +8884,12 @@
8134
8884
  } else if (options2.preserveCamera !== false) {
8135
8885
  viewer.setState({
8136
8886
  ...previousState,
8137
- selectedObjectId: selection?.kind === "object" ? selection.id ?? null : null
8887
+ selectedObjectId: selection?.kind === "object" ? selection.id ?? null : selection?.kind === "event-pose" ? selection.key ?? null : null
8138
8888
  });
8139
8889
  } else if (selection?.kind === "object" && selection.id) {
8140
8890
  viewer.focusObject(selection.id);
8891
+ } else if (selection?.kind === "event-pose" && selection.key) {
8892
+ viewer.focusObject(selection.key);
8141
8893
  }
8142
8894
  ignoreViewerSelection = false;
8143
8895
  }
@@ -8156,8 +8908,11 @@
8156
8908
  selection = normalizeSelection(nextSelection);
8157
8909
  if (syncViewerSelection) {
8158
8910
  ignoreViewerSelection = true;
8911
+ viewer.setRenderOptions({ activeEventId: selection ? selectionEventId(selection) : null });
8159
8912
  if (selection?.kind === "object" && selection.id) {
8160
8913
  viewer.focusObject(selection.id);
8914
+ } else if (selection?.kind === "event-pose" && selection.key) {
8915
+ viewer.focusObject(selection.key);
8161
8916
  } else if (selection?.kind === "viewpoint" && selection.id) {
8162
8917
  viewer.goToViewpoint(selection.id);
8163
8918
  }
@@ -8207,6 +8962,7 @@
8207
8962
  ${OBJECT_TYPES.map((type) => `<option value="${escapeHtml3(type)}"${type === objectType ? " selected" : ""}>${escapeHtml3(humanizeIdentifier4(type))}</option>`).join("")}
8208
8963
  </select>
8209
8964
  <button type="button" data-editor-action="add-object">Add object</button>
8965
+ <button type="button" data-editor-action="add-event">Add event</button>
8210
8966
  <button type="button" data-editor-action="add-viewpoint">Add viewpoint</button>
8211
8967
  <button type="button" data-editor-action="add-annotation">Add annotation</button>
8212
8968
  <button type="button" data-editor-action="add-metadata">Add metadata</button>
@@ -8241,6 +8997,10 @@
8241
8997
  <h3>Annotations</h3>
8242
8998
  ${(atlasDocument.system?.annotations.length ?? 0) > 0 ? atlasDocument.system?.annotations.map((annotation) => renderOutlineButton({ kind: "annotation", id: annotation.id }, annotation.label, activeKey, diagnosticBuckets)).join("") : `<p class="wo-editor-empty">No annotations yet.</p>`}
8243
8999
  </div>
9000
+ <div class="wo-editor-outline-section">
9001
+ <h3>Events</h3>
9002
+ ${atlasDocument.events.length > 0 ? atlasDocument.events.map((eventEntry) => renderEventOutlineItems(eventEntry, activeKey, diagnosticBuckets)).join("") : `<p class="wo-editor-empty">No events yet.</p>`}
9003
+ </div>
8244
9004
  <div class="wo-editor-outline-section">
8245
9005
  <h3>Objects</h3>
8246
9006
  ${atlasDocument.objects.length > 0 ? atlasDocument.objects.map((object) => renderOutlineButton({ kind: "object", id: object.id }, `${object.id} - ${object.type}`, activeKey, diagnosticBuckets)).join("") : `<p class="wo-editor-empty">No objects yet.</p>`}
@@ -8278,6 +9038,7 @@
8278
9038
  selection: selection ? { path: { ...selection } } : null,
8279
9039
  system: atlasDocument.system,
8280
9040
  viewpoints: atlasDocument.system?.viewpoints ?? [],
9041
+ events: atlasDocument.events,
8281
9042
  objects: atlasDocument.objects
8282
9043
  };
8283
9044
  if (!selection) {
@@ -8306,6 +9067,16 @@
8306
9067
  applyInspectorSectionState(inspector, inspectorSectionState);
8307
9068
  decorateInspectorDiagnostics(selection, diagnostics);
8308
9069
  return;
9070
+ case "event":
9071
+ inspector.innerHTML = diagnosticSummary + renderEventInspector(formState, selection.id ?? "");
9072
+ applyInspectorSectionState(inspector, inspectorSectionState);
9073
+ decorateInspectorDiagnostics(selection, diagnostics);
9074
+ return;
9075
+ case "event-pose":
9076
+ inspector.innerHTML = diagnosticSummary + renderEventPoseInspector(formState, selection.id ?? "", selection.key ?? "");
9077
+ applyInspectorSectionState(inspector, inspectorSectionState);
9078
+ decorateInspectorDiagnostics(selection, diagnostics);
9079
+ return;
8309
9080
  case "annotation":
8310
9081
  inspector.innerHTML = diagnosticSummary + renderAnnotationInspector(formState, selection.id ?? "");
8311
9082
  applyInspectorSectionState(inspector, inspectorSectionState);
@@ -8338,10 +9109,11 @@
8338
9109
  return;
8339
9110
  }
8340
9111
  overlay.innerHTML = "";
8341
- if (selection?.kind !== "object" || !selection.id) {
9112
+ const selectedObjectId = selection?.kind === "object" ? selection.id ?? null : selection?.kind === "event-pose" ? selection.key ?? null : null;
9113
+ if (!selectedObjectId) {
8342
9114
  return;
8343
9115
  }
8344
- const details = viewer.getObjectDetails(selection.id);
9116
+ const details = viewer.getObjectDetails(selectedObjectId);
8345
9117
  if (!details) {
8346
9118
  return;
8347
9119
  }
@@ -8453,6 +9225,9 @@
8453
9225
  case "add-viewpoint":
8454
9226
  api.addViewpoint();
8455
9227
  return;
9228
+ case "add-event":
9229
+ api.addEvent();
9230
+ return;
8456
9231
  case "add-annotation":
8457
9232
  api.addAnnotation();
8458
9233
  return;
@@ -8487,6 +9262,32 @@
8487
9262
  key: button.dataset.pathKey || void 0
8488
9263
  }, true, true);
8489
9264
  }
9265
+ function handleInspectorClick(event) {
9266
+ const pathButton = event.target?.closest("[data-path-kind]");
9267
+ if (pathButton) {
9268
+ setSelection({
9269
+ kind: pathButton.dataset.pathKind,
9270
+ id: pathButton.dataset.pathId || void 0,
9271
+ key: pathButton.dataset.pathKey || void 0
9272
+ }, true, true);
9273
+ return;
9274
+ }
9275
+ const actionButton = event.target?.closest("[data-editor-action]");
9276
+ if (!actionButton) {
9277
+ return;
9278
+ }
9279
+ if (actionButton.dataset.editorAction === "add-event-pose") {
9280
+ const eventId = actionButton.dataset.editorEventId || (selection?.kind === "event" || selection?.kind === "event-pose" ? selection.id ?? "" : "");
9281
+ if (!eventId) {
9282
+ return;
9283
+ }
9284
+ const nextDocument = addEventPose(atlasDocument, eventId);
9285
+ const createdEvent = nextDocument.events.find((entry) => entry.id === eventId);
9286
+ const createdPose = createdEvent?.positions.at(-1) ?? createdEvent?.positions[0];
9287
+ replaceAtlasDocument(nextDocument, true, createdPose ? { kind: "event-pose", id: eventId, key: createdPose.objectId } : { kind: "event", id: eventId });
9288
+ return;
9289
+ }
9290
+ }
8490
9291
  function handleInspectorInput() {
8491
9292
  applyInspectorState(false);
8492
9293
  }
@@ -8510,6 +9311,12 @@
8510
9311
  case "viewpoint":
8511
9312
  replaceAtlasDocument(buildViewpointDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
8512
9313
  return;
9314
+ case "event":
9315
+ replaceAtlasDocument(buildEventDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
9316
+ return;
9317
+ case "event-pose":
9318
+ replaceAtlasDocument(buildEventPoseDocumentFromInspector(selection.id ?? "", selection.key ?? ""), commitHistory, selection, false);
9319
+ return;
8513
9320
  case "annotation":
8514
9321
  replaceAtlasDocument(buildAnnotationDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
8515
9322
  return;
@@ -8578,6 +9385,7 @@
8578
9385
  kind,
8579
9386
  objectId,
8580
9387
  pointerId: event.pointerId,
9388
+ path: selection ? { ...selection } : { kind: "object", id: objectId },
8581
9389
  startedFrom: createHistoryEntry(),
8582
9390
  changed: false,
8583
9391
  orbitRadiusContext: kind === "orbit-radius" && details ? createOrbitRadiusDragContext(atlasDocument, viewer.getScene(), details) : null
@@ -8586,7 +9394,7 @@
8586
9394
  event.preventDefault();
8587
9395
  }
8588
9396
  function handleWindowPointerMove(event) {
8589
- if (!dragState || dragState.pointerId !== event.pointerId || selection?.kind !== "object" || selection.id !== dragState.objectId) {
9397
+ if (!dragState || dragState.pointerId !== event.pointerId || !selection || selectionKey(selection) !== selectionKey(dragState.path)) {
8590
9398
  return;
8591
9399
  }
8592
9400
  const details = viewer.getObjectDetails(dragState.objectId);
@@ -8598,27 +9406,27 @@
8598
9406
  switch (dragState.kind) {
8599
9407
  case "orbit-phase":
8600
9408
  if (details.object.placement?.mode === "orbit" && details.orbit) {
8601
- nextDocument = updateOrbitPhase(atlasDocument, dragState.objectId, details, pointer);
9409
+ nextDocument = updateOrbitPhase(atlasDocument, dragState.path, dragState.objectId, details, pointer);
8602
9410
  }
8603
9411
  break;
8604
9412
  case "orbit-radius":
8605
9413
  if (details.object.placement?.mode === "orbit" && details.orbit) {
8606
- nextDocument = updateOrbitRadius(atlasDocument, dragState.objectId, details, pointer, dragState.orbitRadiusContext ?? null);
9414
+ nextDocument = updateOrbitRadius(atlasDocument, dragState.path, dragState.objectId, details, pointer, dragState.orbitRadiusContext ?? null);
8607
9415
  }
8608
9416
  break;
8609
9417
  case "at-reference":
8610
9418
  if (details.object.placement?.mode === "at") {
8611
- nextDocument = updateAtReference(atlasDocument, dragState.objectId, viewer.getScene(), pointer);
9419
+ nextDocument = updateAtReference(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), pointer);
8612
9420
  }
8613
9421
  break;
8614
9422
  case "surface-target":
8615
9423
  if (details.object.placement?.mode === "surface") {
8616
- nextDocument = updateSurfaceTarget(atlasDocument, dragState.objectId, viewer.getScene(), pointer);
9424
+ nextDocument = updateSurfaceTarget(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), pointer);
8617
9425
  }
8618
9426
  break;
8619
9427
  case "free-distance":
8620
9428
  if (details.object.placement?.mode === "free") {
8621
- nextDocument = updateFreeDistance(atlasDocument, dragState.objectId, viewer.getScene(), details, pointer);
9429
+ nextDocument = updateFreeDistance(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), details, pointer);
8622
9430
  }
8623
9431
  break;
8624
9432
  }
@@ -8650,7 +9458,7 @@
8650
9458
  }
8651
9459
  history.push(dragState.startedFrom);
8652
9460
  future.length = 0;
8653
- canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
9461
+ canonicalSource = formatAtlasSource(atlasDocument);
8654
9462
  sourceText = canonicalSource;
8655
9463
  dragState = null;
8656
9464
  renderAll();
@@ -8753,10 +9561,12 @@
8753
9561
  guides: readCheckbox(form, "layer-guides"),
8754
9562
  "orbits-back": readCheckbox(form, "layer-orbits-back"),
8755
9563
  "orbits-front": readCheckbox(form, "layer-orbits-front"),
9564
+ events: readCheckbox(form, "layer-events"),
8756
9565
  objects: readCheckbox(form, "layer-objects"),
8757
9566
  labels: readCheckbox(form, "layer-labels"),
8758
9567
  metadata: readCheckbox(form, "layer-metadata")
8759
9568
  },
9569
+ events: splitTokens(readOptionalTextInput(form, "viewpoint-events")),
8760
9570
  filter: {
8761
9571
  query: readOptionalTextInput(form, "filter-query"),
8762
9572
  objectTypes: parseObjectTypes(readOptionalTextInput(form, "filter-object-types")),
@@ -8770,6 +9580,66 @@
8770
9580
  }
8771
9581
  return nextDocument;
8772
9582
  }
9583
+ function buildEventDocumentFromInspector(currentId) {
9584
+ const nextDocument = cloneAtlasDocument(atlasDocument);
9585
+ const form = inspector?.querySelector("form[data-editor-form='event']");
9586
+ const current = nextDocument.events.find((entry) => entry.id === currentId);
9587
+ if (!form || !current) {
9588
+ return nextDocument;
9589
+ }
9590
+ const nextId = readTextInput(form, "event-id") || current.id;
9591
+ const replacement = {
9592
+ ...current,
9593
+ id: nextId,
9594
+ kind: readTextInput(form, "event-kind"),
9595
+ label: readTextInput(form, "event-label") || current.label,
9596
+ summary: readOptionalTextInput(form, "event-summary"),
9597
+ targetObjectId: readOptionalTextInput(form, "event-target"),
9598
+ participantObjectIds: splitTokens(readOptionalTextInput(form, "event-participants")),
9599
+ timing: readOptionalTextInput(form, "event-timing"),
9600
+ visibility: readOptionalTextInput(form, "event-visibility"),
9601
+ tags: splitTokens(readOptionalTextInput(form, "event-tags")),
9602
+ color: readOptionalTextInput(form, "event-color"),
9603
+ hidden: readCheckbox(form, "event-hidden")
9604
+ };
9605
+ nextDocument.events = nextDocument.events.filter((entry) => entry.id !== current.id).concat(replacement).sort(compareEvents);
9606
+ syncEventViewpointReferences(nextDocument, current.id, replacement.id, splitTokens(readOptionalTextInput(form, "event-viewpoints")));
9607
+ if (current.id !== replacement.id) {
9608
+ selection = { kind: "event", id: replacement.id };
9609
+ }
9610
+ return nextDocument;
9611
+ }
9612
+ function buildEventPoseDocumentFromInspector(eventId, objectId) {
9613
+ const nextDocument = cloneAtlasDocument(atlasDocument);
9614
+ const form = inspector?.querySelector("form[data-editor-form='event-pose']");
9615
+ const eventEntry = nextDocument.events.find((entry) => entry.id === eventId);
9616
+ const currentPose = eventEntry?.positions.find((entry) => entry.objectId === objectId);
9617
+ if (!form || !eventEntry || !currentPose) {
9618
+ return nextDocument;
9619
+ }
9620
+ const nextObjectId = readTextInput(form, "pose-object-id") || currentPose.objectId;
9621
+ const replacement = {
9622
+ objectId: nextObjectId,
9623
+ placement: buildPlacementFromPoseForm(form, currentPose)
9624
+ };
9625
+ const inner = parseOptionalUnit(readOptionalTextInput(form, "prop-inner"));
9626
+ const outer = parseOptionalUnit(readOptionalTextInput(form, "prop-outer"));
9627
+ if (inner) {
9628
+ replacement.inner = inner;
9629
+ }
9630
+ if (outer) {
9631
+ replacement.outer = outer;
9632
+ }
9633
+ eventEntry.positions = eventEntry.positions.filter((entry) => entry.objectId !== currentPose.objectId).concat(replacement).sort(compareEventPoses);
9634
+ if (eventEntry.targetObjectId !== replacement.objectId && !eventEntry.participantObjectIds.includes(replacement.objectId)) {
9635
+ eventEntry.participantObjectIds.push(replacement.objectId);
9636
+ eventEntry.participantObjectIds.sort((left, right) => left.localeCompare(right));
9637
+ }
9638
+ if (currentPose.objectId !== replacement.objectId) {
9639
+ selection = { kind: "event-pose", id: eventId, key: replacement.objectId };
9640
+ }
9641
+ return nextDocument;
9642
+ }
8773
9643
  function buildAnnotationDocumentFromInspector(currentId) {
8774
9644
  const nextDocument = cloneAtlasDocument(atlasDocument);
8775
9645
  const form = inspector?.querySelector("form[data-editor-form='annotation']");
@@ -8962,7 +9832,7 @@
8962
9832
  const atlasDocument2 = cloneAtlasDocument(options.atlasDocument);
8963
9833
  return {
8964
9834
  atlasDocument: atlasDocument2,
8965
- source: formatDocument(atlasDocument2, { schema: "2.0" }),
9835
+ source: formatAtlasSource(atlasDocument2),
8966
9836
  diagnostics: collectDocumentDiagnostics(atlasDocument2)
8967
9837
  };
8968
9838
  }
@@ -8972,7 +9842,7 @@
8972
9842
  const atlasDocument2 = loaded.value.atlasDocument ?? upgradeDocumentToV2(loaded.value.document);
8973
9843
  return {
8974
9844
  atlasDocument: atlasDocument2,
8975
- source: formatDocument(atlasDocument2, { schema: "2.0" }),
9845
+ source: formatAtlasSource(atlasDocument2),
8976
9846
  diagnostics: mergeDiagnostics(resolveAtlasDiagnostics(atlasDocument2, loaded.diagnostics), collectDocumentDiagnostics(atlasDocument2))
8977
9847
  };
8978
9848
  }
@@ -8980,10 +9850,13 @@
8980
9850
  const atlasDocument = createEmptyAtlasDocument("WorldOrbit");
8981
9851
  return {
8982
9852
  atlasDocument,
8983
- source: formatDocument(atlasDocument, { schema: "2.0" }),
9853
+ source: formatAtlasSource(atlasDocument),
8984
9854
  diagnostics: collectDocumentDiagnostics(atlasDocument)
8985
9855
  };
8986
9856
  }
9857
+ function formatAtlasSource(document2) {
9858
+ return formatDocument(document2, { schema: document2.version });
9859
+ }
8987
9860
  function buildEditorMarkup() {
8988
9861
  const previewOpen = shouldPreviewSectionBeOpenByDefault();
8989
9862
  return `<section class="wo-editor-shell">
@@ -9098,6 +9971,12 @@
9098
9971
  const badge = bucket && (bucket.errors > 0 || bucket.warnings > 0) ? `<span class="wo-editor-outline-badge${bucket.errors > 0 ? " is-error" : " is-warning"}">${bucket.errors > 0 ? bucket.errors : bucket.warnings}</span>` : "";
9099
9972
  return `<button type="button" class="wo-editor-outline-item${key === activeKey ? " is-active" : ""}" data-path-kind="${escapeHtml3(path.kind)}"${path.id ? ` data-path-id="${escapeHtml3(path.id)}"` : ""}${path.key ? ` data-path-key="${escapeHtml3(path.key)}"` : ""}><span>${escapeHtml3(label)}</span>${badge}</button>`;
9100
9973
  }
9974
+ function renderEventOutlineItems(eventEntry, activeKey, diagnosticBuckets) {
9975
+ return `<div class="wo-editor-outline-group">
9976
+ ${renderOutlineButton({ kind: "event", id: eventEntry.id }, eventEntry.label || eventEntry.id, activeKey, diagnosticBuckets)}
9977
+ ${eventEntry.positions.length > 0 ? `<div class="wo-editor-outline-children">${[...eventEntry.positions].sort(compareEventPoses).map((pose) => renderOutlineButton({ kind: "event-pose", id: eventEntry.id, key: pose.objectId }, pose.objectId, activeKey, diagnosticBuckets)).join("")}</div>` : ""}
9978
+ </div>`;
9979
+ }
9101
9980
  function renderSystemInspector(formState) {
9102
9981
  return `<form class="wo-editor-form" data-editor-form="system">
9103
9982
  <h2>System</h2>
@@ -9164,6 +10043,7 @@
9164
10043
  ${renderCheckboxField("Guides", "layer-guides", viewpoint.layers.guides !== false)}
9165
10044
  ${renderCheckboxField("Orbits back", "layer-orbits-back", viewpoint.layers["orbits-back"] !== false)}
9166
10045
  ${renderCheckboxField("Orbits front", "layer-orbits-front", viewpoint.layers["orbits-front"] !== false)}
10046
+ ${renderCheckboxField("Events", "layer-events", viewpoint.layers.events !== false)}
9167
10047
  ${renderCheckboxField("Objects", "layer-objects", viewpoint.layers.objects !== false)}
9168
10048
  ${renderCheckboxField("Labels", "layer-labels", viewpoint.layers.labels !== false)}
9169
10049
  ${renderCheckboxField("Metadata", "layer-metadata", viewpoint.layers.metadata !== false)}
@@ -9171,7 +10051,70 @@
9171
10051
  ${renderInspectorSection("viewpoint", "filter", "Filter", `${renderTextField("Filter query", "filter-query", viewpoint.filter?.query ?? "")}
9172
10052
  ${renderTextField("Filter object types", "filter-object-types", viewpoint.filter?.objectTypes.join(" ") ?? "")}
9173
10053
  ${renderTextField("Filter tags", "filter-tags", viewpoint.filter?.tags.join(" ") ?? "")}
9174
- ${renderTextField("Filter groups", "filter-groups", viewpoint.filter?.groupIds.join(" ") ?? "")}`)}
10054
+ ${renderTextField("Filter groups", "filter-groups", viewpoint.filter?.groupIds.join(" ") ?? "")}
10055
+ ${renderTextField("Events", "viewpoint-events", viewpoint.events.join(" "))}`)}
10056
+ </form>`;
10057
+ }
10058
+ function renderEventInspector(formState, id) {
10059
+ const eventEntry = formState.events.find((entry) => entry.id === id);
10060
+ if (!eventEntry) {
10061
+ return `<p class="wo-editor-empty">Event not found.</p>`;
10062
+ }
10063
+ const linkedViewpoints = formState.viewpoints.filter((viewpoint) => viewpoint.events.includes(eventEntry.id)).map((viewpoint) => viewpoint.id).join(" ");
10064
+ return `<form class="wo-editor-form" data-editor-form="event">
10065
+ <h2>Event</h2>
10066
+ ${renderInspectorSection("event", "basics", "Basics", `${renderTextField("ID", "event-id", eventEntry.id)}
10067
+ ${renderTextField("Kind", "event-kind", eventEntry.kind)}
10068
+ ${renderTextField("Label", "event-label", eventEntry.label)}
10069
+ ${renderTextAreaField("Summary", "event-summary", eventEntry.summary ?? "")}
10070
+ ${renderTextField("Target object", "event-target", eventEntry.targetObjectId ?? "")}
10071
+ ${renderTextField("Participants", "event-participants", eventEntry.participantObjectIds.join(" "))}
10072
+ ${renderTextField("Timing", "event-timing", eventEntry.timing ?? "")}
10073
+ ${renderTextField("Visibility", "event-visibility", eventEntry.visibility ?? "")}
10074
+ ${renderTextField("Tags", "event-tags", eventEntry.tags.join(" "))}
10075
+ ${renderTextField("Color", "event-color", eventEntry.color ?? "")}
10076
+ ${renderCheckboxField("Hidden", "event-hidden", eventEntry.hidden === true)}`, true)}
10077
+ ${renderInspectorSection("event", "viewpoints", "Viewpoints", `${renderTextField("Viewpoints", "event-viewpoints", linkedViewpoints)}`)}
10078
+ ${renderInspectorSection("event", "positions", "Positions", `${eventEntry.positions.length > 0 ? `<div class="wo-editor-inline-list">${eventEntry.positions.map((pose) => renderOutlineButton({ kind: "event-pose", id: eventEntry.id, key: pose.objectId }, pose.objectId, null, /* @__PURE__ */ new Map())).join("")}</div>` : `<p class="wo-editor-empty">No event poses yet.</p>`}
10079
+ <div class="wo-editor-inline-actions">
10080
+ <button type="button" data-editor-action="add-event-pose" data-editor-event-id="${escapeHtml3(eventEntry.id)}">Add pose</button>
10081
+ </div>`)}
10082
+ </form>`;
10083
+ }
10084
+ function renderEventPoseInspector(formState, eventId, objectId) {
10085
+ const eventEntry = formState.events.find((entry) => entry.id === eventId);
10086
+ const pose = eventEntry?.positions.find((entry) => entry.objectId === objectId);
10087
+ if (!eventEntry || !pose) {
10088
+ return `<p class="wo-editor-empty">Event pose not found.</p>`;
10089
+ }
10090
+ const placementMode = pose.placement?.mode ?? "";
10091
+ const placementTarget = pose.placement?.mode === "orbit" || pose.placement?.mode === "surface" || pose.placement?.mode === "at" ? pose.placement.target : "";
10092
+ const freeValue = pose.placement?.mode === "free" ? pose.placement.distance ? formatUnitValue3(pose.placement.distance) : pose.placement.descriptor ?? "" : "";
10093
+ return `<form class="wo-editor-form" data-editor-form="event-pose">
10094
+ <h2>Event Pose</h2>
10095
+ <p class="wo-editor-inline-note">Event <strong>${escapeHtml3(eventEntry.label || eventEntry.id)}</strong></p>
10096
+ ${renderInspectorSection("event-pose", "identity", "Identity", `${renderTextField("Object", "pose-object-id", pose.objectId)}
10097
+ <div class="wo-editor-inline-actions">
10098
+ <button type="button" data-path-kind="event" data-path-id="${escapeHtml3(eventEntry.id)}">Select event</button>
10099
+ </div>`, true)}
10100
+ ${renderInspectorSection("event-pose", "placement", "Placement", `${renderSelectField("Placement mode", "placement-mode", [
10101
+ ["", "None"],
10102
+ ["orbit", "Orbit"],
10103
+ ["at", "At"],
10104
+ ["surface", "Surface"],
10105
+ ["free", "Free"]
10106
+ ], placementMode)}
10107
+ ${renderTextField("Placement target", "placement-target", placementTarget)}
10108
+ ${renderTextField("Free value", "placement-free", freeValue)}
10109
+ ${renderTextField("Distance", "placement-distance", pose.placement?.mode === "orbit" && pose.placement.distance ? formatUnitValue3(pose.placement.distance) : "")}
10110
+ ${renderTextField("Semi-major", "placement-semiMajor", pose.placement?.mode === "orbit" && pose.placement.semiMajor ? formatUnitValue3(pose.placement.semiMajor) : "")}
10111
+ ${renderTextField("Eccentricity", "placement-eccentricity", pose.placement?.mode === "orbit" && pose.placement.eccentricity !== void 0 ? String(pose.placement.eccentricity) : "")}
10112
+ ${renderTextField("Period", "placement-period", pose.placement?.mode === "orbit" && pose.placement.period ? formatUnitValue3(pose.placement.period) : "")}
10113
+ ${renderTextField("Angle", "placement-angle", pose.placement?.mode === "orbit" && pose.placement.angle ? formatUnitValue3(pose.placement.angle) : "")}
10114
+ ${renderTextField("Inclination", "placement-inclination", pose.placement?.mode === "orbit" && pose.placement.inclination ? formatUnitValue3(pose.placement.inclination) : "")}
10115
+ ${renderTextField("Phase", "placement-phase", pose.placement?.mode === "orbit" && pose.placement.phase ? formatUnitValue3(pose.placement.phase) : "")}
10116
+ ${renderTextField("Inner", "prop-inner", pose.inner ? formatUnitValue3(pose.inner) : "")}
10117
+ ${renderTextField("Outer", "prop-outer", pose.outer ? formatUnitValue3(pose.outer) : "")}`, true)}
9175
10118
  </form>`;
9176
10119
  }
9177
10120
  function renderAnnotationInspector(formState, id) {
@@ -9276,13 +10219,19 @@
9276
10219
  return form.elements.namedItem(name)?.checked ?? false;
9277
10220
  }
9278
10221
  function buildPlacementFromForm(form, current) {
10222
+ return buildPlacementFromValues(form, current.placement, current.id);
10223
+ }
10224
+ function buildPlacementFromPoseForm(form, current) {
10225
+ return buildPlacementFromValues(form, current.placement, current.objectId);
10226
+ }
10227
+ function buildPlacementFromValues(form, currentPlacement, fallbackTarget) {
9279
10228
  const mode = readTextInput(form, "placement-mode");
9280
10229
  const target = readOptionalTextInput(form, "placement-target");
9281
10230
  switch (mode) {
9282
10231
  case "orbit":
9283
10232
  return {
9284
10233
  mode,
9285
- target: target ?? (current.placement?.mode === "orbit" ? current.placement.target : current.id),
10234
+ target: target ?? (currentPlacement?.mode === "orbit" ? currentPlacement.target : fallbackTarget),
9286
10235
  distance: parseOptionalUnit(readOptionalTextInput(form, "placement-distance")),
9287
10236
  semiMajor: parseOptionalUnit(readOptionalTextInput(form, "placement-semiMajor")),
9288
10237
  eccentricity: parseNullableNumber(readOptionalTextInput(form, "placement-eccentricity")) ?? void 0,
@@ -9294,13 +10243,13 @@
9294
10243
  case "at":
9295
10244
  return {
9296
10245
  mode,
9297
- target: target ?? current.id,
9298
- reference: parseAtReferenceString(target ?? current.id)
10246
+ target: target ?? fallbackTarget,
10247
+ reference: parseAtReferenceString(target ?? fallbackTarget)
9299
10248
  };
9300
10249
  case "surface":
9301
10250
  return {
9302
10251
  mode,
9303
- target: target ?? current.id
10252
+ target: target ?? fallbackTarget
9304
10253
  };
9305
10254
  case "free": {
9306
10255
  const freeValue = readOptionalTextInput(form, "placement-free");
@@ -9431,9 +10380,48 @@
9431
10380
  annotation.sourceObjectId = toId;
9432
10381
  }
9433
10382
  }
10383
+ for (const eventEntry of document2.events) {
10384
+ if (eventEntry.targetObjectId === fromId) {
10385
+ eventEntry.targetObjectId = toId;
10386
+ }
10387
+ eventEntry.participantObjectIds = eventEntry.participantObjectIds.map((entry) => entry === fromId ? toId : entry);
10388
+ for (const pose of eventEntry.positions) {
10389
+ if (pose.objectId === fromId) {
10390
+ pose.objectId = toId;
10391
+ }
10392
+ if (pose.placement?.mode === "orbit" && pose.placement.target === fromId) {
10393
+ pose.placement.target = toId;
10394
+ }
10395
+ if (pose.placement?.mode === "surface" && pose.placement.target === fromId) {
10396
+ pose.placement.target = toId;
10397
+ }
10398
+ if (pose.placement?.mode === "at") {
10399
+ const reference = pose.placement.reference;
10400
+ if (reference.kind === "anchor" && reference.objectId === fromId) {
10401
+ reference.objectId = toId;
10402
+ }
10403
+ if (reference.kind === "lagrange") {
10404
+ if (reference.primary === fromId) {
10405
+ reference.primary = toId;
10406
+ }
10407
+ if (reference.secondary === fromId) {
10408
+ reference.secondary = toId;
10409
+ }
10410
+ }
10411
+ pose.placement.target = formatAtReference2(reference);
10412
+ }
10413
+ }
10414
+ eventEntry.positions.sort(compareEventPoses);
10415
+ }
9434
10416
  }
9435
10417
  function removeSelectedNode(document2, selection) {
9436
10418
  const next = removeAtlasDocumentNode(document2, selection);
10419
+ if (selection.kind === "event" && selection.id) {
10420
+ for (const viewpoint of next.system?.viewpoints ?? []) {
10421
+ viewpoint.events = viewpoint.events.filter((eventId) => eventId !== selection.id);
10422
+ }
10423
+ return next;
10424
+ }
9437
10425
  if (selection.kind !== "object" || !selection.id) {
9438
10426
  return next;
9439
10427
  }
@@ -9468,9 +10456,45 @@
9468
10456
  annotation.sourceObjectId = null;
9469
10457
  }
9470
10458
  }
10459
+ for (const eventEntry of next.events) {
10460
+ if (eventEntry.targetObjectId === selection.id) {
10461
+ eventEntry.targetObjectId = null;
10462
+ }
10463
+ eventEntry.participantObjectIds = eventEntry.participantObjectIds.filter((entry) => entry !== selection.id);
10464
+ eventEntry.positions = eventEntry.positions.filter((entry) => entry.objectId !== selection.id);
10465
+ for (const pose of eventEntry.positions) {
10466
+ if (pose.placement?.mode === "orbit" && pose.placement.target === selection.id) {
10467
+ pose.placement = null;
10468
+ }
10469
+ if (pose.placement?.mode === "surface" && pose.placement.target === selection.id) {
10470
+ pose.placement = null;
10471
+ }
10472
+ if (pose.placement?.mode === "at") {
10473
+ const reference = pose.placement.reference;
10474
+ const touchesSelection = reference.kind === "anchor" && reference.objectId === selection.id || reference.kind === "lagrange" && (reference.primary === selection.id || reference.secondary === selection.id);
10475
+ if (touchesSelection) {
10476
+ pose.placement = null;
10477
+ }
10478
+ }
10479
+ }
10480
+ }
9471
10481
  return next;
9472
10482
  }
9473
- function updateOrbitPhase(document2, objectId, details, pointer) {
10483
+ function findEditablePlacementOwner(document2, path, objectId) {
10484
+ if (path.kind === "event-pose" && path.id && path.key) {
10485
+ const pose = findEventPose2(document2, path.id, path.key);
10486
+ if (pose?.placement) {
10487
+ return { placement: pose.placement };
10488
+ }
10489
+ return null;
10490
+ }
10491
+ const object = findObject2(document2, objectId);
10492
+ if (object?.placement) {
10493
+ return { placement: object.placement };
10494
+ }
10495
+ return null;
10496
+ }
10497
+ function updateOrbitPhase(document2, path, objectId, details, pointer) {
9474
10498
  const orbit = details.orbit;
9475
10499
  if (!orbit || details.object.placement?.mode !== "orbit") {
9476
10500
  return document2;
@@ -9481,17 +10505,17 @@
9481
10505
  const radians = Math.atan2((unrotated.y - orbit.cy) / Math.max(ry, 1), (unrotated.x - orbit.cx) / Math.max(rx, 1));
9482
10506
  const phaseDeg = normalizeDegrees(radians * 180 / Math.PI);
9483
10507
  const next = cloneAtlasDocument(document2);
9484
- const object = next.objects.find((entry) => entry.id === objectId);
9485
- if (!object || object.placement?.mode !== "orbit") {
10508
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10509
+ if (!placementOwner || placementOwner.placement.mode !== "orbit") {
9486
10510
  return document2;
9487
10511
  }
9488
- object.placement.phase = {
10512
+ placementOwner.placement.phase = {
9489
10513
  value: roundNumber(phaseDeg, 2),
9490
10514
  unit: "deg"
9491
10515
  };
9492
10516
  return next;
9493
10517
  }
9494
- function updateOrbitRadius(document2, objectId, details, pointer, dragContext) {
10518
+ function updateOrbitRadius(document2, path, objectId, details, pointer, dragContext) {
9495
10519
  const orbit = details.orbit;
9496
10520
  if (!orbit || details.object.placement?.mode !== "orbit" || !dragContext) {
9497
10521
  return document2;
@@ -9501,47 +10525,47 @@
9501
10525
  const nextBaseRadius = Math.max(nextDisplayedRadius - dragContext.radiusOffsetPx, dragContext.innerPx);
9502
10526
  const nextMetric = orbitRadiusPxToMetric(nextBaseRadius, dragContext.innerPx, dragContext.stepPx);
9503
10527
  const next = cloneAtlasDocument(document2);
9504
- const object = next.objects.find((entry) => entry.id === objectId);
9505
- if (!object || object.placement?.mode !== "orbit") {
10528
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10529
+ if (!placementOwner || placementOwner.placement.mode !== "orbit") {
9506
10530
  return document2;
9507
10531
  }
9508
- const currentValue = object.placement.semiMajor ?? object.placement.distance ?? {
10532
+ const currentValue = placementOwner.placement.semiMajor ?? placementOwner.placement.distance ?? {
9509
10533
  value: 1,
9510
10534
  unit: "au"
9511
10535
  };
9512
10536
  const scaled = distanceMetricToUnitValue(Math.max(nextMetric, 0), dragContext.preferredUnit ?? currentValue.unit);
9513
- if (object.placement.semiMajor) {
9514
- object.placement.semiMajor = scaled;
10537
+ if (placementOwner.placement.semiMajor) {
10538
+ placementOwner.placement.semiMajor = scaled;
9515
10539
  } else {
9516
- object.placement.distance = scaled;
10540
+ placementOwner.placement.distance = scaled;
9517
10541
  }
9518
10542
  return next;
9519
10543
  }
9520
- function updateAtReference(document2, objectId, scene, pointer) {
10544
+ function updateAtReference(document2, path, objectId, scene, pointer) {
9521
10545
  const candidate = findNearestAtCandidate(scene, objectId, pointer);
9522
10546
  if (!candidate) {
9523
10547
  return document2;
9524
10548
  }
9525
10549
  const next = cloneAtlasDocument(document2);
9526
- const object = next.objects.find((entry) => entry.id === objectId);
9527
- if (!object || object.placement?.mode !== "at") {
10550
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10551
+ if (!placementOwner || placementOwner.placement.mode !== "at") {
9528
10552
  return document2;
9529
10553
  }
9530
- object.placement.reference = candidate.reference;
9531
- object.placement.target = formatAtReference2(candidate.reference);
10554
+ placementOwner.placement.reference = candidate.reference;
10555
+ placementOwner.placement.target = formatAtReference2(candidate.reference);
9532
10556
  return next;
9533
10557
  }
9534
- function updateSurfaceTarget(document2, objectId, scene, pointer) {
10558
+ function updateSurfaceTarget(document2, path, objectId, scene, pointer) {
9535
10559
  const target = findNearestSceneObject(scene, objectId, pointer, (entry) => SURFACE_TARGET_TYPES3.has(entry.object.type));
9536
10560
  if (!target) {
9537
10561
  return document2;
9538
10562
  }
9539
10563
  const next = cloneAtlasDocument(document2);
9540
- const object = next.objects.find((entry) => entry.id === objectId);
9541
- if (!object || object.placement?.mode !== "surface") {
10564
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10565
+ if (!placementOwner || placementOwner.placement.mode !== "surface") {
9542
10566
  return document2;
9543
10567
  }
9544
- object.placement.target = target.objectId;
10568
+ placementOwner.placement.target = target.objectId;
9545
10569
  return next;
9546
10570
  }
9547
10571
  function createOrbitRadiusDragContext(document2, scene, details) {
@@ -9549,7 +10573,7 @@
9549
10573
  return null;
9550
10574
  }
9551
10575
  const targetId = details.object.placement.target;
9552
- const siblingCount = document2.objects.filter((entry) => entry.placement?.mode === "orbit" && entry.placement.target === targetId).length;
10576
+ const siblingCount = scene.objects.filter((entry) => entry.object.placement?.mode === "orbit" && entry.object.placement.target === targetId && !entry.hidden).length;
9553
10577
  const spacingFactor = layoutPresetSpacingForScene(scene.layoutPreset);
9554
10578
  const stepPx = (siblingCount > 2 ? 54 : 64) * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
9555
10579
  const innerPx = details.parent.radius + 56 * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
@@ -9564,28 +10588,28 @@
9564
10588
  preferredUnit: currentValue?.unit ?? null
9565
10589
  };
9566
10590
  }
9567
- function updateFreeDistance(document2, objectId, scene, details, pointer) {
10591
+ function updateFreeDistance(document2, path, objectId, scene, details, pointer) {
9568
10592
  if (details.object.placement?.mode !== "free") {
9569
10593
  return document2;
9570
10594
  }
9571
10595
  const railX = scene.width - scene.padding - 140;
9572
10596
  const offsetPx = Math.max(0, railX - pointer.x);
9573
10597
  const next = cloneAtlasDocument(document2);
9574
- const object = next.objects.find((entry) => entry.id === objectId);
9575
- if (!object || object.placement?.mode !== "free") {
10598
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10599
+ if (!placementOwner || placementOwner.placement.mode !== "free") {
9576
10600
  return document2;
9577
10601
  }
9578
- const preferredUnit = normalizeFreeDistanceUnit(object.placement.distance?.unit ?? null);
10602
+ const preferredUnit = normalizeFreeDistanceUnit(placementOwner.placement.distance?.unit ?? null);
9579
10603
  const metric = offsetPx / Math.max(FREE_DISTANCE_PIXEL_FACTOR * scene.scaleModel.freePlacementMultiplier, 1);
9580
10604
  if (metric < 0.01) {
9581
- object.placement.distance = void 0;
9582
- if (!object.placement.descriptor) {
9583
- delete object.placement.descriptor;
10605
+ placementOwner.placement.distance = void 0;
10606
+ if (!placementOwner.placement.descriptor) {
10607
+ delete placementOwner.placement.descriptor;
9584
10608
  }
9585
10609
  return next;
9586
10610
  }
9587
- object.placement.distance = distanceMetricToUnitValue(metric, preferredUnit);
9588
- delete object.placement.descriptor;
10611
+ placementOwner.placement.distance = distanceMetricToUnitValue(metric, preferredUnit);
10612
+ delete placementOwner.placement.descriptor;
9589
10613
  return next;
9590
10614
  }
9591
10615
  function findNearestSceneObject(scene, selectedObjectId, pointer, predicate = () => true) {
@@ -9857,9 +10881,60 @@
9857
10881
  return ["viewpoint-zoom"];
9858
10882
  case "rotationDeg":
9859
10883
  return ["viewpoint-rotation"];
10884
+ case "events":
10885
+ return ["viewpoint-events"];
9860
10886
  default:
9861
10887
  return [];
9862
10888
  }
10889
+ case "event":
10890
+ switch (field) {
10891
+ case "id":
10892
+ return ["event-id"];
10893
+ case "kind":
10894
+ return ["event-kind"];
10895
+ case "label":
10896
+ return ["event-label"];
10897
+ case "summary":
10898
+ return ["event-summary"];
10899
+ case "targetObjectId":
10900
+ case "target":
10901
+ return ["event-target"];
10902
+ case "participantObjectIds":
10903
+ case "participants":
10904
+ return ["event-participants"];
10905
+ case "timing":
10906
+ return ["event-timing"];
10907
+ case "visibility":
10908
+ return ["event-visibility"];
10909
+ case "tags":
10910
+ return ["event-tags"];
10911
+ case "color":
10912
+ return ["event-color"];
10913
+ case "hidden":
10914
+ return ["event-hidden"];
10915
+ default:
10916
+ return [];
10917
+ }
10918
+ case "event-pose":
10919
+ if (field === "objectId") {
10920
+ return ["pose-object-id"];
10921
+ }
10922
+ if (field === "placement") {
10923
+ return ["placement-mode"];
10924
+ }
10925
+ if (field === "reference" || field === "target") {
10926
+ return ["placement-target"];
10927
+ }
10928
+ if (field === "descriptor") {
10929
+ return ["placement-free"];
10930
+ }
10931
+ if (PLACEMENT_DIAGNOSTIC_FIELDS.has(field)) {
10932
+ return [`placement-${field}`];
10933
+ }
10934
+ if (field === "inner" || field === "outer") {
10935
+ return [`prop-${field}`];
10936
+ }
10937
+ return [];
9863
10938
  case "annotation":
9864
10939
  switch (field) {
9865
10940
  case "id":
@@ -9945,6 +11020,10 @@
9945
11020
  return `Metadata: ${path.key ?? ""}`;
9946
11021
  case "group":
9947
11022
  return `Group: ${path.id ?? ""}`;
11023
+ case "event":
11024
+ return `Event: ${path.id ?? ""}`;
11025
+ case "event-pose":
11026
+ return `Event Pose: ${path.id ?? ""} / ${path.key ?? ""}`;
9948
11027
  case "object":
9949
11028
  return `Object: ${path.id ?? ""}`;
9950
11029
  case "viewpoint":
@@ -9956,11 +11035,70 @@
9956
11035
  }
9957
11036
  }
9958
11037
  function selectionKey(path) {
9959
- return path ? `${path.kind}:${path.id ?? path.key ?? ""}` : null;
11038
+ return path ? `${path.kind}:${path.id ?? ""}:${path.key ?? ""}` : null;
11039
+ }
11040
+ function selectionEventId(path) {
11041
+ if (!path) {
11042
+ return null;
11043
+ }
11044
+ return path.kind === "event" || path.kind === "event-pose" ? path.id ?? null : null;
9960
11045
  }
9961
11046
  function compareObjects2(left, right) {
9962
11047
  return left.id.localeCompare(right.id);
9963
11048
  }
11049
+ function compareEvents(left, right) {
11050
+ return left.id.localeCompare(right.id);
11051
+ }
11052
+ function compareEventPoses(left, right) {
11053
+ return left.objectId.localeCompare(right.objectId);
11054
+ }
11055
+ function findEvent2(document2, eventId) {
11056
+ return document2.events.find((entry) => entry.id === eventId) ?? null;
11057
+ }
11058
+ function findEventPose2(document2, eventId, objectId) {
11059
+ return findEvent2(document2, eventId)?.positions.find((entry) => entry.objectId === objectId) ?? null;
11060
+ }
11061
+ function findObject2(document2, objectId) {
11062
+ return document2.objects.find((entry) => entry.id === objectId) ?? null;
11063
+ }
11064
+ function addEventPose(document2, eventId) {
11065
+ const next = cloneAtlasDocument(document2);
11066
+ const eventEntry = next.events.find((entry) => entry.id === eventId);
11067
+ if (!eventEntry) {
11068
+ return document2;
11069
+ }
11070
+ const baseObject = next.objects.find((object) => !eventEntry.positions.some((pose) => pose.objectId === object.id)) ?? next.objects[0];
11071
+ if (!baseObject) {
11072
+ return document2;
11073
+ }
11074
+ if (eventEntry.targetObjectId !== baseObject.id && !eventEntry.participantObjectIds.includes(baseObject.id)) {
11075
+ eventEntry.participantObjectIds.push(baseObject.id);
11076
+ eventEntry.participantObjectIds.sort((left, right) => left.localeCompare(right));
11077
+ }
11078
+ eventEntry.positions.push(createEventPoseFromObject(baseObject));
11079
+ eventEntry.positions.sort(compareEventPoses);
11080
+ return next;
11081
+ }
11082
+ function createEventPoseFromObject(object) {
11083
+ return {
11084
+ objectId: object.id,
11085
+ placement: object.placement ? structuredClone(object.placement) : null,
11086
+ inner: readUnitValue(object.properties.inner),
11087
+ outer: readUnitValue(object.properties.outer)
11088
+ };
11089
+ }
11090
+ function syncEventViewpointReferences(document2, previousEventId, nextEventId, viewpointIds) {
11091
+ const desired = new Set(viewpointIds);
11092
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
11093
+ const currentIds = new Set(viewpoint.events);
11094
+ currentIds.delete(previousEventId);
11095
+ currentIds.delete(nextEventId);
11096
+ if (desired.has(viewpoint.id)) {
11097
+ currentIds.add(nextEventId);
11098
+ }
11099
+ viewpoint.events = [...currentIds].sort((left, right) => left.localeCompare(right));
11100
+ }
11101
+ }
9964
11102
  function createUniqueId(prefix, existing) {
9965
11103
  const safePrefix = prefix.trim() || "item";
9966
11104
  let counter = 1;
@@ -9989,6 +11127,9 @@
9989
11127
  function readUnitProperty(value) {
9990
11128
  return value && typeof value === "object" && "value" in value ? formatUnitValue3(value) : "";
9991
11129
  }
11130
+ function readUnitValue(value) {
11131
+ return value && typeof value === "object" && "value" in value ? value : void 0;
11132
+ }
9992
11133
  function readNumberProperty(value) {
9993
11134
  return typeof value === "number" ? String(value) : "";
9994
11135
  }
@@ -10294,6 +11435,12 @@
10294
11435
  .wo-editor-overlay-diagnostic-warning { border: 1px solid rgba(240, 180, 100, 0.24); }
10295
11436
  .wo-editor-outline { display: grid; gap: 14px; }
10296
11437
  .wo-editor-outline-section { display: grid; gap: 8px; }
11438
+ .wo-editor-outline-group { display: grid; gap: 6px; }
11439
+ .wo-editor-outline-children {
11440
+ display: grid;
11441
+ gap: 6px;
11442
+ padding-left: 16px;
11443
+ }
10297
11444
  .wo-editor-outline-section h3 {
10298
11445
  margin: 0;
10299
11446
  color: rgba(237,246,255,0.68);
@@ -10333,6 +11480,27 @@
10333
11480
  background: rgba(255, 120, 120, 0.18);
10334
11481
  color: #ffb2b2;
10335
11482
  }
11483
+ .wo-editor-inline-list { display: grid; gap: 8px; }
11484
+ .wo-editor-inline-actions {
11485
+ display: flex;
11486
+ flex-wrap: wrap;
11487
+ gap: 10px;
11488
+ margin-top: 12px;
11489
+ }
11490
+ .wo-editor-inline-actions button {
11491
+ border: 1px solid rgba(255,255,255,0.14);
11492
+ border-radius: 999px;
11493
+ background: rgba(255,255,255,0.06);
11494
+ color: #edf6ff;
11495
+ cursor: pointer;
11496
+ font: 600 12px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
11497
+ padding: 8px 12px;
11498
+ }
11499
+ .wo-editor-inline-note {
11500
+ margin: 0 0 12px;
11501
+ color: rgba(237,246,255,0.72);
11502
+ font: 500 12px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
11503
+ }
10336
11504
  .wo-editor-diagnostics { display: grid; gap: 10px; }
10337
11505
  .wo-editor-diagnostic {
10338
11506
  display: grid;