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
@@ -565,6 +565,7 @@ var WorldOrbit = (() => {
565
565
  system,
566
566
  groups: [],
567
567
  relations: [],
568
+ events: [],
568
569
  objects
569
570
  };
570
571
  }
@@ -948,8 +949,10 @@ var WorldOrbit = (() => {
948
949
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
949
950
  const spacingFactor = layoutPresetSpacing(layoutPreset);
950
951
  const systemId = document2.system?.id ?? null;
951
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
952
- const relationships = buildSceneRelationships(document2.objects, objectMap);
952
+ const activeEventId = options.activeEventId ?? null;
953
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
954
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
955
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
953
956
  const positions = /* @__PURE__ */ new Map();
954
957
  const orbitDrafts = [];
955
958
  const leaderDrafts = [];
@@ -958,7 +961,7 @@ var WorldOrbit = (() => {
958
961
  const atObjects = [];
959
962
  const surfaceChildren = /* @__PURE__ */ new Map();
960
963
  const orbitChildren = /* @__PURE__ */ new Map();
961
- for (const object of document2.objects) {
964
+ for (const object of effectiveObjects) {
962
965
  const placement = object.placement;
963
966
  if (!placement) {
964
967
  rootObjects.push(object);
@@ -1053,13 +1056,14 @@ var WorldOrbit = (() => {
1053
1056
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1054
1057
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1055
1058
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1056
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1059
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1057
1060
  const relations = createSceneRelations(document2, objects);
1058
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1059
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1061
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1062
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1063
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1060
1064
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1061
1065
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1062
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1066
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1063
1067
  return {
1064
1068
  width,
1065
1069
  height,
@@ -1085,6 +1089,8 @@ var WorldOrbit = (() => {
1085
1089
  groups,
1086
1090
  semanticGroups,
1087
1091
  viewpoints,
1092
+ events,
1093
+ activeEventId,
1088
1094
  objects,
1089
1095
  orbitVisuals,
1090
1096
  relations,
@@ -1103,6 +1109,35 @@ var WorldOrbit = (() => {
1103
1109
  y: center.y + dx * sin + dy * cos
1104
1110
  };
1105
1111
  }
1112
+ function createEffectiveObjects(objects, events, activeEventId) {
1113
+ const cloned = objects.map((object) => structuredClone(object));
1114
+ if (!activeEventId) {
1115
+ return cloned;
1116
+ }
1117
+ const activeEvent = events.find((event) => event.id === activeEventId);
1118
+ if (!activeEvent) {
1119
+ return cloned;
1120
+ }
1121
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1122
+ for (const pose of activeEvent.positions) {
1123
+ const object = objectMap.get(pose.objectId);
1124
+ if (!object) {
1125
+ continue;
1126
+ }
1127
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1128
+ if (pose.inner) {
1129
+ object.properties.inner = { ...pose.inner };
1130
+ } else {
1131
+ delete object.properties.inner;
1132
+ }
1133
+ if (pose.outer) {
1134
+ object.properties.outer = { ...pose.outer };
1135
+ } else {
1136
+ delete object.properties.outer;
1137
+ }
1138
+ }
1139
+ return cloned;
1140
+ }
1106
1141
  function resolveLayoutPreset(document2) {
1107
1142
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1108
1143
  switch (rawScale) {
@@ -1258,24 +1293,14 @@ var WorldOrbit = (() => {
1258
1293
  hidden: draft.object.properties.hidden === true
1259
1294
  };
1260
1295
  }
1261
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1296
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1262
1297
  const labels = [];
1263
1298
  const occupied = [];
1264
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1299
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1300
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1265
1301
  for (const object of visibleObjects) {
1266
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1267
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1268
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1269
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1270
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1271
- let attempts = 0;
1272
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1273
- labelY += direction * 14 * labelMultiplier;
1274
- secondaryY += direction * 14 * labelMultiplier;
1275
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1276
- attempts += 1;
1277
- }
1278
- occupied.push(bounds);
1302
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1303
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1279
1304
  labels.push({
1280
1305
  renderId: `${object.renderId}-label`,
1281
1306
  objectId: object.objectId,
@@ -1284,17 +1309,128 @@ var WorldOrbit = (() => {
1284
1309
  semanticGroupIds: [...object.semanticGroupIds],
1285
1310
  label: object.label,
1286
1311
  secondaryLabel: object.secondaryLabel,
1287
- x: object.x,
1288
- y: labelY,
1289
- secondaryY,
1290
- textAnchor: "middle",
1291
- direction: direction < 0 ? "above" : "below",
1312
+ x: placement.x,
1313
+ y: placement.labelY,
1314
+ secondaryY: placement.secondaryY,
1315
+ textAnchor: placement.textAnchor,
1316
+ direction: placement.direction,
1292
1317
  hidden: object.hidden
1293
1318
  });
1294
1319
  }
1295
1320
  return labels;
1296
1321
  }
1297
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1322
+ function compareLabelPlacementOrder(left, right) {
1323
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1324
+ if (priorityDiff !== 0) {
1325
+ return priorityDiff;
1326
+ }
1327
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1328
+ if (renderPriorityDiff !== 0) {
1329
+ return renderPriorityDiff;
1330
+ }
1331
+ return left.sortKey - right.sortKey;
1332
+ }
1333
+ function labelPlacementPriority(object) {
1334
+ switch (object.object.type) {
1335
+ case "star":
1336
+ return 0;
1337
+ case "planet":
1338
+ return 1;
1339
+ case "moon":
1340
+ return 2;
1341
+ case "belt":
1342
+ case "ring":
1343
+ return 3;
1344
+ case "asteroid":
1345
+ case "comet":
1346
+ return 4;
1347
+ case "structure":
1348
+ case "phenomenon":
1349
+ return 5;
1350
+ }
1351
+ }
1352
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1353
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1354
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1355
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1356
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1357
+ const rect = createLabelRect(object, placement, labelMultiplier);
1358
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1359
+ return placement;
1360
+ }
1361
+ }
1362
+ }
1363
+ return null;
1364
+ }
1365
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1366
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1367
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1368
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1369
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1370
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1371
+ 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";
1372
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1373
+ }
1374
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1375
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1376
+ return object.y >= parent.y ? "below" : "above";
1377
+ }
1378
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1379
+ }
1380
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1381
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1382
+ return object.x >= parent.x ? "right" : "left";
1383
+ }
1384
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1385
+ }
1386
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1387
+ const step = 14 * labelMultiplier;
1388
+ switch (direction) {
1389
+ case "above": {
1390
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1391
+ return {
1392
+ x: object.x,
1393
+ labelY,
1394
+ secondaryY: labelY - 16 * labelMultiplier,
1395
+ textAnchor: "middle",
1396
+ direction
1397
+ };
1398
+ }
1399
+ case "below": {
1400
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1401
+ return {
1402
+ x: object.x,
1403
+ labelY,
1404
+ secondaryY: labelY + 16 * labelMultiplier,
1405
+ textAnchor: "middle",
1406
+ direction
1407
+ };
1408
+ }
1409
+ case "left": {
1410
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1411
+ const labelY = object.y - 4 * labelMultiplier;
1412
+ return {
1413
+ x,
1414
+ labelY,
1415
+ secondaryY: labelY + 16 * labelMultiplier,
1416
+ textAnchor: "end",
1417
+ direction
1418
+ };
1419
+ }
1420
+ case "right": {
1421
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1422
+ const labelY = object.y - 4 * labelMultiplier;
1423
+ return {
1424
+ x,
1425
+ labelY,
1426
+ secondaryY: labelY + 16 * labelMultiplier,
1427
+ textAnchor: "start",
1428
+ direction
1429
+ };
1430
+ }
1431
+ }
1432
+ }
1433
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1298
1434
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1299
1435
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1300
1436
  return [
@@ -1309,6 +1445,10 @@ var WorldOrbit = (() => {
1309
1445
  id: "relations",
1310
1446
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1311
1447
  },
1448
+ {
1449
+ id: "events",
1450
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1451
+ },
1312
1452
  {
1313
1453
  id: "objects",
1314
1454
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1320,7 +1460,7 @@ var WorldOrbit = (() => {
1320
1460
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1321
1461
  ];
1322
1462
  }
1323
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1463
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1324
1464
  const groups = /* @__PURE__ */ new Map();
1325
1465
  const ensureGroup = (groupId) => {
1326
1466
  if (!groupId) {
@@ -1369,7 +1509,7 @@ var WorldOrbit = (() => {
1369
1509
  }
1370
1510
  }
1371
1511
  for (const group of groups.values()) {
1372
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1512
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1373
1513
  }
1374
1514
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1375
1515
  }
@@ -1403,6 +1543,29 @@ var WorldOrbit = (() => {
1403
1543
  };
1404
1544
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1405
1545
  }
1546
+ function createSceneEvents(events, objects, activeEventId) {
1547
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1548
+ return events.map((event) => {
1549
+ const objectIds = [.../* @__PURE__ */ new Set([
1550
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1551
+ ...event.participantObjectIds
1552
+ ])];
1553
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1554
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1555
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1556
+ return {
1557
+ renderId: `${createRenderId(event.id)}-event`,
1558
+ eventId: event.id,
1559
+ event,
1560
+ objectIds,
1561
+ participantIds: [...event.participantObjectIds],
1562
+ targetObjectId: event.targetObjectId,
1563
+ x: centroidX,
1564
+ y: centroidY,
1565
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1566
+ };
1567
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1568
+ }
1406
1569
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1407
1570
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1408
1571
  const drafts = /* @__PURE__ */ new Map();
@@ -1456,6 +1619,7 @@ var WorldOrbit = (() => {
1456
1619
  summary: "Fit the whole system with the current atlas defaults.",
1457
1620
  objectId: null,
1458
1621
  selectedObjectId: null,
1622
+ eventIds: [],
1459
1623
  projection,
1460
1624
  preset,
1461
1625
  rotationDeg: 0,
@@ -1492,6 +1656,9 @@ var WorldOrbit = (() => {
1492
1656
  draft.select = normalizedValue;
1493
1657
  }
1494
1658
  return;
1659
+ case "events":
1660
+ draft.eventIds = splitListValue(normalizedValue);
1661
+ return;
1495
1662
  case "projection":
1496
1663
  case "view":
1497
1664
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1548,6 +1715,7 @@ var WorldOrbit = (() => {
1548
1715
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1549
1716
  objectId,
1550
1717
  selectedObjectId,
1718
+ eventIds: [...new Set(draft.eventIds ?? [])],
1551
1719
  projection: draft.projection ?? projection,
1552
1720
  preset: draft.preset ?? preset,
1553
1721
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1605,7 +1773,7 @@ var WorldOrbit = (() => {
1605
1773
  next["orbits-front"] = enabled;
1606
1774
  continue;
1607
1775
  }
1608
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1776
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1609
1777
  next[rawLayer] = enabled;
1610
1778
  }
1611
1779
  }
@@ -1653,7 +1821,7 @@ var WorldOrbit = (() => {
1653
1821
  }
1654
1822
  return parts.join(" - ");
1655
1823
  }
1656
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1824
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1657
1825
  let minX = Number.POSITIVE_INFINITY;
1658
1826
  let minY = Number.POSITIVE_INFINITY;
1659
1827
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1683,7 +1851,7 @@ var WorldOrbit = (() => {
1683
1851
  for (const label of labels) {
1684
1852
  if (label.hidden)
1685
1853
  continue;
1686
- includeLabelBounds(label, include);
1854
+ includeLabelBounds(label, include, labelMultiplier);
1687
1855
  }
1688
1856
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1689
1857
  return createBounds(0, 0, width, height);
@@ -1721,13 +1889,10 @@ var WorldOrbit = (() => {
1721
1889
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1722
1890
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1723
1891
  }
1724
- function includeLabelBounds(label, include) {
1725
- const labelScale = 1;
1726
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1727
- include(label.x - labelHalfWidth, label.y - 18);
1728
- include(label.x + labelHalfWidth, label.y + 8);
1729
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1730
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1892
+ function includeLabelBounds(label, include, labelMultiplier) {
1893
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1894
+ include(bounds.left, bounds.top);
1895
+ include(bounds.right, bounds.bottom);
1731
1896
  }
1732
1897
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1733
1898
  if (positions.has(object.id)) {
@@ -2117,7 +2282,7 @@ var WorldOrbit = (() => {
2117
2282
  return null;
2118
2283
  }
2119
2284
  }
2120
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2285
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2121
2286
  let minX = Number.POSITIVE_INFINITY;
2122
2287
  let minY = Number.POSITIVE_INFINITY;
2123
2288
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2146,7 +2311,7 @@ var WorldOrbit = (() => {
2146
2311
  }
2147
2312
  for (const label of labels) {
2148
2313
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2149
- includeLabelBounds(label, include);
2314
+ includeLabelBounds(label, include, labelMultiplier);
2150
2315
  }
2151
2316
  }
2152
2317
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2171,12 +2336,28 @@ var WorldOrbit = (() => {
2171
2336
  }
2172
2337
  return current.id;
2173
2338
  }
2174
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2339
+ function createLabelRect(object, placement, labelMultiplier) {
2340
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2341
+ }
2342
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2343
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2344
+ const labelWidth = labelHalfWidth * 2;
2345
+ const topPadding = direction === "above" ? 18 : 12;
2346
+ const bottomPadding = direction === "above" ? 8 : 12;
2347
+ let left = x - labelHalfWidth;
2348
+ let right = x + labelHalfWidth;
2349
+ if (textAnchor === "start") {
2350
+ left = x;
2351
+ right = x + labelWidth;
2352
+ } else if (textAnchor === "end") {
2353
+ left = x - labelWidth;
2354
+ right = x;
2355
+ }
2175
2356
  return {
2176
- left: x - labelHalfWidth,
2177
- right: x + labelHalfWidth,
2178
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2179
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2357
+ left,
2358
+ right,
2359
+ top: Math.min(labelY, secondaryY) - topPadding,
2360
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2180
2361
  };
2181
2362
  }
2182
2363
  function rectsOverlap(left, right) {
@@ -2363,11 +2544,6 @@ var WorldOrbit = (() => {
2363
2544
  function customColorFor(value) {
2364
2545
  return typeof value === "string" && value.trim() ? value : void 0;
2365
2546
  }
2366
- function estimateLabelHalfWidth(object, labelMultiplier) {
2367
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2368
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2369
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2370
- }
2371
2547
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2372
2548
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2373
2549
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2407,6 +2583,7 @@ var WorldOrbit = (() => {
2407
2583
  system,
2408
2584
  groups: structuredClone(document2.groups ?? []),
2409
2585
  relations: structuredClone(document2.relations ?? []),
2586
+ events: structuredClone(document2.events ?? []),
2410
2587
  objects: document2.objects.map(cloneWorldOrbitObject),
2411
2588
  diagnostics
2412
2589
  };
@@ -2414,7 +2591,7 @@ var WorldOrbit = (() => {
2414
2591
  function upgradeDocumentToDraftV2(document2, options = {}) {
2415
2592
  return convertAtlasDocumentToLegacyDraft(upgradeDocumentToV2(document2, options));
2416
2593
  }
2417
- function materializeAtlasDocument(document2) {
2594
+ function materializeAtlasDocument(document2, options = {}) {
2418
2595
  const system = document2.system ? {
2419
2596
  type: "system",
2420
2597
  id: document2.system.id,
@@ -2425,6 +2602,8 @@ var WorldOrbit = (() => {
2425
2602
  properties: materializeDraftSystemProperties(document2.system),
2426
2603
  info: materializeDraftSystemInfo(document2.system)
2427
2604
  } : null;
2605
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2606
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2428
2607
  return {
2429
2608
  format: "worldorbit",
2430
2609
  version: "1.0",
@@ -2432,7 +2611,8 @@ var WorldOrbit = (() => {
2432
2611
  system,
2433
2612
  groups: structuredClone(document2.groups ?? []),
2434
2613
  relations: structuredClone(document2.relations ?? []),
2435
- objects: document2.objects.map(cloneWorldOrbitObject)
2614
+ events: document2.events.map(cloneWorldOrbitEvent),
2615
+ objects
2436
2616
  };
2437
2617
  }
2438
2618
  function createDraftSystem(document2, defaults, atlasMetadata, annotations, diagnostics, preset) {
@@ -2557,6 +2737,7 @@ var WorldOrbit = (() => {
2557
2737
  summary: viewpoint.summary,
2558
2738
  focusObjectId: viewpoint.objectId,
2559
2739
  selectedObjectId: viewpoint.selectedObjectId,
2740
+ events: [...viewpoint.eventIds],
2560
2741
  projection: viewpoint.projection,
2561
2742
  preset: viewpoint.preset,
2562
2743
  zoom: viewpoint.scale,
@@ -2589,6 +2770,52 @@ var WorldOrbit = (() => {
2589
2770
  info: { ...object.info }
2590
2771
  };
2591
2772
  }
2773
+ function cloneWorldOrbitEvent(event) {
2774
+ return {
2775
+ ...event,
2776
+ participantObjectIds: [...event.participantObjectIds],
2777
+ tags: [...event.tags],
2778
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2779
+ };
2780
+ }
2781
+ function cloneWorldOrbitEventPose(pose) {
2782
+ return {
2783
+ objectId: pose.objectId,
2784
+ placement: clonePlacement(pose.placement),
2785
+ inner: pose.inner ? { ...pose.inner } : void 0,
2786
+ outer: pose.outer ? { ...pose.outer } : void 0
2787
+ };
2788
+ }
2789
+ function clonePlacement(placement) {
2790
+ return placement ? structuredClone(placement) : null;
2791
+ }
2792
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2793
+ if (!activeEventId) {
2794
+ return;
2795
+ }
2796
+ const event = events.find((entry) => entry.id === activeEventId);
2797
+ if (!event) {
2798
+ return;
2799
+ }
2800
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2801
+ for (const pose of event.positions) {
2802
+ const object = objectMap.get(pose.objectId);
2803
+ if (!object) {
2804
+ continue;
2805
+ }
2806
+ object.placement = clonePlacement(pose.placement);
2807
+ if (pose.inner) {
2808
+ object.properties.inner = { ...pose.inner };
2809
+ } else {
2810
+ delete object.properties.inner;
2811
+ }
2812
+ if (pose.outer) {
2813
+ object.properties.outer = { ...pose.outer };
2814
+ } else {
2815
+ delete object.properties.outer;
2816
+ }
2817
+ }
2818
+ }
2592
2819
  function cloneProperties(properties) {
2593
2820
  const next = {};
2594
2821
  for (const [key, value] of Object.entries(properties)) {
@@ -2686,6 +2913,9 @@ var WorldOrbit = (() => {
2686
2913
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2687
2914
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2688
2915
  }
2916
+ if (viewpoint.events.length > 0) {
2917
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2918
+ }
2689
2919
  }
2690
2920
  for (const annotation of system.annotations) {
2691
2921
  const prefix = `annotation.${annotation.id}`;
@@ -2710,7 +2940,7 @@ var WorldOrbit = (() => {
2710
2940
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2711
2941
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2712
2942
  }
2713
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2943
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2714
2944
  if (layers[key] !== void 0) {
2715
2945
  tokens.push(layers[key] ? key : `-${key}`);
2716
2946
  }
@@ -2814,6 +3044,10 @@ var WorldOrbit = (() => {
2814
3044
  lines.push("");
2815
3045
  lines.push(...formatAtlasRelation(relation));
2816
3046
  }
3047
+ for (const event of [...document2.events].sort(compareIdLike)) {
3048
+ lines.push("");
3049
+ lines.push(...formatAtlasEvent(event));
3050
+ }
2817
3051
  const sortedObjects = [...document2.objects].sort(compareObjects);
2818
3052
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2819
3053
  lines.push("");
@@ -2844,6 +3078,10 @@ var WorldOrbit = (() => {
2844
3078
  lines.push("");
2845
3079
  lines.push(...formatAtlasRelation(relation));
2846
3080
  }
3081
+ for (const event of [...legacy.events].sort(compareIdLike)) {
3082
+ lines.push("");
3083
+ lines.push(...formatAtlasEvent(event));
3084
+ }
2847
3085
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2848
3086
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2849
3087
  lines.push("");
@@ -3055,6 +3293,9 @@ var WorldOrbit = (() => {
3055
3293
  if (layerTokens.length > 0) {
3056
3294
  lines.push(` layers ${layerTokens.join(" ")}`);
3057
3295
  }
3296
+ if (viewpoint.events.length > 0) {
3297
+ lines.push(` events ${viewpoint.events.join(" ")}`);
3298
+ }
3058
3299
  if (viewpoint.filter) {
3059
3300
  lines.push(" filter");
3060
3301
  if (viewpoint.filter.query) {
@@ -3127,6 +3368,54 @@ var WorldOrbit = (() => {
3127
3368
  }
3128
3369
  return lines;
3129
3370
  }
3371
+ function formatAtlasEvent(event) {
3372
+ const lines = [`event ${event.id}`, ` kind ${quoteIfNeeded(event.kind)}`];
3373
+ if (event.label) {
3374
+ lines.push(` label ${quoteIfNeeded(event.label)}`);
3375
+ }
3376
+ if (event.summary) {
3377
+ lines.push(` summary ${quoteIfNeeded(event.summary)}`);
3378
+ }
3379
+ if (event.targetObjectId) {
3380
+ lines.push(` target ${event.targetObjectId}`);
3381
+ }
3382
+ if (event.participantObjectIds.length > 0) {
3383
+ lines.push(` participants ${event.participantObjectIds.join(" ")}`);
3384
+ }
3385
+ if (event.timing) {
3386
+ lines.push(` timing ${quoteIfNeeded(event.timing)}`);
3387
+ }
3388
+ if (event.visibility) {
3389
+ lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
3390
+ }
3391
+ if (event.tags.length > 0) {
3392
+ lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3393
+ }
3394
+ if (event.color) {
3395
+ lines.push(` color ${quoteIfNeeded(event.color)}`);
3396
+ }
3397
+ if (event.hidden) {
3398
+ lines.push(" hidden true");
3399
+ }
3400
+ if (event.positions.length > 0) {
3401
+ lines.push("");
3402
+ lines.push(" positions");
3403
+ for (const pose of [...event.positions].sort(comparePoseObjectId)) {
3404
+ lines.push(` pose ${pose.objectId}`);
3405
+ for (const fieldLine of formatEventPoseFields(pose)) {
3406
+ lines.push(` ${fieldLine}`);
3407
+ }
3408
+ }
3409
+ }
3410
+ return lines;
3411
+ }
3412
+ function formatEventPoseFields(pose) {
3413
+ return [
3414
+ ...formatPlacement(pose.placement),
3415
+ ...formatOptionalUnit("inner", pose.inner),
3416
+ ...formatOptionalUnit("outer", pose.outer)
3417
+ ];
3418
+ }
3130
3419
  function formatValue(value) {
3131
3420
  if (Array.isArray(value)) {
3132
3421
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3168,7 +3457,7 @@ var WorldOrbit = (() => {
3168
3457
  if (orbitFront !== void 0 || orbitBack !== void 0) {
3169
3458
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
3170
3459
  }
3171
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3460
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
3172
3461
  if (layers[key] !== void 0) {
3173
3462
  tokens.push(layers[key] ? key : `-${key}`);
3174
3463
  }
@@ -3196,6 +3485,9 @@ var WorldOrbit = (() => {
3196
3485
  function compareIdLike(left, right) {
3197
3486
  return left.id.localeCompare(right.id);
3198
3487
  }
3488
+ function comparePoseObjectId(left, right) {
3489
+ return left.objectId.localeCompare(right.objectId);
3490
+ }
3199
3491
  function objectTypeIndex(objectType) {
3200
3492
  switch (objectType) {
3201
3493
  case "star":
@@ -3391,6 +3683,7 @@ var WorldOrbit = (() => {
3391
3683
  const diagnostics = [];
3392
3684
  const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
3393
3685
  const groupIds = new Set(document2.groups.map((group) => group.id));
3686
+ const eventIds = new Set(document2.events.map((event) => event.id));
3394
3687
  if (!document2.system) {
3395
3688
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3396
3689
  }
@@ -3400,6 +3693,7 @@ var WorldOrbit = (() => {
3400
3693
  ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3401
3694
  ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
3402
3695
  ["relation", document2.relations.map((relation) => relation.id)],
3696
+ ["event", document2.events.map((event) => event.id)],
3403
3697
  ["object", document2.objects.map((object) => object.id)]
3404
3698
  ]) {
3405
3699
  for (const id of ids) {
@@ -3415,11 +3709,14 @@ var WorldOrbit = (() => {
3415
3709
  validateRelation(relation, objectMap, diagnostics);
3416
3710
  }
3417
3711
  for (const viewpoint of document2.system?.viewpoints ?? []) {
3418
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3712
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3419
3713
  }
3420
3714
  for (const object of document2.objects) {
3421
3715
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3422
3716
  }
3717
+ for (const event of document2.events) {
3718
+ validateEvent(event, objectMap, diagnostics);
3719
+ }
3423
3720
  return diagnostics;
3424
3721
  }
3425
3722
  function validateRelation(relation, objectMap, diagnostics) {
@@ -3437,13 +3734,19 @@ var WorldOrbit = (() => {
3437
3734
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3438
3735
  }
3439
3736
  }
3440
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3441
- if (!filter || sourceSchemaVersion !== "2.1") {
3442
- return;
3443
- }
3444
- for (const groupId of filter.groupIds) {
3445
- if (!groupIds.has(groupId)) {
3446
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3737
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3738
+ if (sourceSchemaVersion === "2.1") {
3739
+ if (filter) {
3740
+ for (const groupId of filter.groupIds) {
3741
+ if (!groupIds.has(groupId)) {
3742
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3743
+ }
3744
+ }
3745
+ }
3746
+ for (const eventId of eventRefs) {
3747
+ if (!eventIds.has(eventId)) {
3748
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3749
+ }
3447
3750
  }
3448
3751
  }
3449
3752
  }
@@ -3529,6 +3832,103 @@ var WorldOrbit = (() => {
3529
3832
  }
3530
3833
  }
3531
3834
  }
3835
+ function validateEvent(event, objectMap, diagnostics) {
3836
+ const fieldPrefix = `event.${event.id}`;
3837
+ const referencedIds = /* @__PURE__ */ new Set();
3838
+ if (!event.kind.trim()) {
3839
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3840
+ }
3841
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3842
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3843
+ }
3844
+ if (event.targetObjectId) {
3845
+ referencedIds.add(event.targetObjectId);
3846
+ if (!objectMap.has(event.targetObjectId)) {
3847
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3848
+ }
3849
+ }
3850
+ const seenParticipants = /* @__PURE__ */ new Set();
3851
+ for (const participantId of event.participantObjectIds) {
3852
+ referencedIds.add(participantId);
3853
+ if (seenParticipants.has(participantId)) {
3854
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3855
+ continue;
3856
+ }
3857
+ seenParticipants.add(participantId);
3858
+ if (!objectMap.has(participantId)) {
3859
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3860
+ }
3861
+ }
3862
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3863
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3864
+ }
3865
+ if (event.positions.length === 0) {
3866
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3867
+ }
3868
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3869
+ 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`));
3870
+ }
3871
+ const poseIds = /* @__PURE__ */ new Set();
3872
+ for (const pose of event.positions) {
3873
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3874
+ if (poseIds.has(pose.objectId)) {
3875
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3876
+ continue;
3877
+ }
3878
+ poseIds.add(pose.objectId);
3879
+ const object = objectMap.get(pose.objectId);
3880
+ if (!object) {
3881
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3882
+ continue;
3883
+ }
3884
+ if (!referencedIds.has(pose.objectId)) {
3885
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3886
+ }
3887
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3888
+ }
3889
+ }
3890
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3891
+ const placement = pose.placement;
3892
+ if (!placement) {
3893
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3894
+ return;
3895
+ }
3896
+ if (placement.mode === "orbit") {
3897
+ if (!objectMap.has(placement.target)) {
3898
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3899
+ }
3900
+ if (placement.distance && placement.semiMajor) {
3901
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3902
+ }
3903
+ return;
3904
+ }
3905
+ if (placement.mode === "surface") {
3906
+ const target = objectMap.get(placement.target);
3907
+ if (!target) {
3908
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3909
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3910
+ 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`));
3911
+ }
3912
+ return;
3913
+ }
3914
+ if (placement.mode === "at") {
3915
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3916
+ 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`));
3917
+ }
3918
+ const reference = placement.reference;
3919
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3920
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3921
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3922
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3923
+ } else if (reference.kind === "lagrange") {
3924
+ if (!objectMap.has(reference.primary)) {
3925
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3926
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3927
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3928
+ }
3929
+ }
3930
+ }
3931
+ }
3532
3932
  function validateAtTarget(object, objectMap, diagnostics) {
3533
3933
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3534
3934
  if (!reference) {
@@ -3729,6 +4129,21 @@ var WorldOrbit = (() => {
3729
4129
  });
3730
4130
  }
3731
4131
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
4132
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
4133
+ "orbit",
4134
+ "distance",
4135
+ "semiMajor",
4136
+ "eccentricity",
4137
+ "period",
4138
+ "angle",
4139
+ "inclination",
4140
+ "phase",
4141
+ "at",
4142
+ "surface",
4143
+ "free",
4144
+ "inner",
4145
+ "outer"
4146
+ ]);
3732
4147
  function parseWorldOrbitAtlas(source) {
3733
4148
  return parseAtlasSource(source);
3734
4149
  }
@@ -3743,12 +4158,15 @@ var WorldOrbit = (() => {
3743
4158
  const objectNodes = [];
3744
4159
  const groups = [];
3745
4160
  const relations = [];
4161
+ const events = [];
4162
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3746
4163
  let sawDefaults = false;
3747
4164
  let sawAtlas = false;
3748
4165
  const viewpointIds = /* @__PURE__ */ new Set();
3749
4166
  const annotationIds = /* @__PURE__ */ new Set();
3750
4167
  const groupIds = /* @__PURE__ */ new Set();
3751
4168
  const relationIds = /* @__PURE__ */ new Set();
4169
+ const eventIds = /* @__PURE__ */ new Set();
3752
4170
  for (let index = 0; index < lines.length; index++) {
3753
4171
  const rawLine = lines[index];
3754
4172
  const lineNumber = index + 1;
@@ -3779,7 +4197,7 @@ var WorldOrbit = (() => {
3779
4197
  continue;
3780
4198
  }
3781
4199
  if (indent === 0) {
3782
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
4200
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3783
4201
  if (section.kind === "system") {
3784
4202
  system = section.system;
3785
4203
  } else if (section.kind === "defaults") {
@@ -3798,6 +4216,7 @@ var WorldOrbit = (() => {
3798
4216
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3799
4217
  }
3800
4218
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4219
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3801
4220
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3802
4221
  const baseDocument = {
3803
4222
  format: "worldorbit",
@@ -3805,6 +4224,7 @@ var WorldOrbit = (() => {
3805
4224
  system,
3806
4225
  groups,
3807
4226
  relations,
4227
+ events: normalizedEvents,
3808
4228
  objects,
3809
4229
  diagnostics
3810
4230
  };
@@ -3840,7 +4260,7 @@ var WorldOrbit = (() => {
3840
4260
  const version = tokens[1].value.toLowerCase();
3841
4261
  return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3842
4262
  }
3843
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
4263
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3844
4264
  const keyword = tokens[0]?.value.toLowerCase();
3845
4265
  switch (keyword) {
3846
4266
  case "system":
@@ -3877,7 +4297,7 @@ var WorldOrbit = (() => {
3877
4297
  if (!system) {
3878
4298
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3879
4299
  }
3880
- return startViewpointSection(tokens, line, system, viewpointIds);
4300
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3881
4301
  case "annotation":
3882
4302
  if (!system) {
3883
4303
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3889,6 +4309,9 @@ var WorldOrbit = (() => {
3889
4309
  case "relation":
3890
4310
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3891
4311
  return startRelationSection(tokens, line, relations, relationIds);
4312
+ case "event":
4313
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4314
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3892
4315
  case "object":
3893
4316
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3894
4317
  default:
@@ -3925,7 +4348,7 @@ var WorldOrbit = (() => {
3925
4348
  seenFields: /* @__PURE__ */ new Set()
3926
4349
  };
3927
4350
  }
3928
- function startViewpointSection(tokens, line, system, viewpointIds) {
4351
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3929
4352
  if (tokens.length !== 2) {
3930
4353
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3931
4354
  }
@@ -3942,6 +4365,7 @@ var WorldOrbit = (() => {
3942
4365
  summary: "",
3943
4366
  focusObjectId: null,
3944
4367
  selectedObjectId: null,
4368
+ events: [],
3945
4369
  projection: system.defaults.view,
3946
4370
  preset: system.defaults.preset,
3947
4371
  zoom: null,
@@ -3954,6 +4378,8 @@ var WorldOrbit = (() => {
3954
4378
  return {
3955
4379
  kind: "viewpoint",
3956
4380
  viewpoint,
4381
+ sourceSchemaVersion,
4382
+ diagnostics,
3957
4383
  seenFields: /* @__PURE__ */ new Set(),
3958
4384
  inFilter: false,
3959
4385
  filterIndent: null,
@@ -4044,6 +4470,49 @@ var WorldOrbit = (() => {
4044
4470
  seenFields: /* @__PURE__ */ new Set()
4045
4471
  };
4046
4472
  }
4473
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4474
+ if (tokens.length !== 2) {
4475
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4476
+ }
4477
+ const id = normalizeIdentifier2(tokens[1].value);
4478
+ if (!id) {
4479
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4480
+ }
4481
+ if (eventIds.has(id)) {
4482
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4483
+ }
4484
+ const event = {
4485
+ id,
4486
+ kind: "",
4487
+ label: humanizeIdentifier3(id),
4488
+ summary: null,
4489
+ targetObjectId: null,
4490
+ participantObjectIds: [],
4491
+ timing: null,
4492
+ visibility: null,
4493
+ tags: [],
4494
+ color: null,
4495
+ hidden: false,
4496
+ positions: []
4497
+ };
4498
+ const rawPoses = [];
4499
+ events.push(event);
4500
+ eventPoseNodes.set(id, rawPoses);
4501
+ eventIds.add(id);
4502
+ return {
4503
+ kind: "event",
4504
+ event,
4505
+ sourceSchemaVersion,
4506
+ diagnostics,
4507
+ seenFields: /* @__PURE__ */ new Set(),
4508
+ rawPoses,
4509
+ inPositions: false,
4510
+ positionsIndent: null,
4511
+ activePose: null,
4512
+ poseIndent: null,
4513
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4514
+ };
4515
+ }
4047
4516
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
4048
4517
  if (tokens.length < 3) {
4049
4518
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -4100,6 +4569,9 @@ var WorldOrbit = (() => {
4100
4569
  case "relation":
4101
4570
  applyRelationField(section, tokens, line);
4102
4571
  return;
4572
+ case "event":
4573
+ applyEventField(section, indent, tokens, line);
4574
+ return;
4103
4575
  case "object":
4104
4576
  applyObjectField(section, indent, tokens, line);
4105
4577
  return;
@@ -4226,7 +4698,14 @@ var WorldOrbit = (() => {
4226
4698
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
4227
4699
  return;
4228
4700
  case "layers":
4229
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4701
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4702
+ return;
4703
+ case "events":
4704
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4705
+ line,
4706
+ column: tokens[0].column
4707
+ });
4708
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
4230
4709
  return;
4231
4710
  default:
4232
4711
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -4331,6 +4810,106 @@ var WorldOrbit = (() => {
4331
4810
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4332
4811
  }
4333
4812
  }
4813
+ function applyEventField(section, indent, tokens, line) {
4814
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4815
+ section.activePose = null;
4816
+ section.poseIndent = null;
4817
+ section.activePoseSeenFields.clear();
4818
+ }
4819
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4820
+ section.inPositions = false;
4821
+ section.positionsIndent = null;
4822
+ }
4823
+ if (section.activePose) {
4824
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4825
+ return;
4826
+ }
4827
+ if (section.inPositions) {
4828
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4829
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4830
+ }
4831
+ const objectId = tokens[1].value;
4832
+ if (!objectId.trim()) {
4833
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4834
+ }
4835
+ const rawPose = {
4836
+ objectId,
4837
+ fields: [],
4838
+ location: { line, column: tokens[0].column }
4839
+ };
4840
+ section.rawPoses.push(rawPose);
4841
+ section.activePose = rawPose;
4842
+ section.poseIndent = indent;
4843
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4844
+ return;
4845
+ }
4846
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4847
+ if (section.seenFields.has("positions")) {
4848
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4849
+ }
4850
+ section.seenFields.add("positions");
4851
+ section.inPositions = true;
4852
+ section.positionsIndent = indent;
4853
+ return;
4854
+ }
4855
+ const key = requireUniqueField(tokens, section.seenFields, line);
4856
+ switch (key) {
4857
+ case "kind":
4858
+ section.event.kind = joinFieldValue(tokens, line);
4859
+ return;
4860
+ case "label":
4861
+ section.event.label = joinFieldValue(tokens, line);
4862
+ return;
4863
+ case "summary":
4864
+ section.event.summary = joinFieldValue(tokens, line);
4865
+ return;
4866
+ case "target":
4867
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4868
+ return;
4869
+ case "participants":
4870
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4871
+ return;
4872
+ case "timing":
4873
+ section.event.timing = joinFieldValue(tokens, line);
4874
+ return;
4875
+ case "visibility":
4876
+ section.event.visibility = joinFieldValue(tokens, line);
4877
+ return;
4878
+ case "tags":
4879
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4880
+ return;
4881
+ case "color":
4882
+ section.event.color = joinFieldValue(tokens, line);
4883
+ return;
4884
+ case "hidden":
4885
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4886
+ line,
4887
+ column: tokens[0].column
4888
+ });
4889
+ return;
4890
+ default:
4891
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4892
+ }
4893
+ }
4894
+ function parseEventPoseField(tokens, line, seenFields) {
4895
+ if (tokens.length < 2) {
4896
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4897
+ }
4898
+ const key = tokens[0].value;
4899
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4900
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4901
+ }
4902
+ if (seenFields.has(key)) {
4903
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4904
+ }
4905
+ seenFields.add(key);
4906
+ return {
4907
+ type: "field",
4908
+ key,
4909
+ values: tokens.slice(1).map((token) => token.value),
4910
+ location: { line, column: tokens[0].column }
4911
+ };
4912
+ }
4334
4913
  function applyObjectField(section, indent, tokens, line) {
4335
4914
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4336
4915
  section.activeBlock = null;
@@ -4389,7 +4968,7 @@ var WorldOrbit = (() => {
4389
4968
  function parseObjectTypeTokens(tokens, line) {
4390
4969
  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");
4391
4970
  }
4392
- function parseLayerTokens(tokens, line) {
4971
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4393
4972
  const layers = {};
4394
4973
  for (const token of parseTokenList(tokens, line, "layers")) {
4395
4974
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -4399,7 +4978,13 @@ var WorldOrbit = (() => {
4399
4978
  layers["orbits-front"] = enabled;
4400
4979
  continue;
4401
4980
  }
4402
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4981
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4982
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4983
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4984
+ line,
4985
+ column: tokens[0]?.column ?? 1
4986
+ });
4987
+ }
4403
4988
  layers[raw] = enabled;
4404
4989
  }
4405
4990
  }
@@ -4538,7 +5123,7 @@ var WorldOrbit = (() => {
4538
5123
  }
4539
5124
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4540
5125
  const fieldMap = collectDraftFields(node.fields);
4541
- const placement = extractDraftPlacement(node.objectType, fieldMap);
5126
+ const placement = extractPlacementFromFieldMap(fieldMap);
4542
5127
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
4543
5128
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4544
5129
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -4590,6 +5175,24 @@ var WorldOrbit = (() => {
4590
5175
  }
4591
5176
  return object;
4592
5177
  }
5178
+ function normalizeDraftEvent(event, rawPoses) {
5179
+ return {
5180
+ ...event,
5181
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5182
+ tags: [...new Set(event.tags)],
5183
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5184
+ };
5185
+ }
5186
+ function normalizeDraftEventPose(rawPose) {
5187
+ const fieldMap = collectDraftFields(rawPose.fields);
5188
+ const placement = extractPlacementFromFieldMap(fieldMap);
5189
+ return {
5190
+ objectId: rawPose.objectId,
5191
+ placement,
5192
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5193
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
5194
+ };
5195
+ }
4593
5196
  function collectDraftFields(fields) {
4594
5197
  const grouped = /* @__PURE__ */ new Map();
4595
5198
  for (const field of fields) {
@@ -4606,7 +5209,7 @@ var WorldOrbit = (() => {
4606
5209
  }
4607
5210
  return grouped;
4608
5211
  }
4609
- function extractDraftPlacement(objectType, fieldMap) {
5212
+ function extractPlacementFromFieldMap(fieldMap) {
4610
5213
  const orbitField = fieldMap.get("orbit")?.[0];
4611
5214
  const atField = fieldMap.get("at")?.[0];
4612
5215
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4899,6 +5502,7 @@ var WorldOrbit = (() => {
4899
5502
  },
4900
5503
  groups: [],
4901
5504
  relations: [],
5505
+ events: [],
4902
5506
  objects: [],
4903
5507
  diagnostics: []
4904
5508
  };
@@ -4916,6 +5520,10 @@ var WorldOrbit = (() => {
4916
5520
  return path.key ? document2.system?.atlasMetadata[path.key] ?? null : null;
4917
5521
  case "group":
4918
5522
  return path.id ? findGroup(document2, path.id) : null;
5523
+ case "event":
5524
+ return path.id ? findEvent(document2, path.id) : null;
5525
+ case "event-pose":
5526
+ return path.id && path.key ? findEventPose(document2, path.id, path.key) : null;
4919
5527
  case "object":
4920
5528
  return path.id ? findObject(document2, path.id) : null;
4921
5529
  case "viewpoint":
@@ -4945,6 +5553,19 @@ var WorldOrbit = (() => {
4945
5553
  next.groups = next.groups.filter((group) => group.id !== path.id);
4946
5554
  }
4947
5555
  return next;
5556
+ case "event":
5557
+ if (path.id) {
5558
+ next.events = next.events.filter((event) => event.id !== path.id);
5559
+ }
5560
+ return next;
5561
+ case "event-pose":
5562
+ if (path.id && path.key) {
5563
+ const event = findEvent(next, path.id);
5564
+ if (event) {
5565
+ event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
5566
+ }
5567
+ }
5568
+ return next;
4948
5569
  case "viewpoint":
4949
5570
  if (path.id) {
4950
5571
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -5013,6 +5634,22 @@ var WorldOrbit = (() => {
5013
5634
  };
5014
5635
  }
5015
5636
  }
5637
+ if (diagnostic.field?.startsWith("event.")) {
5638
+ const parts = diagnostic.field.split(".");
5639
+ if (parts[1] && findEvent(document2, parts[1])) {
5640
+ if (parts[2] === "pose" && parts[3] && findEventPose(document2, parts[1], parts[3])) {
5641
+ return {
5642
+ kind: "event-pose",
5643
+ id: parts[1],
5644
+ key: parts[3]
5645
+ };
5646
+ }
5647
+ return {
5648
+ kind: "event",
5649
+ id: parts[1]
5650
+ };
5651
+ }
5652
+ }
5016
5653
  if (diagnostic.field && diagnostic.field in ensureSystem(document2).atlasMetadata) {
5017
5654
  return {
5018
5655
  kind: "metadata",
@@ -5044,6 +5681,12 @@ var WorldOrbit = (() => {
5044
5681
  function findRelation(document2, relationId) {
5045
5682
  return document2.relations.find((relation) => relation.id === relationId) ?? null;
5046
5683
  }
5684
+ function findEvent(document2, eventId) {
5685
+ return document2.events.find((event) => event.id === eventId) ?? null;
5686
+ }
5687
+ function findEventPose(document2, eventId, objectId) {
5688
+ return findEvent(document2, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
5689
+ }
5047
5690
  function findViewpoint(system, viewpointId) {
5048
5691
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
5049
5692
  }
@@ -5221,6 +5864,7 @@ var WorldOrbit = (() => {
5221
5864
  background: true,
5222
5865
  guides: true,
5223
5866
  relations: true,
5867
+ events: true,
5224
5868
  orbits: true,
5225
5869
  objects: true,
5226
5870
  labels: true,
@@ -5369,12 +6013,14 @@ var WorldOrbit = (() => {
5369
6013
  return {
5370
6014
  version: "2.0",
5371
6015
  viewpointId,
6016
+ activeEventId: renderOptions.activeEventId ?? null,
5372
6017
  viewerState: { ...viewerState },
5373
6018
  renderOptions: {
5374
6019
  preset: renderOptions.preset,
5375
6020
  projection: renderOptions.projection,
5376
6021
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
5377
- scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0
6022
+ scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
6023
+ activeEventId: renderOptions.activeEventId ?? null
5378
6024
  },
5379
6025
  filter: normalizeViewerFilter(filter)
5380
6026
  };
@@ -5387,6 +6033,7 @@ var WorldOrbit = (() => {
5387
6033
  return {
5388
6034
  version: "2.0",
5389
6035
  viewpointId: raw.viewpointId ?? null,
6036
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
5390
6037
  viewerState: {
5391
6038
  scale: raw.viewerState?.scale ?? 1,
5392
6039
  rotationDeg: raw.viewerState?.rotationDeg ?? 0,
@@ -5398,7 +6045,8 @@ var WorldOrbit = (() => {
5398
6045
  preset: raw.renderOptions?.preset,
5399
6046
  projection: raw.renderOptions?.projection,
5400
6047
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : void 0,
5401
- scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0
6048
+ scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0,
6049
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null
5402
6050
  },
5403
6051
  filter: normalizeViewerFilter(raw.filter ?? null)
5404
6052
  };
@@ -5414,7 +6062,8 @@ var WorldOrbit = (() => {
5414
6062
  renderOptions: {
5415
6063
  ...atlasState.renderOptions,
5416
6064
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : void 0,
5417
- scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0
6065
+ scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0,
6066
+ activeEventId: atlasState.renderOptions.activeEventId ?? null
5418
6067
  },
5419
6068
  filter: atlasState.filter ? { ...atlasState.filter } : null
5420
6069
  }
@@ -5432,6 +6081,7 @@ var WorldOrbit = (() => {
5432
6081
  background: viewpoint.layers.background,
5433
6082
  guides: viewpoint.layers.guides,
5434
6083
  relations: viewpoint.layers.relations,
6084
+ events: viewpoint.layers.events,
5435
6085
  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,
5436
6086
  objects: viewpoint.layers.objects,
5437
6087
  labels: viewpoint.layers.labels,
@@ -5718,6 +6368,7 @@ var WorldOrbit = (() => {
5718
6368
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
5719
6369
  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("") : "";
5720
6370
  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("") : "";
6371
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
5721
6372
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
5722
6373
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
5723
6374
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -5753,6 +6404,9 @@ var WorldOrbit = (() => {
5753
6404
  .wo-orbit-front { opacity: 0.9; }
5754
6405
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
5755
6406
  .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
6407
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
6408
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
6409
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
5756
6410
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
5757
6411
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
5758
6412
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -5787,6 +6441,7 @@ var WorldOrbit = (() => {
5787
6441
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
5788
6442
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
5789
6443
  ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
6444
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
5790
6445
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
5791
6446
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
5792
6447
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -5794,6 +6449,20 @@ var WorldOrbit = (() => {
5794
6449
  </g>
5795
6450
  </g>
5796
6451
  </svg>`;
6452
+ }
6453
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
6454
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
6455
+ if (participants.length === 0) {
6456
+ return "";
6457
+ }
6458
+ const stroke = event.event.color || theme.accent;
6459
+ const label = event.event.label || event.event.id;
6460
+ 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("");
6461
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
6462
+ ${lineMarkup}
6463
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
6464
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
6465
+ </g>`;
5797
6466
  }
5798
6467
  function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
5799
6468
  const backParts = [];
@@ -6376,6 +7045,13 @@ var WorldOrbit = (() => {
6376
7045
  value: `${details.object.resonance.targetObjectId} ${details.object.resonance.ratio}`
6377
7046
  });
6378
7047
  }
7048
+ if (details.relatedEvents.length > 0) {
7049
+ fields.set("events", {
7050
+ key: "events",
7051
+ label: "Events",
7052
+ value: details.relatedEvents.map((event) => event.event.label || event.event.id).join(", ")
7053
+ });
7054
+ }
6379
7055
  if (placement?.mode === "at") {
6380
7056
  fields.set("placement", {
6381
7057
  key: "placement",
@@ -6790,6 +7466,12 @@ var WorldOrbit = (() => {
6790
7466
  emitAtlasStateChange();
6791
7467
  return true;
6792
7468
  },
7469
+ getActiveEventId() {
7470
+ return renderOptions.activeEventId ?? null;
7471
+ },
7472
+ setActiveEvent(id) {
7473
+ api.setRenderOptions({ activeEventId: id });
7474
+ },
6793
7475
  search(query, limit = 12) {
6794
7476
  return searchSceneObjects(scene, query, limit);
6795
7477
  },
@@ -7054,6 +7736,7 @@ var WorldOrbit = (() => {
7054
7736
  orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
7055
7737
  relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden && (orbit.objectId === renderObject.objectId || renderObject.ancestorIds.includes(orbit.objectId) || renderObject.childIds.includes(orbit.objectId))),
7056
7738
  relations: scene.relations.filter((relation) => !relation.hidden && (relation.fromObjectId === renderObject.objectId || relation.toObjectId === renderObject.objectId)),
7739
+ relatedEvents: scene.events.filter((event) => !event.hidden && (event.targetObjectId === renderObject.objectId || event.objectIds.includes(renderObject.objectId))),
7057
7740
  parent: getObjectById(renderObject.parentId),
7058
7741
  children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
7059
7742
  ancestors: renderObject.ancestorIds.map((ancestorId) => getObjectById(ancestorId)).filter(Boolean),
@@ -7370,7 +8053,8 @@ var WorldOrbit = (() => {
7370
8053
  filter: renderOptions.filter ? { ...renderOptions.filter } : void 0,
7371
8054
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
7372
8055
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
7373
- theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme
8056
+ theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme,
8057
+ activeEventId: renderOptions.activeEventId ?? null
7374
8058
  };
7375
8059
  }
7376
8060
  function mergeRenderOptions(current, next) {
@@ -7390,7 +8074,7 @@ var WorldOrbit = (() => {
7390
8074
  };
7391
8075
  }
7392
8076
  function hasSceneAffectingRenderOptions(options) {
7393
- 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;
8077
+ 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;
7394
8078
  }
7395
8079
  function resolveSourceRenderOptions(loaded, renderOptions) {
7396
8080
  const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
@@ -7723,6 +8407,34 @@ var WorldOrbit = (() => {
7723
8407
  description: "Rotates the saved camera angle in degrees.",
7724
8408
  references: ["90deg = quarter turn", "180deg = flip"]
7725
8409
  },
8410
+ "viewpoint-events": {
8411
+ description: "Lists event IDs that this viewpoint should feature in its detail panel.",
8412
+ references: ["solar-eclipse-naar", "transit-window conjunction"]
8413
+ },
8414
+ "event-kind": {
8415
+ description: "Short semantic event type for tooling and viewer overlays.",
8416
+ references: ["solar-eclipse", "lunar-eclipse", "transit"]
8417
+ },
8418
+ "event-target": {
8419
+ description: "Primary object this event is centered on.",
8420
+ references: ["Naar", "Seyra"]
8421
+ },
8422
+ "event-participants": {
8423
+ description: "Objects that participate in the event snapshot or description.",
8424
+ references: ["Iyath Naar Seyra", "Naar Seyra Orun"]
8425
+ },
8426
+ "event-timing": {
8427
+ description: "Free-text timing note for the event.",
8428
+ references: ['"Every late bloom season"', '"At local midyear"']
8429
+ },
8430
+ "event-visibility": {
8431
+ description: "Notes where or how the event is visible.",
8432
+ references: ['"Visible from Naar"', '"Southern hemisphere only"']
8433
+ },
8434
+ "event-viewpoints": {
8435
+ description: "Viewpoint IDs that should list this event prominently.",
8436
+ references: ["naar-system", "overview inner-system"]
8437
+ },
7726
8438
  "placement-target": {
7727
8439
  description: "Names the body or reference this object is attached to.",
7728
8440
  references: ["orbit Primary", "surface Homeworld", "at Naar:L4"]
@@ -7841,12 +8553,23 @@ var WorldOrbit = (() => {
7841
8553
  minimap: true,
7842
8554
  tooltipMode: "hover",
7843
8555
  onSelectionChange(selectedObject) {
8556
+ const activeEventId = selection ? selectionEventId(selection) : null;
7844
8557
  if (ignoreViewerSelection || !selectedObject) {
7845
- if (!ignoreViewerSelection && selection?.kind === "object") {
7846
- setSelection(null, false, true);
8558
+ if (!ignoreViewerSelection) {
8559
+ if (selection?.kind === "event-pose" && selection.id) {
8560
+ setSelection({ kind: "event", id: selection.id }, false, true);
8561
+ } else if (selection?.kind === "object") {
8562
+ setSelection(activeEventId ? { kind: "event", id: activeEventId } : null, false, true);
8563
+ } else if (selection?.kind === "event" && selection.id) {
8564
+ setSelection({ kind: "event", id: selection.id }, false, true);
8565
+ }
7847
8566
  }
7848
8567
  return;
7849
8568
  }
8569
+ if (activeEventId && findEventPose2(atlasDocument, activeEventId, selectedObject.objectId)) {
8570
+ setSelection({ kind: "event-pose", id: activeEventId, key: selectedObject.objectId }, false, true);
8571
+ return;
8572
+ }
7850
8573
  setSelection({ kind: "object", id: selectedObject.objectId }, false, true);
7851
8574
  },
7852
8575
  onViewChange() {
@@ -7856,6 +8579,7 @@ var WorldOrbit = (() => {
7856
8579
  toolbar.addEventListener("click", handleToolbarClick);
7857
8580
  outline.addEventListener("click", handleOutlineClick);
7858
8581
  overlay.addEventListener("pointerdown", handleOverlayPointerDown);
8582
+ inspector?.addEventListener("click", handleInspectorClick);
7859
8583
  inspector?.addEventListener("input", handleInspectorInput);
7860
8584
  inspector?.addEventListener("change", handleInspectorChange);
7861
8585
  sourcePane?.addEventListener("input", handleSourceInput);
@@ -7930,6 +8654,28 @@ var WorldOrbit = (() => {
7930
8654
  replaceAtlasDocument(nextDocument, true, { kind: "object", id });
7931
8655
  return id;
7932
8656
  },
8657
+ addEvent() {
8658
+ const id = createUniqueId("event", atlasDocument.events.map((event) => event.id));
8659
+ const created = {
8660
+ id,
8661
+ kind: "",
8662
+ label: humanizeIdentifier4(id),
8663
+ summary: null,
8664
+ targetObjectId: null,
8665
+ participantObjectIds: [],
8666
+ timing: null,
8667
+ visibility: null,
8668
+ tags: [],
8669
+ color: null,
8670
+ hidden: false,
8671
+ positions: []
8672
+ };
8673
+ const nextDocument = cloneAtlasDocument(atlasDocument);
8674
+ nextDocument.events.push(created);
8675
+ nextDocument.events.sort(compareEvents);
8676
+ replaceAtlasDocument(nextDocument, true, { kind: "event", id });
8677
+ return id;
8678
+ },
7933
8679
  addViewpoint() {
7934
8680
  const id = createUniqueId("viewpoint", atlasDocument.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []);
7935
8681
  const created = {
@@ -7938,6 +8684,7 @@ var WorldOrbit = (() => {
7938
8684
  summary: "",
7939
8685
  focusObjectId: null,
7940
8686
  selectedObjectId: null,
8687
+ events: [],
7941
8688
  projection: atlasDocument.system?.defaults.view ?? "topdown",
7942
8689
  preset: atlasDocument.system?.defaults.preset ?? null,
7943
8690
  zoom: null,
@@ -7998,6 +8745,7 @@ var WorldOrbit = (() => {
7998
8745
  toolbar.removeEventListener("click", handleToolbarClick);
7999
8746
  outline.removeEventListener("click", handleOutlineClick);
8000
8747
  overlay.removeEventListener("pointerdown", handleOverlayPointerDown);
8748
+ inspector?.removeEventListener("click", handleInspectorClick);
8001
8749
  inspector?.removeEventListener("input", handleInspectorInput);
8002
8750
  inspector?.removeEventListener("change", handleInspectorChange);
8003
8751
  sourcePane?.removeEventListener("input", handleSourceInput);
@@ -8043,7 +8791,7 @@ var WorldOrbit = (() => {
8043
8791
  }
8044
8792
  function getCurrentSourceForExport() {
8045
8793
  if (dragState?.changed) {
8046
- return formatDocument(atlasDocument, { schema: "2.0" });
8794
+ return formatAtlasSource(atlasDocument);
8047
8795
  }
8048
8796
  return canonicalSource;
8049
8797
  }
@@ -8082,7 +8830,7 @@ var WorldOrbit = (() => {
8082
8830
  }
8083
8831
  clearSourceInputTimer();
8084
8832
  atlasDocument = cloneAtlasDocument(nextDocument);
8085
- canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
8833
+ canonicalSource = formatAtlasSource(atlasDocument);
8086
8834
  if (!preserveSourceText) {
8087
8835
  sourceText = canonicalSource;
8088
8836
  }
@@ -8123,10 +8871,10 @@ var WorldOrbit = (() => {
8123
8871
  if (commitHistory) {
8124
8872
  history.push(createHistoryEntry());
8125
8873
  future.length = 0;
8126
- sourceText = formatDocument(nextDocument, { schema: "2.0" });
8874
+ sourceText = formatAtlasSource(nextDocument);
8127
8875
  }
8128
8876
  atlasDocument = cloneAtlasDocument(nextDocument);
8129
- canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
8877
+ canonicalSource = formatAtlasSource(atlasDocument);
8130
8878
  diagnostics = mergeDiagnostics(loadedDiagnostics, collectDocumentDiagnostics(atlasDocument));
8131
8879
  selection = normalizeSelection(selection);
8132
8880
  syncViewer({
@@ -8145,11 +8893,13 @@ var WorldOrbit = (() => {
8145
8893
  const previousState = viewer.getState();
8146
8894
  const currentRenderOptions = viewer.getRenderOptions();
8147
8895
  const nextPreset = atlasDocument.system?.defaults.preset ?? "atlas-card";
8896
+ const nextActiveEventId = selection ? selectionEventId(selection) : null;
8148
8897
  ignoreViewerSelection = true;
8149
- if (currentRenderOptions.preset !== nextPreset || currentRenderOptions.projection !== "document") {
8898
+ if (currentRenderOptions.preset !== nextPreset || currentRenderOptions.projection !== "document" || (currentRenderOptions.activeEventId ?? null) !== nextActiveEventId) {
8150
8899
  viewer.setRenderOptions({
8151
8900
  preset: nextPreset,
8152
- projection: "document"
8901
+ projection: "document",
8902
+ activeEventId: nextActiveEventId
8153
8903
  });
8154
8904
  }
8155
8905
  viewer.setDocument(materializeAtlasDocument(atlasDocument));
@@ -8158,10 +8908,12 @@ var WorldOrbit = (() => {
8158
8908
  } else if (options2.preserveCamera !== false) {
8159
8909
  viewer.setState({
8160
8910
  ...previousState,
8161
- selectedObjectId: selection?.kind === "object" ? selection.id ?? null : null
8911
+ selectedObjectId: selection?.kind === "object" ? selection.id ?? null : selection?.kind === "event-pose" ? selection.key ?? null : null
8162
8912
  });
8163
8913
  } else if (selection?.kind === "object" && selection.id) {
8164
8914
  viewer.focusObject(selection.id);
8915
+ } else if (selection?.kind === "event-pose" && selection.key) {
8916
+ viewer.focusObject(selection.key);
8165
8917
  }
8166
8918
  ignoreViewerSelection = false;
8167
8919
  }
@@ -8180,8 +8932,11 @@ var WorldOrbit = (() => {
8180
8932
  selection = normalizeSelection(nextSelection);
8181
8933
  if (syncViewerSelection) {
8182
8934
  ignoreViewerSelection = true;
8935
+ viewer.setRenderOptions({ activeEventId: selection ? selectionEventId(selection) : null });
8183
8936
  if (selection?.kind === "object" && selection.id) {
8184
8937
  viewer.focusObject(selection.id);
8938
+ } else if (selection?.kind === "event-pose" && selection.key) {
8939
+ viewer.focusObject(selection.key);
8185
8940
  } else if (selection?.kind === "viewpoint" && selection.id) {
8186
8941
  viewer.goToViewpoint(selection.id);
8187
8942
  }
@@ -8231,6 +8986,7 @@ var WorldOrbit = (() => {
8231
8986
  ${OBJECT_TYPES.map((type) => `<option value="${escapeHtml3(type)}"${type === objectType ? " selected" : ""}>${escapeHtml3(humanizeIdentifier4(type))}</option>`).join("")}
8232
8987
  </select>
8233
8988
  <button type="button" data-editor-action="add-object">Add object</button>
8989
+ <button type="button" data-editor-action="add-event">Add event</button>
8234
8990
  <button type="button" data-editor-action="add-viewpoint">Add viewpoint</button>
8235
8991
  <button type="button" data-editor-action="add-annotation">Add annotation</button>
8236
8992
  <button type="button" data-editor-action="add-metadata">Add metadata</button>
@@ -8265,6 +9021,10 @@ var WorldOrbit = (() => {
8265
9021
  <h3>Annotations</h3>
8266
9022
  ${(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>`}
8267
9023
  </div>
9024
+ <div class="wo-editor-outline-section">
9025
+ <h3>Events</h3>
9026
+ ${atlasDocument.events.length > 0 ? atlasDocument.events.map((eventEntry) => renderEventOutlineItems(eventEntry, activeKey, diagnosticBuckets)).join("") : `<p class="wo-editor-empty">No events yet.</p>`}
9027
+ </div>
8268
9028
  <div class="wo-editor-outline-section">
8269
9029
  <h3>Objects</h3>
8270
9030
  ${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>`}
@@ -8302,6 +9062,7 @@ var WorldOrbit = (() => {
8302
9062
  selection: selection ? { path: { ...selection } } : null,
8303
9063
  system: atlasDocument.system,
8304
9064
  viewpoints: atlasDocument.system?.viewpoints ?? [],
9065
+ events: atlasDocument.events,
8305
9066
  objects: atlasDocument.objects
8306
9067
  };
8307
9068
  if (!selection) {
@@ -8330,6 +9091,16 @@ var WorldOrbit = (() => {
8330
9091
  applyInspectorSectionState(inspector, inspectorSectionState);
8331
9092
  decorateInspectorDiagnostics(selection, diagnostics);
8332
9093
  return;
9094
+ case "event":
9095
+ inspector.innerHTML = diagnosticSummary + renderEventInspector(formState, selection.id ?? "");
9096
+ applyInspectorSectionState(inspector, inspectorSectionState);
9097
+ decorateInspectorDiagnostics(selection, diagnostics);
9098
+ return;
9099
+ case "event-pose":
9100
+ inspector.innerHTML = diagnosticSummary + renderEventPoseInspector(formState, selection.id ?? "", selection.key ?? "");
9101
+ applyInspectorSectionState(inspector, inspectorSectionState);
9102
+ decorateInspectorDiagnostics(selection, diagnostics);
9103
+ return;
8333
9104
  case "annotation":
8334
9105
  inspector.innerHTML = diagnosticSummary + renderAnnotationInspector(formState, selection.id ?? "");
8335
9106
  applyInspectorSectionState(inspector, inspectorSectionState);
@@ -8362,10 +9133,11 @@ var WorldOrbit = (() => {
8362
9133
  return;
8363
9134
  }
8364
9135
  overlay.innerHTML = "";
8365
- if (selection?.kind !== "object" || !selection.id) {
9136
+ const selectedObjectId = selection?.kind === "object" ? selection.id ?? null : selection?.kind === "event-pose" ? selection.key ?? null : null;
9137
+ if (!selectedObjectId) {
8366
9138
  return;
8367
9139
  }
8368
- const details = viewer.getObjectDetails(selection.id);
9140
+ const details = viewer.getObjectDetails(selectedObjectId);
8369
9141
  if (!details) {
8370
9142
  return;
8371
9143
  }
@@ -8477,6 +9249,9 @@ var WorldOrbit = (() => {
8477
9249
  case "add-viewpoint":
8478
9250
  api.addViewpoint();
8479
9251
  return;
9252
+ case "add-event":
9253
+ api.addEvent();
9254
+ return;
8480
9255
  case "add-annotation":
8481
9256
  api.addAnnotation();
8482
9257
  return;
@@ -8511,6 +9286,32 @@ var WorldOrbit = (() => {
8511
9286
  key: button.dataset.pathKey || void 0
8512
9287
  }, true, true);
8513
9288
  }
9289
+ function handleInspectorClick(event) {
9290
+ const pathButton = event.target?.closest("[data-path-kind]");
9291
+ if (pathButton) {
9292
+ setSelection({
9293
+ kind: pathButton.dataset.pathKind,
9294
+ id: pathButton.dataset.pathId || void 0,
9295
+ key: pathButton.dataset.pathKey || void 0
9296
+ }, true, true);
9297
+ return;
9298
+ }
9299
+ const actionButton = event.target?.closest("[data-editor-action]");
9300
+ if (!actionButton) {
9301
+ return;
9302
+ }
9303
+ if (actionButton.dataset.editorAction === "add-event-pose") {
9304
+ const eventId = actionButton.dataset.editorEventId || (selection?.kind === "event" || selection?.kind === "event-pose" ? selection.id ?? "" : "");
9305
+ if (!eventId) {
9306
+ return;
9307
+ }
9308
+ const nextDocument = addEventPose(atlasDocument, eventId);
9309
+ const createdEvent = nextDocument.events.find((entry) => entry.id === eventId);
9310
+ const createdPose = createdEvent?.positions.at(-1) ?? createdEvent?.positions[0];
9311
+ replaceAtlasDocument(nextDocument, true, createdPose ? { kind: "event-pose", id: eventId, key: createdPose.objectId } : { kind: "event", id: eventId });
9312
+ return;
9313
+ }
9314
+ }
8514
9315
  function handleInspectorInput() {
8515
9316
  applyInspectorState(false);
8516
9317
  }
@@ -8534,6 +9335,12 @@ var WorldOrbit = (() => {
8534
9335
  case "viewpoint":
8535
9336
  replaceAtlasDocument(buildViewpointDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
8536
9337
  return;
9338
+ case "event":
9339
+ replaceAtlasDocument(buildEventDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
9340
+ return;
9341
+ case "event-pose":
9342
+ replaceAtlasDocument(buildEventPoseDocumentFromInspector(selection.id ?? "", selection.key ?? ""), commitHistory, selection, false);
9343
+ return;
8537
9344
  case "annotation":
8538
9345
  replaceAtlasDocument(buildAnnotationDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
8539
9346
  return;
@@ -8602,6 +9409,7 @@ var WorldOrbit = (() => {
8602
9409
  kind,
8603
9410
  objectId,
8604
9411
  pointerId: event.pointerId,
9412
+ path: selection ? { ...selection } : { kind: "object", id: objectId },
8605
9413
  startedFrom: createHistoryEntry(),
8606
9414
  changed: false,
8607
9415
  orbitRadiusContext: kind === "orbit-radius" && details ? createOrbitRadiusDragContext(atlasDocument, viewer.getScene(), details) : null
@@ -8610,7 +9418,7 @@ var WorldOrbit = (() => {
8610
9418
  event.preventDefault();
8611
9419
  }
8612
9420
  function handleWindowPointerMove(event) {
8613
- if (!dragState || dragState.pointerId !== event.pointerId || selection?.kind !== "object" || selection.id !== dragState.objectId) {
9421
+ if (!dragState || dragState.pointerId !== event.pointerId || !selection || selectionKey(selection) !== selectionKey(dragState.path)) {
8614
9422
  return;
8615
9423
  }
8616
9424
  const details = viewer.getObjectDetails(dragState.objectId);
@@ -8622,27 +9430,27 @@ var WorldOrbit = (() => {
8622
9430
  switch (dragState.kind) {
8623
9431
  case "orbit-phase":
8624
9432
  if (details.object.placement?.mode === "orbit" && details.orbit) {
8625
- nextDocument = updateOrbitPhase(atlasDocument, dragState.objectId, details, pointer);
9433
+ nextDocument = updateOrbitPhase(atlasDocument, dragState.path, dragState.objectId, details, pointer);
8626
9434
  }
8627
9435
  break;
8628
9436
  case "orbit-radius":
8629
9437
  if (details.object.placement?.mode === "orbit" && details.orbit) {
8630
- nextDocument = updateOrbitRadius(atlasDocument, dragState.objectId, details, pointer, dragState.orbitRadiusContext ?? null);
9438
+ nextDocument = updateOrbitRadius(atlasDocument, dragState.path, dragState.objectId, details, pointer, dragState.orbitRadiusContext ?? null);
8631
9439
  }
8632
9440
  break;
8633
9441
  case "at-reference":
8634
9442
  if (details.object.placement?.mode === "at") {
8635
- nextDocument = updateAtReference(atlasDocument, dragState.objectId, viewer.getScene(), pointer);
9443
+ nextDocument = updateAtReference(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), pointer);
8636
9444
  }
8637
9445
  break;
8638
9446
  case "surface-target":
8639
9447
  if (details.object.placement?.mode === "surface") {
8640
- nextDocument = updateSurfaceTarget(atlasDocument, dragState.objectId, viewer.getScene(), pointer);
9448
+ nextDocument = updateSurfaceTarget(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), pointer);
8641
9449
  }
8642
9450
  break;
8643
9451
  case "free-distance":
8644
9452
  if (details.object.placement?.mode === "free") {
8645
- nextDocument = updateFreeDistance(atlasDocument, dragState.objectId, viewer.getScene(), details, pointer);
9453
+ nextDocument = updateFreeDistance(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), details, pointer);
8646
9454
  }
8647
9455
  break;
8648
9456
  }
@@ -8674,7 +9482,7 @@ var WorldOrbit = (() => {
8674
9482
  }
8675
9483
  history.push(dragState.startedFrom);
8676
9484
  future.length = 0;
8677
- canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
9485
+ canonicalSource = formatAtlasSource(atlasDocument);
8678
9486
  sourceText = canonicalSource;
8679
9487
  dragState = null;
8680
9488
  renderAll();
@@ -8777,10 +9585,12 @@ var WorldOrbit = (() => {
8777
9585
  guides: readCheckbox(form, "layer-guides"),
8778
9586
  "orbits-back": readCheckbox(form, "layer-orbits-back"),
8779
9587
  "orbits-front": readCheckbox(form, "layer-orbits-front"),
9588
+ events: readCheckbox(form, "layer-events"),
8780
9589
  objects: readCheckbox(form, "layer-objects"),
8781
9590
  labels: readCheckbox(form, "layer-labels"),
8782
9591
  metadata: readCheckbox(form, "layer-metadata")
8783
9592
  },
9593
+ events: splitTokens(readOptionalTextInput(form, "viewpoint-events")),
8784
9594
  filter: {
8785
9595
  query: readOptionalTextInput(form, "filter-query"),
8786
9596
  objectTypes: parseObjectTypes(readOptionalTextInput(form, "filter-object-types")),
@@ -8794,6 +9604,66 @@ var WorldOrbit = (() => {
8794
9604
  }
8795
9605
  return nextDocument;
8796
9606
  }
9607
+ function buildEventDocumentFromInspector(currentId) {
9608
+ const nextDocument = cloneAtlasDocument(atlasDocument);
9609
+ const form = inspector?.querySelector("form[data-editor-form='event']");
9610
+ const current = nextDocument.events.find((entry) => entry.id === currentId);
9611
+ if (!form || !current) {
9612
+ return nextDocument;
9613
+ }
9614
+ const nextId = readTextInput(form, "event-id") || current.id;
9615
+ const replacement = {
9616
+ ...current,
9617
+ id: nextId,
9618
+ kind: readTextInput(form, "event-kind"),
9619
+ label: readTextInput(form, "event-label") || current.label,
9620
+ summary: readOptionalTextInput(form, "event-summary"),
9621
+ targetObjectId: readOptionalTextInput(form, "event-target"),
9622
+ participantObjectIds: splitTokens(readOptionalTextInput(form, "event-participants")),
9623
+ timing: readOptionalTextInput(form, "event-timing"),
9624
+ visibility: readOptionalTextInput(form, "event-visibility"),
9625
+ tags: splitTokens(readOptionalTextInput(form, "event-tags")),
9626
+ color: readOptionalTextInput(form, "event-color"),
9627
+ hidden: readCheckbox(form, "event-hidden")
9628
+ };
9629
+ nextDocument.events = nextDocument.events.filter((entry) => entry.id !== current.id).concat(replacement).sort(compareEvents);
9630
+ syncEventViewpointReferences(nextDocument, current.id, replacement.id, splitTokens(readOptionalTextInput(form, "event-viewpoints")));
9631
+ if (current.id !== replacement.id) {
9632
+ selection = { kind: "event", id: replacement.id };
9633
+ }
9634
+ return nextDocument;
9635
+ }
9636
+ function buildEventPoseDocumentFromInspector(eventId, objectId) {
9637
+ const nextDocument = cloneAtlasDocument(atlasDocument);
9638
+ const form = inspector?.querySelector("form[data-editor-form='event-pose']");
9639
+ const eventEntry = nextDocument.events.find((entry) => entry.id === eventId);
9640
+ const currentPose = eventEntry?.positions.find((entry) => entry.objectId === objectId);
9641
+ if (!form || !eventEntry || !currentPose) {
9642
+ return nextDocument;
9643
+ }
9644
+ const nextObjectId = readTextInput(form, "pose-object-id") || currentPose.objectId;
9645
+ const replacement = {
9646
+ objectId: nextObjectId,
9647
+ placement: buildPlacementFromPoseForm(form, currentPose)
9648
+ };
9649
+ const inner = parseOptionalUnit(readOptionalTextInput(form, "prop-inner"));
9650
+ const outer = parseOptionalUnit(readOptionalTextInput(form, "prop-outer"));
9651
+ if (inner) {
9652
+ replacement.inner = inner;
9653
+ }
9654
+ if (outer) {
9655
+ replacement.outer = outer;
9656
+ }
9657
+ eventEntry.positions = eventEntry.positions.filter((entry) => entry.objectId !== currentPose.objectId).concat(replacement).sort(compareEventPoses);
9658
+ if (eventEntry.targetObjectId !== replacement.objectId && !eventEntry.participantObjectIds.includes(replacement.objectId)) {
9659
+ eventEntry.participantObjectIds.push(replacement.objectId);
9660
+ eventEntry.participantObjectIds.sort((left, right) => left.localeCompare(right));
9661
+ }
9662
+ if (currentPose.objectId !== replacement.objectId) {
9663
+ selection = { kind: "event-pose", id: eventId, key: replacement.objectId };
9664
+ }
9665
+ return nextDocument;
9666
+ }
8797
9667
  function buildAnnotationDocumentFromInspector(currentId) {
8798
9668
  const nextDocument = cloneAtlasDocument(atlasDocument);
8799
9669
  const form = inspector?.querySelector("form[data-editor-form='annotation']");
@@ -8986,7 +9856,7 @@ var WorldOrbit = (() => {
8986
9856
  const atlasDocument2 = cloneAtlasDocument(options.atlasDocument);
8987
9857
  return {
8988
9858
  atlasDocument: atlasDocument2,
8989
- source: formatDocument(atlasDocument2, { schema: "2.0" }),
9859
+ source: formatAtlasSource(atlasDocument2),
8990
9860
  diagnostics: collectDocumentDiagnostics(atlasDocument2)
8991
9861
  };
8992
9862
  }
@@ -8996,7 +9866,7 @@ var WorldOrbit = (() => {
8996
9866
  const atlasDocument2 = loaded.value.atlasDocument ?? upgradeDocumentToV2(loaded.value.document);
8997
9867
  return {
8998
9868
  atlasDocument: atlasDocument2,
8999
- source: formatDocument(atlasDocument2, { schema: "2.0" }),
9869
+ source: formatAtlasSource(atlasDocument2),
9000
9870
  diagnostics: mergeDiagnostics(resolveAtlasDiagnostics(atlasDocument2, loaded.diagnostics), collectDocumentDiagnostics(atlasDocument2))
9001
9871
  };
9002
9872
  }
@@ -9004,10 +9874,13 @@ var WorldOrbit = (() => {
9004
9874
  const atlasDocument = createEmptyAtlasDocument("WorldOrbit");
9005
9875
  return {
9006
9876
  atlasDocument,
9007
- source: formatDocument(atlasDocument, { schema: "2.0" }),
9877
+ source: formatAtlasSource(atlasDocument),
9008
9878
  diagnostics: collectDocumentDiagnostics(atlasDocument)
9009
9879
  };
9010
9880
  }
9881
+ function formatAtlasSource(document2) {
9882
+ return formatDocument(document2, { schema: document2.version });
9883
+ }
9011
9884
  function buildEditorMarkup() {
9012
9885
  const previewOpen = shouldPreviewSectionBeOpenByDefault();
9013
9886
  return `<section class="wo-editor-shell">
@@ -9122,6 +9995,12 @@ var WorldOrbit = (() => {
9122
9995
  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>` : "";
9123
9996
  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>`;
9124
9997
  }
9998
+ function renderEventOutlineItems(eventEntry, activeKey, diagnosticBuckets) {
9999
+ return `<div class="wo-editor-outline-group">
10000
+ ${renderOutlineButton({ kind: "event", id: eventEntry.id }, eventEntry.label || eventEntry.id, activeKey, diagnosticBuckets)}
10001
+ ${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>` : ""}
10002
+ </div>`;
10003
+ }
9125
10004
  function renderSystemInspector(formState) {
9126
10005
  return `<form class="wo-editor-form" data-editor-form="system">
9127
10006
  <h2>System</h2>
@@ -9188,6 +10067,7 @@ var WorldOrbit = (() => {
9188
10067
  ${renderCheckboxField("Guides", "layer-guides", viewpoint.layers.guides !== false)}
9189
10068
  ${renderCheckboxField("Orbits back", "layer-orbits-back", viewpoint.layers["orbits-back"] !== false)}
9190
10069
  ${renderCheckboxField("Orbits front", "layer-orbits-front", viewpoint.layers["orbits-front"] !== false)}
10070
+ ${renderCheckboxField("Events", "layer-events", viewpoint.layers.events !== false)}
9191
10071
  ${renderCheckboxField("Objects", "layer-objects", viewpoint.layers.objects !== false)}
9192
10072
  ${renderCheckboxField("Labels", "layer-labels", viewpoint.layers.labels !== false)}
9193
10073
  ${renderCheckboxField("Metadata", "layer-metadata", viewpoint.layers.metadata !== false)}
@@ -9195,7 +10075,70 @@ var WorldOrbit = (() => {
9195
10075
  ${renderInspectorSection("viewpoint", "filter", "Filter", `${renderTextField("Filter query", "filter-query", viewpoint.filter?.query ?? "")}
9196
10076
  ${renderTextField("Filter object types", "filter-object-types", viewpoint.filter?.objectTypes.join(" ") ?? "")}
9197
10077
  ${renderTextField("Filter tags", "filter-tags", viewpoint.filter?.tags.join(" ") ?? "")}
9198
- ${renderTextField("Filter groups", "filter-groups", viewpoint.filter?.groupIds.join(" ") ?? "")}`)}
10078
+ ${renderTextField("Filter groups", "filter-groups", viewpoint.filter?.groupIds.join(" ") ?? "")}
10079
+ ${renderTextField("Events", "viewpoint-events", viewpoint.events.join(" "))}`)}
10080
+ </form>`;
10081
+ }
10082
+ function renderEventInspector(formState, id) {
10083
+ const eventEntry = formState.events.find((entry) => entry.id === id);
10084
+ if (!eventEntry) {
10085
+ return `<p class="wo-editor-empty">Event not found.</p>`;
10086
+ }
10087
+ const linkedViewpoints = formState.viewpoints.filter((viewpoint) => viewpoint.events.includes(eventEntry.id)).map((viewpoint) => viewpoint.id).join(" ");
10088
+ return `<form class="wo-editor-form" data-editor-form="event">
10089
+ <h2>Event</h2>
10090
+ ${renderInspectorSection("event", "basics", "Basics", `${renderTextField("ID", "event-id", eventEntry.id)}
10091
+ ${renderTextField("Kind", "event-kind", eventEntry.kind)}
10092
+ ${renderTextField("Label", "event-label", eventEntry.label)}
10093
+ ${renderTextAreaField("Summary", "event-summary", eventEntry.summary ?? "")}
10094
+ ${renderTextField("Target object", "event-target", eventEntry.targetObjectId ?? "")}
10095
+ ${renderTextField("Participants", "event-participants", eventEntry.participantObjectIds.join(" "))}
10096
+ ${renderTextField("Timing", "event-timing", eventEntry.timing ?? "")}
10097
+ ${renderTextField("Visibility", "event-visibility", eventEntry.visibility ?? "")}
10098
+ ${renderTextField("Tags", "event-tags", eventEntry.tags.join(" "))}
10099
+ ${renderTextField("Color", "event-color", eventEntry.color ?? "")}
10100
+ ${renderCheckboxField("Hidden", "event-hidden", eventEntry.hidden === true)}`, true)}
10101
+ ${renderInspectorSection("event", "viewpoints", "Viewpoints", `${renderTextField("Viewpoints", "event-viewpoints", linkedViewpoints)}`)}
10102
+ ${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>`}
10103
+ <div class="wo-editor-inline-actions">
10104
+ <button type="button" data-editor-action="add-event-pose" data-editor-event-id="${escapeHtml3(eventEntry.id)}">Add pose</button>
10105
+ </div>`)}
10106
+ </form>`;
10107
+ }
10108
+ function renderEventPoseInspector(formState, eventId, objectId) {
10109
+ const eventEntry = formState.events.find((entry) => entry.id === eventId);
10110
+ const pose = eventEntry?.positions.find((entry) => entry.objectId === objectId);
10111
+ if (!eventEntry || !pose) {
10112
+ return `<p class="wo-editor-empty">Event pose not found.</p>`;
10113
+ }
10114
+ const placementMode = pose.placement?.mode ?? "";
10115
+ const placementTarget = pose.placement?.mode === "orbit" || pose.placement?.mode === "surface" || pose.placement?.mode === "at" ? pose.placement.target : "";
10116
+ const freeValue = pose.placement?.mode === "free" ? pose.placement.distance ? formatUnitValue3(pose.placement.distance) : pose.placement.descriptor ?? "" : "";
10117
+ return `<form class="wo-editor-form" data-editor-form="event-pose">
10118
+ <h2>Event Pose</h2>
10119
+ <p class="wo-editor-inline-note">Event <strong>${escapeHtml3(eventEntry.label || eventEntry.id)}</strong></p>
10120
+ ${renderInspectorSection("event-pose", "identity", "Identity", `${renderTextField("Object", "pose-object-id", pose.objectId)}
10121
+ <div class="wo-editor-inline-actions">
10122
+ <button type="button" data-path-kind="event" data-path-id="${escapeHtml3(eventEntry.id)}">Select event</button>
10123
+ </div>`, true)}
10124
+ ${renderInspectorSection("event-pose", "placement", "Placement", `${renderSelectField("Placement mode", "placement-mode", [
10125
+ ["", "None"],
10126
+ ["orbit", "Orbit"],
10127
+ ["at", "At"],
10128
+ ["surface", "Surface"],
10129
+ ["free", "Free"]
10130
+ ], placementMode)}
10131
+ ${renderTextField("Placement target", "placement-target", placementTarget)}
10132
+ ${renderTextField("Free value", "placement-free", freeValue)}
10133
+ ${renderTextField("Distance", "placement-distance", pose.placement?.mode === "orbit" && pose.placement.distance ? formatUnitValue3(pose.placement.distance) : "")}
10134
+ ${renderTextField("Semi-major", "placement-semiMajor", pose.placement?.mode === "orbit" && pose.placement.semiMajor ? formatUnitValue3(pose.placement.semiMajor) : "")}
10135
+ ${renderTextField("Eccentricity", "placement-eccentricity", pose.placement?.mode === "orbit" && pose.placement.eccentricity !== void 0 ? String(pose.placement.eccentricity) : "")}
10136
+ ${renderTextField("Period", "placement-period", pose.placement?.mode === "orbit" && pose.placement.period ? formatUnitValue3(pose.placement.period) : "")}
10137
+ ${renderTextField("Angle", "placement-angle", pose.placement?.mode === "orbit" && pose.placement.angle ? formatUnitValue3(pose.placement.angle) : "")}
10138
+ ${renderTextField("Inclination", "placement-inclination", pose.placement?.mode === "orbit" && pose.placement.inclination ? formatUnitValue3(pose.placement.inclination) : "")}
10139
+ ${renderTextField("Phase", "placement-phase", pose.placement?.mode === "orbit" && pose.placement.phase ? formatUnitValue3(pose.placement.phase) : "")}
10140
+ ${renderTextField("Inner", "prop-inner", pose.inner ? formatUnitValue3(pose.inner) : "")}
10141
+ ${renderTextField("Outer", "prop-outer", pose.outer ? formatUnitValue3(pose.outer) : "")}`, true)}
9199
10142
  </form>`;
9200
10143
  }
9201
10144
  function renderAnnotationInspector(formState, id) {
@@ -9300,13 +10243,19 @@ var WorldOrbit = (() => {
9300
10243
  return form.elements.namedItem(name)?.checked ?? false;
9301
10244
  }
9302
10245
  function buildPlacementFromForm(form, current) {
10246
+ return buildPlacementFromValues(form, current.placement, current.id);
10247
+ }
10248
+ function buildPlacementFromPoseForm(form, current) {
10249
+ return buildPlacementFromValues(form, current.placement, current.objectId);
10250
+ }
10251
+ function buildPlacementFromValues(form, currentPlacement, fallbackTarget) {
9303
10252
  const mode = readTextInput(form, "placement-mode");
9304
10253
  const target = readOptionalTextInput(form, "placement-target");
9305
10254
  switch (mode) {
9306
10255
  case "orbit":
9307
10256
  return {
9308
10257
  mode,
9309
- target: target ?? (current.placement?.mode === "orbit" ? current.placement.target : current.id),
10258
+ target: target ?? (currentPlacement?.mode === "orbit" ? currentPlacement.target : fallbackTarget),
9310
10259
  distance: parseOptionalUnit(readOptionalTextInput(form, "placement-distance")),
9311
10260
  semiMajor: parseOptionalUnit(readOptionalTextInput(form, "placement-semiMajor")),
9312
10261
  eccentricity: parseNullableNumber(readOptionalTextInput(form, "placement-eccentricity")) ?? void 0,
@@ -9318,13 +10267,13 @@ var WorldOrbit = (() => {
9318
10267
  case "at":
9319
10268
  return {
9320
10269
  mode,
9321
- target: target ?? current.id,
9322
- reference: parseAtReferenceString(target ?? current.id)
10270
+ target: target ?? fallbackTarget,
10271
+ reference: parseAtReferenceString(target ?? fallbackTarget)
9323
10272
  };
9324
10273
  case "surface":
9325
10274
  return {
9326
10275
  mode,
9327
- target: target ?? current.id
10276
+ target: target ?? fallbackTarget
9328
10277
  };
9329
10278
  case "free": {
9330
10279
  const freeValue = readOptionalTextInput(form, "placement-free");
@@ -9455,9 +10404,48 @@ var WorldOrbit = (() => {
9455
10404
  annotation.sourceObjectId = toId;
9456
10405
  }
9457
10406
  }
10407
+ for (const eventEntry of document2.events) {
10408
+ if (eventEntry.targetObjectId === fromId) {
10409
+ eventEntry.targetObjectId = toId;
10410
+ }
10411
+ eventEntry.participantObjectIds = eventEntry.participantObjectIds.map((entry) => entry === fromId ? toId : entry);
10412
+ for (const pose of eventEntry.positions) {
10413
+ if (pose.objectId === fromId) {
10414
+ pose.objectId = toId;
10415
+ }
10416
+ if (pose.placement?.mode === "orbit" && pose.placement.target === fromId) {
10417
+ pose.placement.target = toId;
10418
+ }
10419
+ if (pose.placement?.mode === "surface" && pose.placement.target === fromId) {
10420
+ pose.placement.target = toId;
10421
+ }
10422
+ if (pose.placement?.mode === "at") {
10423
+ const reference = pose.placement.reference;
10424
+ if (reference.kind === "anchor" && reference.objectId === fromId) {
10425
+ reference.objectId = toId;
10426
+ }
10427
+ if (reference.kind === "lagrange") {
10428
+ if (reference.primary === fromId) {
10429
+ reference.primary = toId;
10430
+ }
10431
+ if (reference.secondary === fromId) {
10432
+ reference.secondary = toId;
10433
+ }
10434
+ }
10435
+ pose.placement.target = formatAtReference2(reference);
10436
+ }
10437
+ }
10438
+ eventEntry.positions.sort(compareEventPoses);
10439
+ }
9458
10440
  }
9459
10441
  function removeSelectedNode(document2, selection) {
9460
10442
  const next = removeAtlasDocumentNode(document2, selection);
10443
+ if (selection.kind === "event" && selection.id) {
10444
+ for (const viewpoint of next.system?.viewpoints ?? []) {
10445
+ viewpoint.events = viewpoint.events.filter((eventId) => eventId !== selection.id);
10446
+ }
10447
+ return next;
10448
+ }
9461
10449
  if (selection.kind !== "object" || !selection.id) {
9462
10450
  return next;
9463
10451
  }
@@ -9492,9 +10480,45 @@ var WorldOrbit = (() => {
9492
10480
  annotation.sourceObjectId = null;
9493
10481
  }
9494
10482
  }
10483
+ for (const eventEntry of next.events) {
10484
+ if (eventEntry.targetObjectId === selection.id) {
10485
+ eventEntry.targetObjectId = null;
10486
+ }
10487
+ eventEntry.participantObjectIds = eventEntry.participantObjectIds.filter((entry) => entry !== selection.id);
10488
+ eventEntry.positions = eventEntry.positions.filter((entry) => entry.objectId !== selection.id);
10489
+ for (const pose of eventEntry.positions) {
10490
+ if (pose.placement?.mode === "orbit" && pose.placement.target === selection.id) {
10491
+ pose.placement = null;
10492
+ }
10493
+ if (pose.placement?.mode === "surface" && pose.placement.target === selection.id) {
10494
+ pose.placement = null;
10495
+ }
10496
+ if (pose.placement?.mode === "at") {
10497
+ const reference = pose.placement.reference;
10498
+ const touchesSelection = reference.kind === "anchor" && reference.objectId === selection.id || reference.kind === "lagrange" && (reference.primary === selection.id || reference.secondary === selection.id);
10499
+ if (touchesSelection) {
10500
+ pose.placement = null;
10501
+ }
10502
+ }
10503
+ }
10504
+ }
9495
10505
  return next;
9496
10506
  }
9497
- function updateOrbitPhase(document2, objectId, details, pointer) {
10507
+ function findEditablePlacementOwner(document2, path, objectId) {
10508
+ if (path.kind === "event-pose" && path.id && path.key) {
10509
+ const pose = findEventPose2(document2, path.id, path.key);
10510
+ if (pose?.placement) {
10511
+ return { placement: pose.placement };
10512
+ }
10513
+ return null;
10514
+ }
10515
+ const object = findObject2(document2, objectId);
10516
+ if (object?.placement) {
10517
+ return { placement: object.placement };
10518
+ }
10519
+ return null;
10520
+ }
10521
+ function updateOrbitPhase(document2, path, objectId, details, pointer) {
9498
10522
  const orbit = details.orbit;
9499
10523
  if (!orbit || details.object.placement?.mode !== "orbit") {
9500
10524
  return document2;
@@ -9505,17 +10529,17 @@ var WorldOrbit = (() => {
9505
10529
  const radians = Math.atan2((unrotated.y - orbit.cy) / Math.max(ry, 1), (unrotated.x - orbit.cx) / Math.max(rx, 1));
9506
10530
  const phaseDeg = normalizeDegrees(radians * 180 / Math.PI);
9507
10531
  const next = cloneAtlasDocument(document2);
9508
- const object = next.objects.find((entry) => entry.id === objectId);
9509
- if (!object || object.placement?.mode !== "orbit") {
10532
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10533
+ if (!placementOwner || placementOwner.placement.mode !== "orbit") {
9510
10534
  return document2;
9511
10535
  }
9512
- object.placement.phase = {
10536
+ placementOwner.placement.phase = {
9513
10537
  value: roundNumber(phaseDeg, 2),
9514
10538
  unit: "deg"
9515
10539
  };
9516
10540
  return next;
9517
10541
  }
9518
- function updateOrbitRadius(document2, objectId, details, pointer, dragContext) {
10542
+ function updateOrbitRadius(document2, path, objectId, details, pointer, dragContext) {
9519
10543
  const orbit = details.orbit;
9520
10544
  if (!orbit || details.object.placement?.mode !== "orbit" || !dragContext) {
9521
10545
  return document2;
@@ -9525,47 +10549,47 @@ var WorldOrbit = (() => {
9525
10549
  const nextBaseRadius = Math.max(nextDisplayedRadius - dragContext.radiusOffsetPx, dragContext.innerPx);
9526
10550
  const nextMetric = orbitRadiusPxToMetric(nextBaseRadius, dragContext.innerPx, dragContext.stepPx);
9527
10551
  const next = cloneAtlasDocument(document2);
9528
- const object = next.objects.find((entry) => entry.id === objectId);
9529
- if (!object || object.placement?.mode !== "orbit") {
10552
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10553
+ if (!placementOwner || placementOwner.placement.mode !== "orbit") {
9530
10554
  return document2;
9531
10555
  }
9532
- const currentValue = object.placement.semiMajor ?? object.placement.distance ?? {
10556
+ const currentValue = placementOwner.placement.semiMajor ?? placementOwner.placement.distance ?? {
9533
10557
  value: 1,
9534
10558
  unit: "au"
9535
10559
  };
9536
10560
  const scaled = distanceMetricToUnitValue(Math.max(nextMetric, 0), dragContext.preferredUnit ?? currentValue.unit);
9537
- if (object.placement.semiMajor) {
9538
- object.placement.semiMajor = scaled;
10561
+ if (placementOwner.placement.semiMajor) {
10562
+ placementOwner.placement.semiMajor = scaled;
9539
10563
  } else {
9540
- object.placement.distance = scaled;
10564
+ placementOwner.placement.distance = scaled;
9541
10565
  }
9542
10566
  return next;
9543
10567
  }
9544
- function updateAtReference(document2, objectId, scene, pointer) {
10568
+ function updateAtReference(document2, path, objectId, scene, pointer) {
9545
10569
  const candidate = findNearestAtCandidate(scene, objectId, pointer);
9546
10570
  if (!candidate) {
9547
10571
  return document2;
9548
10572
  }
9549
10573
  const next = cloneAtlasDocument(document2);
9550
- const object = next.objects.find((entry) => entry.id === objectId);
9551
- if (!object || object.placement?.mode !== "at") {
10574
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10575
+ if (!placementOwner || placementOwner.placement.mode !== "at") {
9552
10576
  return document2;
9553
10577
  }
9554
- object.placement.reference = candidate.reference;
9555
- object.placement.target = formatAtReference2(candidate.reference);
10578
+ placementOwner.placement.reference = candidate.reference;
10579
+ placementOwner.placement.target = formatAtReference2(candidate.reference);
9556
10580
  return next;
9557
10581
  }
9558
- function updateSurfaceTarget(document2, objectId, scene, pointer) {
10582
+ function updateSurfaceTarget(document2, path, objectId, scene, pointer) {
9559
10583
  const target = findNearestSceneObject(scene, objectId, pointer, (entry) => SURFACE_TARGET_TYPES3.has(entry.object.type));
9560
10584
  if (!target) {
9561
10585
  return document2;
9562
10586
  }
9563
10587
  const next = cloneAtlasDocument(document2);
9564
- const object = next.objects.find((entry) => entry.id === objectId);
9565
- if (!object || object.placement?.mode !== "surface") {
10588
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10589
+ if (!placementOwner || placementOwner.placement.mode !== "surface") {
9566
10590
  return document2;
9567
10591
  }
9568
- object.placement.target = target.objectId;
10592
+ placementOwner.placement.target = target.objectId;
9569
10593
  return next;
9570
10594
  }
9571
10595
  function createOrbitRadiusDragContext(document2, scene, details) {
@@ -9573,7 +10597,7 @@ var WorldOrbit = (() => {
9573
10597
  return null;
9574
10598
  }
9575
10599
  const targetId = details.object.placement.target;
9576
- const siblingCount = document2.objects.filter((entry) => entry.placement?.mode === "orbit" && entry.placement.target === targetId).length;
10600
+ const siblingCount = scene.objects.filter((entry) => entry.object.placement?.mode === "orbit" && entry.object.placement.target === targetId && !entry.hidden).length;
9577
10601
  const spacingFactor = layoutPresetSpacingForScene(scene.layoutPreset);
9578
10602
  const stepPx = (siblingCount > 2 ? 54 : 64) * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
9579
10603
  const innerPx = details.parent.radius + 56 * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
@@ -9588,28 +10612,28 @@ var WorldOrbit = (() => {
9588
10612
  preferredUnit: currentValue?.unit ?? null
9589
10613
  };
9590
10614
  }
9591
- function updateFreeDistance(document2, objectId, scene, details, pointer) {
10615
+ function updateFreeDistance(document2, path, objectId, scene, details, pointer) {
9592
10616
  if (details.object.placement?.mode !== "free") {
9593
10617
  return document2;
9594
10618
  }
9595
10619
  const railX = scene.width - scene.padding - 140;
9596
10620
  const offsetPx = Math.max(0, railX - pointer.x);
9597
10621
  const next = cloneAtlasDocument(document2);
9598
- const object = next.objects.find((entry) => entry.id === objectId);
9599
- if (!object || object.placement?.mode !== "free") {
10622
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
10623
+ if (!placementOwner || placementOwner.placement.mode !== "free") {
9600
10624
  return document2;
9601
10625
  }
9602
- const preferredUnit = normalizeFreeDistanceUnit(object.placement.distance?.unit ?? null);
10626
+ const preferredUnit = normalizeFreeDistanceUnit(placementOwner.placement.distance?.unit ?? null);
9603
10627
  const metric = offsetPx / Math.max(FREE_DISTANCE_PIXEL_FACTOR * scene.scaleModel.freePlacementMultiplier, 1);
9604
10628
  if (metric < 0.01) {
9605
- object.placement.distance = void 0;
9606
- if (!object.placement.descriptor) {
9607
- delete object.placement.descriptor;
10629
+ placementOwner.placement.distance = void 0;
10630
+ if (!placementOwner.placement.descriptor) {
10631
+ delete placementOwner.placement.descriptor;
9608
10632
  }
9609
10633
  return next;
9610
10634
  }
9611
- object.placement.distance = distanceMetricToUnitValue(metric, preferredUnit);
9612
- delete object.placement.descriptor;
10635
+ placementOwner.placement.distance = distanceMetricToUnitValue(metric, preferredUnit);
10636
+ delete placementOwner.placement.descriptor;
9613
10637
  return next;
9614
10638
  }
9615
10639
  function findNearestSceneObject(scene, selectedObjectId, pointer, predicate = () => true) {
@@ -9881,9 +10905,60 @@ var WorldOrbit = (() => {
9881
10905
  return ["viewpoint-zoom"];
9882
10906
  case "rotationDeg":
9883
10907
  return ["viewpoint-rotation"];
10908
+ case "events":
10909
+ return ["viewpoint-events"];
9884
10910
  default:
9885
10911
  return [];
9886
10912
  }
10913
+ case "event":
10914
+ switch (field) {
10915
+ case "id":
10916
+ return ["event-id"];
10917
+ case "kind":
10918
+ return ["event-kind"];
10919
+ case "label":
10920
+ return ["event-label"];
10921
+ case "summary":
10922
+ return ["event-summary"];
10923
+ case "targetObjectId":
10924
+ case "target":
10925
+ return ["event-target"];
10926
+ case "participantObjectIds":
10927
+ case "participants":
10928
+ return ["event-participants"];
10929
+ case "timing":
10930
+ return ["event-timing"];
10931
+ case "visibility":
10932
+ return ["event-visibility"];
10933
+ case "tags":
10934
+ return ["event-tags"];
10935
+ case "color":
10936
+ return ["event-color"];
10937
+ case "hidden":
10938
+ return ["event-hidden"];
10939
+ default:
10940
+ return [];
10941
+ }
10942
+ case "event-pose":
10943
+ if (field === "objectId") {
10944
+ return ["pose-object-id"];
10945
+ }
10946
+ if (field === "placement") {
10947
+ return ["placement-mode"];
10948
+ }
10949
+ if (field === "reference" || field === "target") {
10950
+ return ["placement-target"];
10951
+ }
10952
+ if (field === "descriptor") {
10953
+ return ["placement-free"];
10954
+ }
10955
+ if (PLACEMENT_DIAGNOSTIC_FIELDS.has(field)) {
10956
+ return [`placement-${field}`];
10957
+ }
10958
+ if (field === "inner" || field === "outer") {
10959
+ return [`prop-${field}`];
10960
+ }
10961
+ return [];
9887
10962
  case "annotation":
9888
10963
  switch (field) {
9889
10964
  case "id":
@@ -9969,6 +11044,10 @@ var WorldOrbit = (() => {
9969
11044
  return `Metadata: ${path.key ?? ""}`;
9970
11045
  case "group":
9971
11046
  return `Group: ${path.id ?? ""}`;
11047
+ case "event":
11048
+ return `Event: ${path.id ?? ""}`;
11049
+ case "event-pose":
11050
+ return `Event Pose: ${path.id ?? ""} / ${path.key ?? ""}`;
9972
11051
  case "object":
9973
11052
  return `Object: ${path.id ?? ""}`;
9974
11053
  case "viewpoint":
@@ -9980,11 +11059,70 @@ var WorldOrbit = (() => {
9980
11059
  }
9981
11060
  }
9982
11061
  function selectionKey(path) {
9983
- return path ? `${path.kind}:${path.id ?? path.key ?? ""}` : null;
11062
+ return path ? `${path.kind}:${path.id ?? ""}:${path.key ?? ""}` : null;
11063
+ }
11064
+ function selectionEventId(path) {
11065
+ if (!path) {
11066
+ return null;
11067
+ }
11068
+ return path.kind === "event" || path.kind === "event-pose" ? path.id ?? null : null;
9984
11069
  }
9985
11070
  function compareObjects2(left, right) {
9986
11071
  return left.id.localeCompare(right.id);
9987
11072
  }
11073
+ function compareEvents(left, right) {
11074
+ return left.id.localeCompare(right.id);
11075
+ }
11076
+ function compareEventPoses(left, right) {
11077
+ return left.objectId.localeCompare(right.objectId);
11078
+ }
11079
+ function findEvent2(document2, eventId) {
11080
+ return document2.events.find((entry) => entry.id === eventId) ?? null;
11081
+ }
11082
+ function findEventPose2(document2, eventId, objectId) {
11083
+ return findEvent2(document2, eventId)?.positions.find((entry) => entry.objectId === objectId) ?? null;
11084
+ }
11085
+ function findObject2(document2, objectId) {
11086
+ return document2.objects.find((entry) => entry.id === objectId) ?? null;
11087
+ }
11088
+ function addEventPose(document2, eventId) {
11089
+ const next = cloneAtlasDocument(document2);
11090
+ const eventEntry = next.events.find((entry) => entry.id === eventId);
11091
+ if (!eventEntry) {
11092
+ return document2;
11093
+ }
11094
+ const baseObject = next.objects.find((object) => !eventEntry.positions.some((pose) => pose.objectId === object.id)) ?? next.objects[0];
11095
+ if (!baseObject) {
11096
+ return document2;
11097
+ }
11098
+ if (eventEntry.targetObjectId !== baseObject.id && !eventEntry.participantObjectIds.includes(baseObject.id)) {
11099
+ eventEntry.participantObjectIds.push(baseObject.id);
11100
+ eventEntry.participantObjectIds.sort((left, right) => left.localeCompare(right));
11101
+ }
11102
+ eventEntry.positions.push(createEventPoseFromObject(baseObject));
11103
+ eventEntry.positions.sort(compareEventPoses);
11104
+ return next;
11105
+ }
11106
+ function createEventPoseFromObject(object) {
11107
+ return {
11108
+ objectId: object.id,
11109
+ placement: object.placement ? structuredClone(object.placement) : null,
11110
+ inner: readUnitValue(object.properties.inner),
11111
+ outer: readUnitValue(object.properties.outer)
11112
+ };
11113
+ }
11114
+ function syncEventViewpointReferences(document2, previousEventId, nextEventId, viewpointIds) {
11115
+ const desired = new Set(viewpointIds);
11116
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
11117
+ const currentIds = new Set(viewpoint.events);
11118
+ currentIds.delete(previousEventId);
11119
+ currentIds.delete(nextEventId);
11120
+ if (desired.has(viewpoint.id)) {
11121
+ currentIds.add(nextEventId);
11122
+ }
11123
+ viewpoint.events = [...currentIds].sort((left, right) => left.localeCompare(right));
11124
+ }
11125
+ }
9988
11126
  function createUniqueId(prefix, existing) {
9989
11127
  const safePrefix = prefix.trim() || "item";
9990
11128
  let counter = 1;
@@ -10013,6 +11151,9 @@ var WorldOrbit = (() => {
10013
11151
  function readUnitProperty(value) {
10014
11152
  return value && typeof value === "object" && "value" in value ? formatUnitValue3(value) : "";
10015
11153
  }
11154
+ function readUnitValue(value) {
11155
+ return value && typeof value === "object" && "value" in value ? value : void 0;
11156
+ }
10016
11157
  function readNumberProperty(value) {
10017
11158
  return typeof value === "number" ? String(value) : "";
10018
11159
  }
@@ -10318,6 +11459,12 @@ var WorldOrbit = (() => {
10318
11459
  .wo-editor-overlay-diagnostic-warning { border: 1px solid rgba(240, 180, 100, 0.24); }
10319
11460
  .wo-editor-outline { display: grid; gap: 14px; }
10320
11461
  .wo-editor-outline-section { display: grid; gap: 8px; }
11462
+ .wo-editor-outline-group { display: grid; gap: 6px; }
11463
+ .wo-editor-outline-children {
11464
+ display: grid;
11465
+ gap: 6px;
11466
+ padding-left: 16px;
11467
+ }
10321
11468
  .wo-editor-outline-section h3 {
10322
11469
  margin: 0;
10323
11470
  color: rgba(237,246,255,0.68);
@@ -10357,6 +11504,27 @@ var WorldOrbit = (() => {
10357
11504
  background: rgba(255, 120, 120, 0.18);
10358
11505
  color: #ffb2b2;
10359
11506
  }
11507
+ .wo-editor-inline-list { display: grid; gap: 8px; }
11508
+ .wo-editor-inline-actions {
11509
+ display: flex;
11510
+ flex-wrap: wrap;
11511
+ gap: 10px;
11512
+ margin-top: 12px;
11513
+ }
11514
+ .wo-editor-inline-actions button {
11515
+ border: 1px solid rgba(255,255,255,0.14);
11516
+ border-radius: 999px;
11517
+ background: rgba(255,255,255,0.06);
11518
+ color: #edf6ff;
11519
+ cursor: pointer;
11520
+ font: 600 12px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
11521
+ padding: 8px 12px;
11522
+ }
11523
+ .wo-editor-inline-note {
11524
+ margin: 0 0 12px;
11525
+ color: rgba(237,246,255,0.72);
11526
+ font: 500 12px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
11527
+ }
10360
11528
  .wo-editor-diagnostics { display: grid; gap: 10px; }
10361
11529
  .wo-editor-diagnostic {
10362
11530
  display: grid;