worldorbit 2.5.16 → 2.6.0

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 (35) hide show
  1. package/README.md +81 -15
  2. package/dist/browser/core/dist/index.js +1228 -110
  3. package/dist/browser/editor/dist/index.js +1896 -180
  4. package/dist/browser/markdown/dist/index.js +1071 -99
  5. package/dist/browser/viewer/dist/index.js +1127 -113
  6. package/dist/unpkg/core/dist/index.js +1228 -110
  7. package/dist/unpkg/editor/dist/index.js +1896 -180
  8. package/dist/unpkg/markdown/dist/index.js +1071 -99
  9. package/dist/unpkg/viewer/dist/index.js +1127 -113
  10. package/dist/unpkg/worldorbit-core.min.js +12 -12
  11. package/dist/unpkg/worldorbit-editor.min.js +295 -203
  12. package/dist/unpkg/worldorbit-markdown.min.js +66 -58
  13. package/dist/unpkg/worldorbit-viewer.min.js +84 -76
  14. package/dist/unpkg/worldorbit.js +1304 -124
  15. package/dist/unpkg/worldorbit.min.js +88 -80
  16. package/package.json +1 -1
  17. package/packages/core/dist/atlas-edit.js +75 -1
  18. package/packages/core/dist/atlas-validate.js +211 -8
  19. package/packages/core/dist/draft-parse.js +401 -22
  20. package/packages/core/dist/draft.d.ts +5 -2
  21. package/packages/core/dist/draft.js +103 -8
  22. package/packages/core/dist/format.js +99 -6
  23. package/packages/core/dist/load.js +9 -2
  24. package/packages/core/dist/normalize.js +1 -0
  25. package/packages/core/dist/scene.js +400 -64
  26. package/packages/core/dist/types.d.ts +60 -4
  27. package/packages/editor/dist/editor.js +702 -65
  28. package/packages/editor/dist/types.d.ts +3 -1
  29. package/packages/viewer/dist/atlas-state.js +11 -2
  30. package/packages/viewer/dist/atlas-viewer.js +19 -7
  31. package/packages/viewer/dist/render.js +31 -2
  32. package/packages/viewer/dist/theme.js +1 -0
  33. package/packages/viewer/dist/tooltip.js +9 -0
  34. package/packages/viewer/dist/types.d.ts +12 -2
  35. package/packages/viewer/dist/viewer.js +28 -1
@@ -544,6 +544,7 @@
544
544
  system,
545
545
  groups: [],
546
546
  relations: [],
547
+ events: [],
547
548
  objects
548
549
  };
549
550
  }
@@ -997,12 +998,16 @@
997
998
  const height = frame.height;
998
999
  const padding = frame.padding;
999
1000
  const layoutPreset = resolveLayoutPreset(document);
1000
- const projection = resolveProjection(document, options.projection);
1001
+ const schemaProjection = resolveProjection(document, options.projection);
1002
+ const camera = normalizeViewCamera(options.camera ?? null);
1003
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
1001
1004
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1002
1005
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1003
1006
  const systemId = document.system?.id ?? null;
1004
- const objectMap = new Map(document.objects.map((object) => [object.id, object]));
1005
- const relationships = buildSceneRelationships(document.objects, objectMap);
1007
+ const activeEventId = options.activeEventId ?? null;
1008
+ const effectiveObjects = createEffectiveObjects(document.objects, document.events ?? [], activeEventId);
1009
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
1010
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
1006
1011
  const positions = /* @__PURE__ */ new Map();
1007
1012
  const orbitDrafts = [];
1008
1013
  const leaderDrafts = [];
@@ -1011,7 +1016,7 @@
1011
1016
  const atObjects = [];
1012
1017
  const surfaceChildren = /* @__PURE__ */ new Map();
1013
1018
  const orbitChildren = /* @__PURE__ */ new Map();
1014
- for (const object of document.objects) {
1019
+ for (const object of effectiveObjects) {
1015
1020
  const placement = object.placement;
1016
1021
  if (!placement) {
1017
1022
  rootObjects.push(object);
@@ -1038,7 +1043,7 @@
1038
1043
  surfaceChildren,
1039
1044
  objectMap,
1040
1045
  spacingFactor,
1041
- projection,
1046
+ projection: renderProjection,
1042
1047
  scaleModel
1043
1048
  };
1044
1049
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -1050,7 +1055,7 @@
1050
1055
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1051
1056
  secondaryRoots.forEach((object, index) => {
1052
1057
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
1053
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
1058
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
1054
1059
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
1055
1060
  });
1056
1061
  }
@@ -1106,38 +1111,48 @@
1106
1111
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1107
1112
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1108
1113
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1109
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1114
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1110
1115
  const relations = createSceneRelations(document, objects);
1111
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1112
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1116
+ const events = createSceneEvents(document.events ?? [], objects, activeEventId);
1117
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1118
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1113
1119
  const semanticGroups = createSceneSemanticGroups(document, objects);
1114
- const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
1115
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1120
+ const viewpoints = createSceneViewpoints(document, schemaProjection, frame.preset, relationships, objectMap);
1121
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1116
1122
  return {
1117
1123
  width,
1118
1124
  height,
1119
1125
  padding,
1120
1126
  renderPreset: frame.preset,
1121
- projection,
1127
+ projection: schemaProjection,
1128
+ renderProjection,
1129
+ camera,
1122
1130
  scaleModel,
1123
1131
  title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1124
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1132
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1125
1133
  systemId,
1126
- viewMode: projection,
1134
+ viewMode: schemaProjection,
1127
1135
  layoutPreset,
1128
1136
  metadata: {
1129
1137
  format: document.format,
1130
1138
  version: document.version,
1131
- view: projection,
1139
+ view: schemaProjection,
1140
+ renderProjection,
1132
1141
  scale: String(document.system?.properties.scale ?? layoutPreset),
1133
1142
  units: String(document.system?.properties.units ?? "mixed"),
1134
- preset: frame.preset ?? "custom"
1143
+ preset: frame.preset ?? "custom",
1144
+ ...camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {},
1145
+ ...camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {},
1146
+ ...camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {},
1147
+ ...camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}
1135
1148
  },
1136
1149
  contentBounds,
1137
1150
  layers,
1138
1151
  groups,
1139
1152
  semanticGroups,
1140
1153
  viewpoints,
1154
+ events,
1155
+ activeEventId,
1141
1156
  objects,
1142
1157
  orbitVisuals,
1143
1158
  relations,
@@ -1156,6 +1171,56 @@
1156
1171
  y: center.y + dx * sin + dy * cos
1157
1172
  };
1158
1173
  }
1174
+ function createEffectiveObjects(objects, events, activeEventId) {
1175
+ const cloned = objects.map((object) => structuredClone(object));
1176
+ if (!activeEventId) {
1177
+ return cloned;
1178
+ }
1179
+ const activeEvent = events.find((event) => event.id === activeEventId);
1180
+ if (!activeEvent) {
1181
+ return cloned;
1182
+ }
1183
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1184
+ const referencedIds = /* @__PURE__ */ new Set([
1185
+ ...activeEvent.targetObjectId ? [activeEvent.targetObjectId] : [],
1186
+ ...activeEvent.participantObjectIds,
1187
+ ...activeEvent.positions.map((pose) => pose.objectId)
1188
+ ]);
1189
+ for (const objectId of referencedIds) {
1190
+ const object = objectMap.get(objectId);
1191
+ if (!object) {
1192
+ continue;
1193
+ }
1194
+ if (activeEvent.epoch) {
1195
+ object.epoch = activeEvent.epoch;
1196
+ }
1197
+ if (activeEvent.referencePlane) {
1198
+ object.referencePlane = activeEvent.referencePlane;
1199
+ }
1200
+ }
1201
+ for (const pose of activeEvent.positions) {
1202
+ const object = objectMap.get(pose.objectId);
1203
+ if (!object) {
1204
+ continue;
1205
+ }
1206
+ if (pose.placement) {
1207
+ object.placement = structuredClone(pose.placement);
1208
+ }
1209
+ if (pose.inner) {
1210
+ object.properties.inner = { ...pose.inner };
1211
+ }
1212
+ if (pose.outer) {
1213
+ object.properties.outer = { ...pose.outer };
1214
+ }
1215
+ if (pose.epoch) {
1216
+ object.epoch = pose.epoch;
1217
+ }
1218
+ if (pose.referencePlane) {
1219
+ object.referencePlane = pose.referencePlane;
1220
+ }
1221
+ }
1222
+ return cloned;
1223
+ }
1159
1224
  function resolveLayoutPreset(document) {
1160
1225
  const rawScale = String(document.system?.properties.scale ?? "balanced").toLowerCase();
1161
1226
  switch (rawScale) {
@@ -1192,10 +1257,59 @@
1192
1257
  }
1193
1258
  }
1194
1259
  function resolveProjection(document, projection) {
1195
- if (projection === "topdown" || projection === "isometric") {
1260
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1196
1261
  return projection;
1197
1262
  }
1198
- return String(document.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1263
+ const documentView = String(document.system?.properties.view ?? "topdown").toLowerCase();
1264
+ return parseViewProjection(documentView) ?? "topdown";
1265
+ }
1266
+ function resolveRenderProjection(projection, camera) {
1267
+ switch (projection) {
1268
+ case "topdown":
1269
+ return "topdown";
1270
+ case "isometric":
1271
+ return "isometric";
1272
+ case "orthographic":
1273
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null) ? "isometric" : "topdown";
1274
+ case "perspective":
1275
+ return "isometric";
1276
+ }
1277
+ }
1278
+ function normalizeViewCamera(camera) {
1279
+ if (!camera) {
1280
+ return null;
1281
+ }
1282
+ const normalized = {
1283
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
1284
+ elevation: normalizeFiniteCameraValue(camera.elevation),
1285
+ roll: normalizeFiniteCameraValue(camera.roll),
1286
+ distance: normalizePositiveCameraDistance(camera.distance)
1287
+ };
1288
+ return normalized.azimuth !== null || normalized.elevation !== null || normalized.roll !== null || normalized.distance !== null ? normalized : null;
1289
+ }
1290
+ function normalizeFiniteCameraValue(value) {
1291
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1292
+ }
1293
+ function normalizePositiveCameraDistance(value) {
1294
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1295
+ }
1296
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
1297
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
1298
+ if (projection !== renderProjection) {
1299
+ parts.push(`2D ${renderProjection} fallback`);
1300
+ }
1301
+ if (camera) {
1302
+ const cameraParts = [
1303
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
1304
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
1305
+ camera.roll !== null ? `roll ${camera.roll}` : null,
1306
+ camera.distance !== null ? `dist ${camera.distance}` : null
1307
+ ].filter(Boolean);
1308
+ if (cameraParts.length > 0) {
1309
+ parts.push(`camera ${cameraParts.join(" / ")}`);
1310
+ }
1311
+ }
1312
+ return parts.join(" - ");
1199
1313
  }
1200
1314
  function resolveScaleModel(layoutPreset, overrides) {
1201
1315
  const defaults = defaultScaleModel(layoutPreset);
@@ -1311,24 +1425,14 @@
1311
1425
  hidden: draft.object.properties.hidden === true
1312
1426
  };
1313
1427
  }
1314
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1428
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1315
1429
  const labels = [];
1316
1430
  const occupied = [];
1317
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1431
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1432
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1318
1433
  for (const object of visibleObjects) {
1319
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1320
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1321
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1322
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1323
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1324
- let attempts = 0;
1325
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1326
- labelY += direction * 14 * labelMultiplier;
1327
- secondaryY += direction * 14 * labelMultiplier;
1328
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1329
- attempts += 1;
1330
- }
1331
- occupied.push(bounds);
1434
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1435
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1332
1436
  labels.push({
1333
1437
  renderId: `${object.renderId}-label`,
1334
1438
  objectId: object.objectId,
@@ -1337,17 +1441,128 @@
1337
1441
  semanticGroupIds: [...object.semanticGroupIds],
1338
1442
  label: object.label,
1339
1443
  secondaryLabel: object.secondaryLabel,
1340
- x: object.x,
1341
- y: labelY,
1342
- secondaryY,
1343
- textAnchor: "middle",
1344
- direction: direction < 0 ? "above" : "below",
1444
+ x: placement.x,
1445
+ y: placement.labelY,
1446
+ secondaryY: placement.secondaryY,
1447
+ textAnchor: placement.textAnchor,
1448
+ direction: placement.direction,
1345
1449
  hidden: object.hidden
1346
1450
  });
1347
1451
  }
1348
1452
  return labels;
1349
1453
  }
1350
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1454
+ function compareLabelPlacementOrder(left, right) {
1455
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1456
+ if (priorityDiff !== 0) {
1457
+ return priorityDiff;
1458
+ }
1459
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1460
+ if (renderPriorityDiff !== 0) {
1461
+ return renderPriorityDiff;
1462
+ }
1463
+ return left.sortKey - right.sortKey;
1464
+ }
1465
+ function labelPlacementPriority(object) {
1466
+ switch (object.object.type) {
1467
+ case "star":
1468
+ return 0;
1469
+ case "planet":
1470
+ return 1;
1471
+ case "moon":
1472
+ return 2;
1473
+ case "belt":
1474
+ case "ring":
1475
+ return 3;
1476
+ case "asteroid":
1477
+ case "comet":
1478
+ return 4;
1479
+ case "structure":
1480
+ case "phenomenon":
1481
+ return 5;
1482
+ }
1483
+ }
1484
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1485
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1486
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1487
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1488
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1489
+ const rect = createLabelRect(object, placement, labelMultiplier);
1490
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1491
+ return placement;
1492
+ }
1493
+ }
1494
+ }
1495
+ return null;
1496
+ }
1497
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1498
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1499
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1500
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1501
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1502
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1503
+ 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";
1504
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1505
+ }
1506
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1507
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1508
+ return object.y >= parent.y ? "below" : "above";
1509
+ }
1510
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1511
+ }
1512
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1513
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1514
+ return object.x >= parent.x ? "right" : "left";
1515
+ }
1516
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1517
+ }
1518
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1519
+ const step = 14 * labelMultiplier;
1520
+ switch (direction) {
1521
+ case "above": {
1522
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1523
+ return {
1524
+ x: object.x,
1525
+ labelY,
1526
+ secondaryY: labelY - 16 * labelMultiplier,
1527
+ textAnchor: "middle",
1528
+ direction
1529
+ };
1530
+ }
1531
+ case "below": {
1532
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1533
+ return {
1534
+ x: object.x,
1535
+ labelY,
1536
+ secondaryY: labelY + 16 * labelMultiplier,
1537
+ textAnchor: "middle",
1538
+ direction
1539
+ };
1540
+ }
1541
+ case "left": {
1542
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1543
+ const labelY = object.y - 4 * labelMultiplier;
1544
+ return {
1545
+ x,
1546
+ labelY,
1547
+ secondaryY: labelY + 16 * labelMultiplier,
1548
+ textAnchor: "end",
1549
+ direction
1550
+ };
1551
+ }
1552
+ case "right": {
1553
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1554
+ const labelY = object.y - 4 * labelMultiplier;
1555
+ return {
1556
+ x,
1557
+ labelY,
1558
+ secondaryY: labelY + 16 * labelMultiplier,
1559
+ textAnchor: "start",
1560
+ direction
1561
+ };
1562
+ }
1563
+ }
1564
+ }
1565
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1351
1566
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1352
1567
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1353
1568
  return [
@@ -1362,6 +1577,10 @@
1362
1577
  id: "relations",
1363
1578
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1364
1579
  },
1580
+ {
1581
+ id: "events",
1582
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1583
+ },
1365
1584
  {
1366
1585
  id: "objects",
1367
1586
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1373,7 +1592,7 @@
1373
1592
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1374
1593
  ];
1375
1594
  }
1376
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1595
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1377
1596
  const groups = /* @__PURE__ */ new Map();
1378
1597
  const ensureGroup = (groupId) => {
1379
1598
  if (!groupId) {
@@ -1422,7 +1641,7 @@
1422
1641
  }
1423
1642
  }
1424
1643
  for (const group of groups.values()) {
1425
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1644
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1426
1645
  }
1427
1646
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1428
1647
  }
@@ -1456,6 +1675,29 @@
1456
1675
  };
1457
1676
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1458
1677
  }
1678
+ function createSceneEvents(events, objects, activeEventId) {
1679
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1680
+ return events.map((event) => {
1681
+ const objectIds = [.../* @__PURE__ */ new Set([
1682
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1683
+ ...event.participantObjectIds
1684
+ ])];
1685
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1686
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1687
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1688
+ return {
1689
+ renderId: `${createRenderId(event.id)}-event`,
1690
+ eventId: event.id,
1691
+ event,
1692
+ objectIds,
1693
+ participantIds: [...event.participantObjectIds],
1694
+ targetObjectId: event.targetObjectId,
1695
+ x: centroidX,
1696
+ y: centroidY,
1697
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1698
+ };
1699
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1700
+ }
1459
1701
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
1460
1702
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
1461
1703
  const drafts = /* @__PURE__ */ new Map();
@@ -1503,13 +1745,18 @@
1503
1745
  function createGeneratedOverviewViewpoint(document, projection, preset) {
1504
1746
  const title = document.system?.title ?? document.system?.properties.title;
1505
1747
  const label = title ? `${String(title)} Overview` : "Overview";
1748
+ const camera = normalizeViewCamera(null);
1749
+ const renderProjection = resolveRenderProjection(projection, camera);
1506
1750
  return {
1507
1751
  id: "overview",
1508
1752
  label,
1509
1753
  summary: "Fit the whole system with the current atlas defaults.",
1510
1754
  objectId: null,
1511
1755
  selectedObjectId: null,
1756
+ eventIds: [],
1512
1757
  projection,
1758
+ renderProjection,
1759
+ camera,
1513
1760
  preset,
1514
1761
  rotationDeg: 0,
1515
1762
  scale: null,
@@ -1545,6 +1792,9 @@
1545
1792
  draft.select = normalizedValue;
1546
1793
  }
1547
1794
  return;
1795
+ case "events":
1796
+ draft.eventIds = splitListValue(normalizedValue);
1797
+ return;
1548
1798
  case "projection":
1549
1799
  case "view":
1550
1800
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1556,6 +1806,30 @@
1556
1806
  case "angle":
1557
1807
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1558
1808
  return;
1809
+ case "camera.azimuth":
1810
+ draft.camera = {
1811
+ ...draft.camera ?? createEmptyViewCamera(),
1812
+ azimuth: parseFiniteNumber(normalizedValue)
1813
+ };
1814
+ return;
1815
+ case "camera.elevation":
1816
+ draft.camera = {
1817
+ ...draft.camera ?? createEmptyViewCamera(),
1818
+ elevation: parseFiniteNumber(normalizedValue)
1819
+ };
1820
+ return;
1821
+ case "camera.roll":
1822
+ draft.camera = {
1823
+ ...draft.camera ?? createEmptyViewCamera(),
1824
+ roll: parseFiniteNumber(normalizedValue)
1825
+ };
1826
+ return;
1827
+ case "camera.distance":
1828
+ draft.camera = {
1829
+ ...draft.camera ?? createEmptyViewCamera(),
1830
+ distance: parsePositiveNumber(normalizedValue)
1831
+ };
1832
+ return;
1559
1833
  case "zoom":
1560
1834
  case "scale":
1561
1835
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -1595,13 +1869,19 @@
1595
1869
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1596
1870
  const filter = normalizeViewpointFilter(draft.filter);
1597
1871
  const label = draft.label?.trim() || humanizeIdentifier(draft.id);
1872
+ const resolvedProjection = draft.projection ?? projection;
1873
+ const camera = normalizeViewCamera(draft.camera ?? null);
1874
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
1598
1875
  return {
1599
1876
  id: draft.id,
1600
1877
  label,
1601
1878
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1602
1879
  objectId,
1603
1880
  selectedObjectId,
1604
- projection: draft.projection ?? projection,
1881
+ eventIds: [...new Set(draft.eventIds ?? [])],
1882
+ projection: resolvedProjection,
1883
+ renderProjection,
1884
+ camera,
1605
1885
  preset: draft.preset ?? preset,
1606
1886
  rotationDeg: draft.rotationDeg ?? 0,
1607
1887
  scale: draft.scale ?? null,
@@ -1618,6 +1898,14 @@
1618
1898
  groupIds: []
1619
1899
  };
1620
1900
  }
1901
+ function createEmptyViewCamera() {
1902
+ return {
1903
+ azimuth: null,
1904
+ elevation: null,
1905
+ roll: null,
1906
+ distance: null
1907
+ };
1908
+ }
1621
1909
  function normalizeViewpointFilter(filter) {
1622
1910
  if (!filter) {
1623
1911
  return null;
@@ -1631,7 +1919,18 @@
1631
1919
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1632
1920
  }
1633
1921
  function parseViewProjection(value) {
1634
- return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
1922
+ switch (value.toLowerCase()) {
1923
+ case "topdown":
1924
+ return "topdown";
1925
+ case "isometric":
1926
+ return "isometric";
1927
+ case "orthographic":
1928
+ return "orthographic";
1929
+ case "perspective":
1930
+ return "perspective";
1931
+ default:
1932
+ return null;
1933
+ }
1635
1934
  }
1636
1935
  function parseRenderPreset(value) {
1637
1936
  const normalized = value.toLowerCase();
@@ -1658,7 +1957,7 @@
1658
1957
  next["orbits-front"] = enabled;
1659
1958
  continue;
1660
1959
  }
1661
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1960
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1662
1961
  next[rawLayer] = enabled;
1663
1962
  }
1664
1963
  }
@@ -1669,7 +1968,7 @@
1669
1968
  }
1670
1969
  function parseViewpointGroups(value, document, relationships, objectMap) {
1671
1970
  return splitListValue(value).map((entry) => {
1672
- if (document.schemaVersion === "2.1" || document.groups.some((group) => group.id === entry)) {
1971
+ if (document.schemaVersion === "2.1" || document.schemaVersion === "2.5" || document.groups.some((group) => group.id === entry)) {
1673
1972
  return entry;
1674
1973
  }
1675
1974
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -1706,7 +2005,7 @@
1706
2005
  }
1707
2006
  return parts.join(" - ");
1708
2007
  }
1709
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
2008
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1710
2009
  let minX = Number.POSITIVE_INFINITY;
1711
2010
  let minY = Number.POSITIVE_INFINITY;
1712
2011
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1736,7 +2035,7 @@
1736
2035
  for (const label of labels) {
1737
2036
  if (label.hidden)
1738
2037
  continue;
1739
- includeLabelBounds(label, include);
2038
+ includeLabelBounds(label, include, labelMultiplier);
1740
2039
  }
1741
2040
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1742
2041
  return createBounds(0, 0, width, height);
@@ -1774,13 +2073,10 @@
1774
2073
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1775
2074
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1776
2075
  }
1777
- function includeLabelBounds(label, include) {
1778
- const labelScale = 1;
1779
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1780
- include(label.x - labelHalfWidth, label.y - 18);
1781
- include(label.x + labelHalfWidth, label.y + 8);
1782
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1783
- include(label.x + labelHalfWidth, label.secondaryY + 8);
2076
+ function includeLabelBounds(label, include, labelMultiplier) {
2077
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
2078
+ include(bounds.left, bounds.top);
2079
+ include(bounds.right, bounds.bottom);
1784
2080
  }
1785
2081
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1786
2082
  if (positions.has(object.id)) {
@@ -2170,7 +2466,7 @@
2170
2466
  return null;
2171
2467
  }
2172
2468
  }
2173
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2469
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2174
2470
  let minX = Number.POSITIVE_INFINITY;
2175
2471
  let minY = Number.POSITIVE_INFINITY;
2176
2472
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2199,7 +2495,7 @@
2199
2495
  }
2200
2496
  for (const label of labels) {
2201
2497
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2202
- includeLabelBounds(label, include);
2498
+ includeLabelBounds(label, include, labelMultiplier);
2203
2499
  }
2204
2500
  }
2205
2501
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2224,12 +2520,28 @@
2224
2520
  }
2225
2521
  return current.id;
2226
2522
  }
2227
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2523
+ function createLabelRect(object, placement, labelMultiplier) {
2524
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2525
+ }
2526
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2527
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2528
+ const labelWidth = labelHalfWidth * 2;
2529
+ const topPadding = direction === "above" ? 18 : 12;
2530
+ const bottomPadding = direction === "above" ? 8 : 12;
2531
+ let left = x - labelHalfWidth;
2532
+ let right = x + labelHalfWidth;
2533
+ if (textAnchor === "start") {
2534
+ left = x;
2535
+ right = x + labelWidth;
2536
+ } else if (textAnchor === "end") {
2537
+ left = x - labelWidth;
2538
+ right = x;
2539
+ }
2228
2540
  return {
2229
- left: x - labelHalfWidth,
2230
- right: x + labelHalfWidth,
2231
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2232
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2541
+ left,
2542
+ right,
2543
+ top: Math.min(labelY, secondaryY) - topPadding,
2544
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2233
2545
  };
2234
2546
  }
2235
2547
  function rectsOverlap(left, right) {
@@ -2416,11 +2728,6 @@
2416
2728
  function customColorFor(value) {
2417
2729
  return typeof value === "string" && value.trim() ? value : void 0;
2418
2730
  }
2419
- function estimateLabelHalfWidth(object, labelMultiplier) {
2420
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2421
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2422
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2423
- }
2424
2731
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2425
2732
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2426
2733
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2454,12 +2761,13 @@
2454
2761
  }
2455
2762
  return {
2456
2763
  format: "worldorbit",
2457
- version: "2.0",
2458
- schemaVersion: "2.0",
2764
+ version: "2.5",
2765
+ schemaVersion: "2.5",
2459
2766
  sourceVersion: document.version,
2460
2767
  system,
2461
2768
  groups: structuredClone(document.groups ?? []),
2462
2769
  relations: structuredClone(document.relations ?? []),
2770
+ events: structuredClone(document.events ?? []),
2463
2771
  objects: document.objects.map(cloneWorldOrbitObject),
2464
2772
  diagnostics
2465
2773
  };
@@ -2467,7 +2775,7 @@
2467
2775
  function upgradeDocumentToDraftV2(document, options = {}) {
2468
2776
  return convertAtlasDocumentToLegacyDraft(upgradeDocumentToV2(document, options));
2469
2777
  }
2470
- function materializeAtlasDocument(document) {
2778
+ function materializeAtlasDocument(document, options = {}) {
2471
2779
  const system = document.system ? {
2472
2780
  type: "system",
2473
2781
  id: document.system.id,
@@ -2478,6 +2786,8 @@
2478
2786
  properties: materializeDraftSystemProperties(document.system),
2479
2787
  info: materializeDraftSystemInfo(document.system)
2480
2788
  } : null;
2789
+ const objects = document.objects.map(cloneWorldOrbitObject);
2790
+ applyEventPoseOverrides(objects, document.events ?? [], options.activeEventId ?? null);
2481
2791
  return {
2482
2792
  format: "worldorbit",
2483
2793
  version: "1.0",
@@ -2485,7 +2795,8 @@
2485
2795
  system,
2486
2796
  groups: structuredClone(document.groups ?? []),
2487
2797
  relations: structuredClone(document.relations ?? []),
2488
- objects: document.objects.map(cloneWorldOrbitObject)
2798
+ events: document.events.map(cloneWorldOrbitEvent),
2799
+ objects
2489
2800
  };
2490
2801
  }
2491
2802
  function materializeDraftDocument(document) {
@@ -2510,8 +2821,9 @@
2510
2821
  };
2511
2822
  }
2512
2823
  function createDraftDefaults(document, preset, projection) {
2824
+ const rawView = typeof document.system?.properties.view === "string" ? document.system.properties.view.toLowerCase() : null;
2513
2825
  return {
2514
- view: typeof document.system?.properties.view === "string" && document.system.properties.view.toLowerCase() === "topdown" ? "topdown" : projection,
2826
+ view: rawView === "topdown" || rawView === "isometric" || rawView === "orthographic" || rawView === "perspective" ? rawView : projection,
2515
2827
  scale: typeof document.system?.properties.scale === "string" ? document.system.properties.scale : null,
2516
2828
  units: typeof document.system?.properties.units === "string" ? document.system.properties.units : null,
2517
2829
  preset,
@@ -2613,10 +2925,12 @@
2613
2925
  summary: viewpoint.summary,
2614
2926
  focusObjectId: viewpoint.objectId,
2615
2927
  selectedObjectId: viewpoint.selectedObjectId,
2928
+ events: [...viewpoint.eventIds],
2616
2929
  projection: viewpoint.projection,
2617
2930
  preset: viewpoint.preset,
2618
2931
  zoom: viewpoint.scale,
2619
2932
  rotationDeg: viewpoint.rotationDeg,
2933
+ camera: viewpoint.camera ? { ...viewpoint.camera } : null,
2620
2934
  layers: { ...viewpoint.layers },
2621
2935
  filter: viewpoint.filter ? {
2622
2936
  query: viewpoint.filter.query,
@@ -2645,6 +2959,75 @@
2645
2959
  info: { ...object.info }
2646
2960
  };
2647
2961
  }
2962
+ function cloneWorldOrbitEvent(event) {
2963
+ return {
2964
+ ...event,
2965
+ participantObjectIds: [...event.participantObjectIds],
2966
+ tags: [...event.tags],
2967
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2968
+ };
2969
+ }
2970
+ function cloneWorldOrbitEventPose(pose) {
2971
+ return {
2972
+ objectId: pose.objectId,
2973
+ placement: clonePlacement(pose.placement),
2974
+ inner: pose.inner ? { ...pose.inner } : void 0,
2975
+ outer: pose.outer ? { ...pose.outer } : void 0,
2976
+ epoch: pose.epoch ?? null,
2977
+ referencePlane: pose.referencePlane ?? null
2978
+ };
2979
+ }
2980
+ function clonePlacement(placement) {
2981
+ return placement ? structuredClone(placement) : null;
2982
+ }
2983
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2984
+ if (!activeEventId) {
2985
+ return;
2986
+ }
2987
+ const event = events.find((entry) => entry.id === activeEventId);
2988
+ if (!event) {
2989
+ return;
2990
+ }
2991
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2992
+ const referencedIds = /* @__PURE__ */ new Set([
2993
+ ...event.targetObjectId ? [event.targetObjectId] : [],
2994
+ ...event.participantObjectIds,
2995
+ ...event.positions.map((pose) => pose.objectId)
2996
+ ]);
2997
+ for (const objectId of referencedIds) {
2998
+ const object = objectMap.get(objectId);
2999
+ if (!object) {
3000
+ continue;
3001
+ }
3002
+ if (event.epoch) {
3003
+ object.epoch = event.epoch;
3004
+ }
3005
+ if (event.referencePlane) {
3006
+ object.referencePlane = event.referencePlane;
3007
+ }
3008
+ }
3009
+ for (const pose of event.positions) {
3010
+ const object = objectMap.get(pose.objectId);
3011
+ if (!object) {
3012
+ continue;
3013
+ }
3014
+ if (pose.placement) {
3015
+ object.placement = clonePlacement(pose.placement);
3016
+ }
3017
+ if (pose.inner) {
3018
+ object.properties.inner = { ...pose.inner };
3019
+ }
3020
+ if (pose.outer) {
3021
+ object.properties.outer = { ...pose.outer };
3022
+ }
3023
+ if (pose.epoch) {
3024
+ object.epoch = pose.epoch;
3025
+ }
3026
+ if (pose.referencePlane) {
3027
+ object.referencePlane = pose.referencePlane;
3028
+ }
3029
+ }
3030
+ }
2648
3031
  function cloneProperties(properties) {
2649
3032
  const next = {};
2650
3033
  for (const [key, value] of Object.entries(properties)) {
@@ -2726,6 +3109,18 @@
2726
3109
  if (viewpoint.rotationDeg !== 0) {
2727
3110
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2728
3111
  }
3112
+ if (viewpoint.camera?.azimuth !== null) {
3113
+ info2[`${prefix}.camera.azimuth`] = String(viewpoint.camera?.azimuth);
3114
+ }
3115
+ if (viewpoint.camera?.elevation !== null) {
3116
+ info2[`${prefix}.camera.elevation`] = String(viewpoint.camera?.elevation);
3117
+ }
3118
+ if (viewpoint.camera?.roll !== null) {
3119
+ info2[`${prefix}.camera.roll`] = String(viewpoint.camera?.roll);
3120
+ }
3121
+ if (viewpoint.camera?.distance !== null) {
3122
+ info2[`${prefix}.camera.distance`] = String(viewpoint.camera?.distance);
3123
+ }
2729
3124
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2730
3125
  if (serializedLayers) {
2731
3126
  info2[`${prefix}.layers`] = serializedLayers;
@@ -2742,6 +3137,9 @@
2742
3137
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2743
3138
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2744
3139
  }
3140
+ if (viewpoint.events.length > 0) {
3141
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
3142
+ }
2745
3143
  }
2746
3144
  for (const annotation of system.annotations) {
2747
3145
  const prefix = `annotation.${annotation.id}`;
@@ -2766,7 +3164,7 @@
2766
3164
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2767
3165
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2768
3166
  }
2769
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3167
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2770
3168
  if (layers[key] !== void 0) {
2771
3169
  tokens.push(layers[key] ? key : `-${key}`);
2772
3170
  }
@@ -2819,26 +3217,26 @@
2819
3217
  ];
2820
3218
  function formatDocument(document, options = {}) {
2821
3219
  const schema = options.schema ?? "auto";
2822
- const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.1" || document.version === "2.0-draft";
3220
+ const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.5" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.1" || document.version === "2.5" || document.version === "2.0-draft";
2823
3221
  if (useDraft) {
2824
3222
  if (schema === "2.0-draft") {
2825
- const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" ? {
3223
+ const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" || document.version === "2.5" ? {
2826
3224
  ...document,
2827
3225
  version: "2.0-draft",
2828
3226
  schemaVersion: "2.0-draft"
2829
3227
  } : upgradeDocumentToDraftV2(document);
2830
3228
  return formatDraftDocument(legacyDraftDocument);
2831
3229
  }
2832
- const atlasDocument = document.version === "2.0" || document.version === "2.1" ? document : document.version === "2.0-draft" ? {
3230
+ const atlasDocument = document.version === "2.0" || document.version === "2.1" || document.version === "2.5" ? document : document.version === "2.0-draft" ? {
2833
3231
  ...document,
2834
3232
  version: "2.0",
2835
3233
  schemaVersion: "2.0"
2836
3234
  } : upgradeDocumentToV2(document);
2837
- if (schema === "2.1" && atlasDocument.version !== "2.1") {
3235
+ if ((schema === "2.0" || schema === "2.1" || schema === "2.5") && atlasDocument.version !== schema) {
2838
3236
  return formatAtlasDocument({
2839
3237
  ...atlasDocument,
2840
- version: "2.1",
2841
- schemaVersion: "2.1"
3238
+ version: schema,
3239
+ schemaVersion: schema
2842
3240
  });
2843
3241
  }
2844
3242
  return formatAtlasDocument(atlasDocument);
@@ -2870,6 +3268,10 @@
2870
3268
  lines.push("");
2871
3269
  lines.push(...formatAtlasRelation(relation));
2872
3270
  }
3271
+ for (const event of [...document.events].sort(compareIdLike)) {
3272
+ lines.push("");
3273
+ lines.push(...formatAtlasEvent(event));
3274
+ }
2873
3275
  const sortedObjects = [...document.objects].sort(compareObjects);
2874
3276
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2875
3277
  lines.push("");
@@ -2900,6 +3302,10 @@
2900
3302
  lines.push("");
2901
3303
  lines.push(...formatAtlasRelation(relation));
2902
3304
  }
3305
+ for (const event of [...legacy.events].sort(compareIdLike)) {
3306
+ lines.push("");
3307
+ lines.push(...formatAtlasEvent(event));
3308
+ }
2903
3309
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2904
3310
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2905
3311
  lines.push("");
@@ -3107,10 +3513,28 @@
3107
3513
  if (viewpoint.rotationDeg !== 0) {
3108
3514
  lines.push(` rotation ${viewpoint.rotationDeg}`);
3109
3515
  }
3516
+ if (viewpoint.camera && hasCameraValues(viewpoint.camera)) {
3517
+ lines.push(" camera");
3518
+ if (viewpoint.camera.azimuth !== null) {
3519
+ lines.push(` azimuth ${viewpoint.camera.azimuth}`);
3520
+ }
3521
+ if (viewpoint.camera.elevation !== null) {
3522
+ lines.push(` elevation ${viewpoint.camera.elevation}`);
3523
+ }
3524
+ if (viewpoint.camera.roll !== null) {
3525
+ lines.push(` roll ${viewpoint.camera.roll}`);
3526
+ }
3527
+ if (viewpoint.camera.distance !== null) {
3528
+ lines.push(` distance ${viewpoint.camera.distance}`);
3529
+ }
3530
+ }
3110
3531
  const layerTokens = formatDraftLayers(viewpoint.layers);
3111
3532
  if (layerTokens.length > 0) {
3112
3533
  lines.push(` layers ${layerTokens.join(" ")}`);
3113
3534
  }
3535
+ if (viewpoint.events.length > 0) {
3536
+ lines.push(` events ${viewpoint.events.join(" ")}`);
3537
+ }
3114
3538
  if (viewpoint.filter) {
3115
3539
  lines.push(" filter");
3116
3540
  if (viewpoint.filter.query) {
@@ -3183,6 +3607,65 @@
3183
3607
  }
3184
3608
  return lines;
3185
3609
  }
3610
+ function formatAtlasEvent(event) {
3611
+ const lines = [`event ${event.id}`, ` kind ${quoteIfNeeded(event.kind)}`];
3612
+ if (event.label) {
3613
+ lines.push(` label ${quoteIfNeeded(event.label)}`);
3614
+ }
3615
+ if (event.summary) {
3616
+ lines.push(` summary ${quoteIfNeeded(event.summary)}`);
3617
+ }
3618
+ if (event.targetObjectId) {
3619
+ lines.push(` target ${event.targetObjectId}`);
3620
+ }
3621
+ if (event.participantObjectIds.length > 0) {
3622
+ lines.push(` participants ${event.participantObjectIds.join(" ")}`);
3623
+ }
3624
+ if (event.timing) {
3625
+ lines.push(` timing ${quoteIfNeeded(event.timing)}`);
3626
+ }
3627
+ if (event.visibility) {
3628
+ lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
3629
+ }
3630
+ if (event.epoch) {
3631
+ lines.push(` epoch ${quoteIfNeeded(event.epoch)}`);
3632
+ }
3633
+ if (event.referencePlane) {
3634
+ lines.push(` referencePlane ${quoteIfNeeded(event.referencePlane)}`);
3635
+ }
3636
+ if (event.tags.length > 0) {
3637
+ lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3638
+ }
3639
+ if (event.color) {
3640
+ lines.push(` color ${quoteIfNeeded(event.color)}`);
3641
+ }
3642
+ if (event.hidden) {
3643
+ lines.push(" hidden true");
3644
+ }
3645
+ if (event.positions.length > 0) {
3646
+ lines.push("");
3647
+ lines.push(" positions");
3648
+ for (const pose of [...event.positions].sort(comparePoseObjectId)) {
3649
+ lines.push(` pose ${pose.objectId}`);
3650
+ for (const fieldLine of formatEventPoseFields(pose)) {
3651
+ lines.push(` ${fieldLine}`);
3652
+ }
3653
+ }
3654
+ }
3655
+ return lines;
3656
+ }
3657
+ function formatEventPoseFields(pose) {
3658
+ return [
3659
+ ...formatPlacement(pose.placement),
3660
+ ...pose.epoch ? [`epoch ${quoteIfNeeded(pose.epoch)}`] : [],
3661
+ ...pose.referencePlane ? [`referencePlane ${quoteIfNeeded(pose.referencePlane)}`] : [],
3662
+ ...formatOptionalUnit("inner", pose.inner),
3663
+ ...formatOptionalUnit("outer", pose.outer)
3664
+ ];
3665
+ }
3666
+ function hasCameraValues(camera) {
3667
+ return camera.azimuth !== null || camera.elevation !== null || camera.roll !== null || camera.distance !== null;
3668
+ }
3186
3669
  function formatValue(value) {
3187
3670
  if (Array.isArray(value)) {
3188
3671
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3224,7 +3707,7 @@
3224
3707
  if (orbitFront !== void 0 || orbitBack !== void 0) {
3225
3708
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
3226
3709
  }
3227
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3710
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
3228
3711
  if (layers[key] !== void 0) {
3229
3712
  tokens.push(layers[key] ? key : `-${key}`);
3230
3713
  }
@@ -3252,6 +3735,9 @@
3252
3735
  function compareIdLike(left, right) {
3253
3736
  return left.id.localeCompare(right.id);
3254
3737
  }
3738
+ function comparePoseObjectId(left, right) {
3739
+ return left.objectId.localeCompare(right.objectId);
3740
+ }
3255
3741
  function objectTypeIndex(objectType) {
3256
3742
  switch (objectType) {
3257
3743
  case "star":
@@ -3447,6 +3933,7 @@
3447
3933
  const diagnostics = [];
3448
3934
  const objectMap = new Map(document.objects.map((object) => [object.id, object]));
3449
3935
  const groupIds = new Set(document.groups.map((group) => group.id));
3936
+ const eventIds = new Set(document.events.map((event) => event.id));
3450
3937
  if (!document.system) {
3451
3938
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3452
3939
  }
@@ -3456,6 +3943,7 @@
3456
3943
  ["viewpoint", document.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3457
3944
  ["annotation", document.system?.annotations.map((annotation) => annotation.id) ?? []],
3458
3945
  ["relation", document.relations.map((relation) => relation.id)],
3946
+ ["event", document.events.map((event) => event.id)],
3459
3947
  ["object", document.objects.map((object) => object.id)]
3460
3948
  ]) {
3461
3949
  for (const id of ids) {
@@ -3471,11 +3959,14 @@
3471
3959
  validateRelation(relation, objectMap, diagnostics);
3472
3960
  }
3473
3961
  for (const viewpoint of document.system?.viewpoints ?? []) {
3474
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3962
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
3475
3963
  }
3476
3964
  for (const object of document.objects) {
3477
3965
  validateObject(object, document.system, objectMap, groupIds, diagnostics);
3478
3966
  }
3967
+ for (const event of document.events) {
3968
+ validateEvent(event, document.system, objectMap, diagnostics);
3969
+ }
3479
3970
  return diagnostics;
3480
3971
  }
3481
3972
  function validateRelation(relation, objectMap, diagnostics) {
@@ -3493,15 +3984,24 @@
3493
3984
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3494
3985
  }
3495
3986
  }
3496
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3497
- if (!filter || sourceSchemaVersion !== "2.1") {
3498
- return;
3499
- }
3500
- for (const groupId of filter.groupIds) {
3501
- if (!groupIds.has(groupId)) {
3502
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3987
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
3988
+ const filter = viewpoint.filter;
3989
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
3990
+ if (filter) {
3991
+ for (const groupId of filter.groupIds) {
3992
+ if (!groupIds.has(groupId)) {
3993
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
3994
+ }
3995
+ }
3996
+ }
3997
+ for (const eventId of viewpoint.events ?? []) {
3998
+ if (!eventIds.has(eventId)) {
3999
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
4000
+ }
3503
4001
  }
3504
4002
  }
4003
+ validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
4004
+ validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
3505
4005
  }
3506
4006
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
3507
4007
  const placement = object.placement;
@@ -3514,6 +4014,12 @@
3514
4014
  }
3515
4015
  }
3516
4016
  }
4017
+ if (typeof object.epoch === "string" && !object.epoch.trim()) {
4018
+ diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
4019
+ }
4020
+ if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
4021
+ diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
4022
+ }
3517
4023
  if (orbitPlacement) {
3518
4024
  if (!objectMap.has(orbitPlacement.target)) {
3519
4025
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -3585,6 +4091,122 @@
3585
4091
  }
3586
4092
  }
3587
4093
  }
4094
+ function validateEvent(event, system, objectMap, diagnostics) {
4095
+ const fieldPrefix = `event.${event.id}`;
4096
+ const referencedIds = /* @__PURE__ */ new Set();
4097
+ if (!event.kind.trim()) {
4098
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
4099
+ }
4100
+ if (typeof event.epoch === "string" && !event.epoch.trim()) {
4101
+ diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, void 0, `${fieldPrefix}.epoch`));
4102
+ }
4103
+ if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
4104
+ diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, void 0, `${fieldPrefix}.referencePlane`));
4105
+ }
4106
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
4107
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
4108
+ }
4109
+ if (event.targetObjectId) {
4110
+ referencedIds.add(event.targetObjectId);
4111
+ if (!objectMap.has(event.targetObjectId)) {
4112
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
4113
+ }
4114
+ }
4115
+ const seenParticipants = /* @__PURE__ */ new Set();
4116
+ for (const participantId of event.participantObjectIds) {
4117
+ referencedIds.add(participantId);
4118
+ if (seenParticipants.has(participantId)) {
4119
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
4120
+ continue;
4121
+ }
4122
+ seenParticipants.add(participantId);
4123
+ if (!objectMap.has(participantId)) {
4124
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
4125
+ }
4126
+ }
4127
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
4128
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
4129
+ }
4130
+ if (event.positions.length === 0) {
4131
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
4132
+ }
4133
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
4134
+ 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`));
4135
+ }
4136
+ const poseIds = /* @__PURE__ */ new Set();
4137
+ for (const pose of event.positions) {
4138
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
4139
+ if (poseIds.has(pose.objectId)) {
4140
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
4141
+ continue;
4142
+ }
4143
+ poseIds.add(pose.objectId);
4144
+ const object = objectMap.get(pose.objectId);
4145
+ if (!object) {
4146
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
4147
+ continue;
4148
+ }
4149
+ if (!referencedIds.has(pose.objectId)) {
4150
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
4151
+ }
4152
+ validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
4153
+ }
4154
+ const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
4155
+ if (event.positions.length > 0 && missingPoseIds.length > 0) {
4156
+ diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, void 0, `${fieldPrefix}.positions`));
4157
+ }
4158
+ }
4159
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
4160
+ const placement = pose.placement;
4161
+ if (!placement) {
4162
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
4163
+ return;
4164
+ }
4165
+ if (placement.mode === "orbit") {
4166
+ if (!objectMap.has(placement.target)) {
4167
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
4168
+ }
4169
+ if (placement.distance && placement.semiMajor) {
4170
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
4171
+ }
4172
+ if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
4173
+ diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, void 0, `${fieldPrefix}.phase`));
4174
+ }
4175
+ if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
4176
+ diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, void 0, `${fieldPrefix}.inclination`));
4177
+ }
4178
+ if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
4179
+ diagnostics.push(warn("validate.event.pose.period.massMissing", `Event "${eventId}" pose "${pose.objectId}" sets "period" but its central mass cannot be derived.`, void 0, `${fieldPrefix}.period`));
4180
+ }
4181
+ return;
4182
+ }
4183
+ if (placement.mode === "surface") {
4184
+ const target = objectMap.get(placement.target);
4185
+ if (!target) {
4186
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
4187
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
4188
+ 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`));
4189
+ }
4190
+ return;
4191
+ }
4192
+ if (placement.mode === "at") {
4193
+ if (object.type !== "structure" && object.type !== "phenomenon") {
4194
+ 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`));
4195
+ }
4196
+ const reference = placement.reference;
4197
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
4198
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4199
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
4200
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4201
+ } else if (reference.kind === "lagrange") {
4202
+ if (!objectMap.has(reference.primary)) {
4203
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4204
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
4205
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4206
+ }
4207
+ }
4208
+ }
4209
+ }
3588
4210
  function validateAtTarget(object, objectMap, diagnostics) {
3589
4211
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3590
4212
  if (!reference) {
@@ -3690,6 +4312,52 @@
3690
4312
  return null;
3691
4313
  }
3692
4314
  }
4315
+ function validateProjection(projection, diagnostics, field, viewpointId) {
4316
+ if (projection !== "topdown" && projection !== "isometric" && projection !== "orthographic" && projection !== "perspective") {
4317
+ diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, void 0, field));
4318
+ }
4319
+ }
4320
+ function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
4321
+ if (!camera) {
4322
+ return;
4323
+ }
4324
+ const prefix = `viewpoint.${viewpointId}.camera`;
4325
+ for (const [key, value] of [
4326
+ ["azimuth", camera.azimuth],
4327
+ ["elevation", camera.elevation],
4328
+ ["roll", camera.roll],
4329
+ ["distance", camera.distance]
4330
+ ]) {
4331
+ if (value !== null && (!Number.isFinite(value) || key === "distance" && value <= 0)) {
4332
+ diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, void 0, `${prefix}.${key}`));
4333
+ }
4334
+ }
4335
+ if (camera.distance !== null && projection !== "perspective") {
4336
+ diagnostics.push(warn("validate.viewpoint.camera.distance.partialEffect", `Camera "distance" only has a semantic effect in perspective viewpoints; "${viewpointId}" uses "${projection}".`, void 0, `${prefix}.distance`));
4337
+ }
4338
+ if (projection === "topdown" && (camera.elevation !== null || camera.roll !== null)) {
4339
+ diagnostics.push(warn("validate.viewpoint.camera.topdownPartial", `Camera elevation/roll on topdown viewpoint "${viewpointId}" are currently stored for future 3D use and only partially affect 2D rendering.`, void 0, prefix));
4340
+ }
4341
+ if (projection === "isometric" && camera.elevation !== null) {
4342
+ diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, void 0, `${prefix}.elevation`));
4343
+ }
4344
+ if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
4345
+ diagnostics.push(warn("validate.viewpoint.rotation.cameraOverlap", `Viewpoint "${viewpointId}" uses camera.azimuth; keep "rotation" only for 2D screen rotation to avoid ambiguity.`, void 0, `${prefix}.azimuth`));
4346
+ }
4347
+ const hasAnchor = focusObjectId !== null && objectMap.has(focusObjectId) || selectedObjectId !== null && objectMap.has(selectedObjectId) || !!filter;
4348
+ if (!hasAnchor) {
4349
+ diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, void 0, prefix));
4350
+ }
4351
+ }
4352
+ function resolveEffectiveEpoch(system, object, event, pose) {
4353
+ return normalizeOptionalContextString(pose?.epoch) ?? normalizeOptionalContextString(event?.epoch) ?? normalizeOptionalContextString(object.epoch) ?? normalizeOptionalContextString(system?.epoch) ?? null;
4354
+ }
4355
+ function resolveEffectiveReferencePlane(system, object, event, pose) {
4356
+ return normalizeOptionalContextString(pose?.referencePlane) ?? normalizeOptionalContextString(event?.referencePlane) ?? normalizeOptionalContextString(object.referencePlane) ?? normalizeOptionalContextString(system?.referencePlane) ?? null;
4357
+ }
4358
+ function normalizeOptionalContextString(value) {
4359
+ return typeof value === "string" && value.trim() ? value.trim() : null;
4360
+ }
3693
4361
  function toleranceForField(object, field) {
3694
4362
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3695
4363
  if (typeof tolerance === "number") {
@@ -3785,6 +4453,23 @@
3785
4453
  });
3786
4454
  }
3787
4455
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
4456
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
4457
+ "orbit",
4458
+ "distance",
4459
+ "semiMajor",
4460
+ "eccentricity",
4461
+ "period",
4462
+ "angle",
4463
+ "inclination",
4464
+ "phase",
4465
+ "at",
4466
+ "surface",
4467
+ "free",
4468
+ "inner",
4469
+ "outer",
4470
+ "epoch",
4471
+ "referencePlane"
4472
+ ]);
3788
4473
  function parseWorldOrbitAtlas(source) {
3789
4474
  return parseAtlasSource(source);
3790
4475
  }
@@ -3802,12 +4487,15 @@
3802
4487
  const objectNodes = [];
3803
4488
  const groups = [];
3804
4489
  const relations = [];
4490
+ const events = [];
4491
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3805
4492
  let sawDefaults = false;
3806
4493
  let sawAtlas = false;
3807
4494
  const viewpointIds = /* @__PURE__ */ new Set();
3808
4495
  const annotationIds = /* @__PURE__ */ new Set();
3809
4496
  const groupIds = /* @__PURE__ */ new Set();
3810
4497
  const relationIds = /* @__PURE__ */ new Set();
4498
+ const eventIds = /* @__PURE__ */ new Set();
3811
4499
  for (let index = 0; index < lines.length; index++) {
3812
4500
  const rawLine = lines[index];
3813
4501
  const lineNumber = index + 1;
@@ -3825,7 +4513,7 @@
3825
4513
  if (!sawSchemaHeader) {
3826
4514
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3827
4515
  sawSchemaHeader = true;
3828
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
4516
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3829
4517
  diagnostics.push({
3830
4518
  code: "parse.schema21.commentCompatibility",
3831
4519
  severity: "warning",
@@ -3838,7 +4526,7 @@
3838
4526
  continue;
3839
4527
  }
3840
4528
  if (indent === 0) {
3841
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
4529
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3842
4530
  if (section.kind === "system") {
3843
4531
  system = section.system;
3844
4532
  } else if (section.kind === "defaults") {
@@ -3857,6 +4545,7 @@
3857
4545
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3858
4546
  }
3859
4547
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4548
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3860
4549
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3861
4550
  const baseDocument = {
3862
4551
  format: "worldorbit",
@@ -3864,6 +4553,7 @@
3864
4553
  system,
3865
4554
  groups,
3866
4555
  relations,
4556
+ events: normalizedEvents,
3867
4557
  objects,
3868
4558
  diagnostics
3869
4559
  };
@@ -3893,13 +4583,13 @@
3893
4583
  return document;
3894
4584
  }
3895
4585
  function assertDraftSchemaHeader(tokens, line) {
3896
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3897
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4586
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
4587
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", "schema 2.5", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3898
4588
  }
3899
4589
  const version = tokens[1].value.toLowerCase();
3900
- return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
4590
+ return version === "2.5" ? "2.5" : version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3901
4591
  }
3902
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
4592
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3903
4593
  const keyword = tokens[0]?.value.toLowerCase();
3904
4594
  switch (keyword) {
3905
4595
  case "system":
@@ -3917,6 +4607,8 @@
3917
4607
  return {
3918
4608
  kind: "defaults",
3919
4609
  system,
4610
+ sourceSchemaVersion,
4611
+ diagnostics,
3920
4612
  seenFields: /* @__PURE__ */ new Set()
3921
4613
  };
3922
4614
  case "atlas":
@@ -3936,7 +4628,7 @@
3936
4628
  if (!system) {
3937
4629
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3938
4630
  }
3939
- return startViewpointSection(tokens, line, system, viewpointIds);
4631
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3940
4632
  case "annotation":
3941
4633
  if (!system) {
3942
4634
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3948,6 +4640,9 @@
3948
4640
  case "relation":
3949
4641
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3950
4642
  return startRelationSection(tokens, line, relations, relationIds);
4643
+ case "event":
4644
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4645
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3951
4646
  case "object":
3952
4647
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3953
4648
  default:
@@ -3984,7 +4679,7 @@
3984
4679
  seenFields: /* @__PURE__ */ new Set()
3985
4680
  };
3986
4681
  }
3987
- function startViewpointSection(tokens, line, system, viewpointIds) {
4682
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3988
4683
  if (tokens.length !== 2) {
3989
4684
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3990
4685
  }
@@ -4001,10 +4696,12 @@
4001
4696
  summary: "",
4002
4697
  focusObjectId: null,
4003
4698
  selectedObjectId: null,
4699
+ events: [],
4004
4700
  projection: system.defaults.view,
4005
4701
  preset: system.defaults.preset,
4006
4702
  zoom: null,
4007
4703
  rotationDeg: 0,
4704
+ camera: null,
4008
4705
  layers: {},
4009
4706
  filter: null
4010
4707
  };
@@ -4013,10 +4710,15 @@
4013
4710
  return {
4014
4711
  kind: "viewpoint",
4015
4712
  viewpoint,
4713
+ sourceSchemaVersion,
4714
+ diagnostics,
4016
4715
  seenFields: /* @__PURE__ */ new Set(),
4017
4716
  inFilter: false,
4018
4717
  filterIndent: null,
4019
- seenFilterFields: /* @__PURE__ */ new Set()
4718
+ seenFilterFields: /* @__PURE__ */ new Set(),
4719
+ inCamera: false,
4720
+ cameraIndent: null,
4721
+ seenCameraFields: /* @__PURE__ */ new Set()
4020
4722
  };
4021
4723
  }
4022
4724
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -4103,6 +4805,51 @@
4103
4805
  seenFields: /* @__PURE__ */ new Set()
4104
4806
  };
4105
4807
  }
4808
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4809
+ if (tokens.length !== 2) {
4810
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4811
+ }
4812
+ const id = normalizeIdentifier2(tokens[1].value);
4813
+ if (!id) {
4814
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4815
+ }
4816
+ if (eventIds.has(id)) {
4817
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4818
+ }
4819
+ const event = {
4820
+ id,
4821
+ kind: "",
4822
+ label: humanizeIdentifier3(id),
4823
+ summary: null,
4824
+ targetObjectId: null,
4825
+ participantObjectIds: [],
4826
+ timing: null,
4827
+ visibility: null,
4828
+ epoch: null,
4829
+ referencePlane: null,
4830
+ tags: [],
4831
+ color: null,
4832
+ hidden: false,
4833
+ positions: []
4834
+ };
4835
+ const rawPoses = [];
4836
+ events.push(event);
4837
+ eventPoseNodes.set(id, rawPoses);
4838
+ eventIds.add(id);
4839
+ return {
4840
+ kind: "event",
4841
+ event,
4842
+ sourceSchemaVersion,
4843
+ diagnostics,
4844
+ seenFields: /* @__PURE__ */ new Set(),
4845
+ rawPoses,
4846
+ inPositions: false,
4847
+ positionsIndent: null,
4848
+ activePose: null,
4849
+ poseIndent: null,
4850
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4851
+ };
4852
+ }
4106
4853
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
4107
4854
  if (tokens.length < 3) {
4108
4855
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -4159,6 +4906,9 @@
4159
4906
  case "relation":
4160
4907
  applyRelationField(section, tokens, line);
4161
4908
  return;
4909
+ case "event":
4910
+ applyEventField(section, indent, tokens, line);
4911
+ return;
4162
4912
  case "object":
4163
4913
  applyObjectField(section, indent, tokens, line);
4164
4914
  return;
@@ -4201,6 +4951,12 @@
4201
4951
  const value = joinFieldValue(tokens, line);
4202
4952
  switch (key) {
4203
4953
  case "view":
4954
+ if (isSchema25Projection(value)) {
4955
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
4956
+ line,
4957
+ column: tokens[0].column
4958
+ });
4959
+ }
4204
4960
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
4205
4961
  return;
4206
4962
  case "scale":
@@ -4240,14 +4996,36 @@
4240
4996
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
4241
4997
  }
4242
4998
  function applyViewpointField2(section, indent, tokens, line) {
4999
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
5000
+ section.inCamera = false;
5001
+ section.cameraIndent = null;
5002
+ }
4243
5003
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
4244
5004
  section.inFilter = false;
4245
5005
  section.filterIndent = null;
4246
5006
  }
5007
+ if (section.inCamera) {
5008
+ applyViewpointCameraField(section, tokens, line);
5009
+ return;
5010
+ }
4247
5011
  if (section.inFilter) {
4248
5012
  applyViewpointFilterField(section, tokens, line);
4249
5013
  return;
4250
5014
  }
5015
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
5016
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
5017
+ line,
5018
+ column: tokens[0].column
5019
+ });
5020
+ if (section.seenFields.has("camera")) {
5021
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
5022
+ }
5023
+ section.seenFields.add("camera");
5024
+ section.inCamera = true;
5025
+ section.cameraIndent = indent;
5026
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera2();
5027
+ return;
5028
+ }
4251
5029
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
4252
5030
  if (section.seenFields.has("filter")) {
4253
5031
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -4273,6 +5051,12 @@
4273
5051
  section.viewpoint.selectedObjectId = value;
4274
5052
  return;
4275
5053
  case "projection":
5054
+ if (isSchema25Projection(value)) {
5055
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
5056
+ line,
5057
+ column: tokens[0].column
5058
+ });
5059
+ }
4276
5060
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
4277
5061
  return;
4278
5062
  case "preset":
@@ -4284,13 +5068,49 @@
4284
5068
  case "rotation":
4285
5069
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
4286
5070
  return;
5071
+ case "camera":
5072
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
5073
+ line,
5074
+ column: tokens[0].column
5075
+ });
5076
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
5077
+ return;
4287
5078
  case "layers":
4288
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
5079
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
5080
+ return;
5081
+ case "events":
5082
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
5083
+ line,
5084
+ column: tokens[0].column
5085
+ });
5086
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
4289
5087
  return;
4290
5088
  default:
4291
5089
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
4292
5090
  }
4293
5091
  }
5092
+ function applyViewpointCameraField(section, tokens, line) {
5093
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
5094
+ const value = joinFieldValue(tokens, line);
5095
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera2();
5096
+ switch (key) {
5097
+ case "azimuth":
5098
+ camera.azimuth = parseFiniteNumber2(value, line, tokens[0].column, "camera.azimuth");
5099
+ break;
5100
+ case "elevation":
5101
+ camera.elevation = parseFiniteNumber2(value, line, tokens[0].column, "camera.elevation");
5102
+ break;
5103
+ case "roll":
5104
+ camera.roll = parseFiniteNumber2(value, line, tokens[0].column, "camera.roll");
5105
+ break;
5106
+ case "distance":
5107
+ camera.distance = parsePositiveNumber2(value, line, tokens[0].column, "camera.distance");
5108
+ break;
5109
+ default:
5110
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
5111
+ }
5112
+ section.viewpoint.camera = camera;
5113
+ }
4294
5114
  function applyViewpointFilterField(section, tokens, line) {
4295
5115
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
4296
5116
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -4390,6 +5210,126 @@
4390
5210
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4391
5211
  }
4392
5212
  }
5213
+ function applyEventField(section, indent, tokens, line) {
5214
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
5215
+ section.activePose = null;
5216
+ section.poseIndent = null;
5217
+ section.activePoseSeenFields.clear();
5218
+ }
5219
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
5220
+ section.inPositions = false;
5221
+ section.positionsIndent = null;
5222
+ }
5223
+ if (section.activePose) {
5224
+ if (tokens[0]?.value === "epoch" || tokens[0]?.value === "referencePlane") {
5225
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
5226
+ line,
5227
+ column: tokens[0]?.column ?? 1
5228
+ });
5229
+ }
5230
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
5231
+ return;
5232
+ }
5233
+ if (section.inPositions) {
5234
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
5235
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
5236
+ }
5237
+ const objectId = tokens[1].value;
5238
+ if (!objectId.trim()) {
5239
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
5240
+ }
5241
+ const rawPose = {
5242
+ objectId,
5243
+ fields: [],
5244
+ location: { line, column: tokens[0].column }
5245
+ };
5246
+ section.rawPoses.push(rawPose);
5247
+ section.activePose = rawPose;
5248
+ section.poseIndent = indent;
5249
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
5250
+ return;
5251
+ }
5252
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
5253
+ if (section.seenFields.has("positions")) {
5254
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
5255
+ }
5256
+ section.seenFields.add("positions");
5257
+ section.inPositions = true;
5258
+ section.positionsIndent = indent;
5259
+ return;
5260
+ }
5261
+ const key = requireUniqueField(tokens, section.seenFields, line);
5262
+ switch (key) {
5263
+ case "kind":
5264
+ section.event.kind = joinFieldValue(tokens, line);
5265
+ return;
5266
+ case "label":
5267
+ section.event.label = joinFieldValue(tokens, line);
5268
+ return;
5269
+ case "summary":
5270
+ section.event.summary = joinFieldValue(tokens, line);
5271
+ return;
5272
+ case "target":
5273
+ section.event.targetObjectId = joinFieldValue(tokens, line);
5274
+ return;
5275
+ case "participants":
5276
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
5277
+ return;
5278
+ case "timing":
5279
+ section.event.timing = joinFieldValue(tokens, line);
5280
+ return;
5281
+ case "visibility":
5282
+ section.event.visibility = joinFieldValue(tokens, line);
5283
+ return;
5284
+ case "epoch":
5285
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
5286
+ line,
5287
+ column: tokens[0].column
5288
+ });
5289
+ section.event.epoch = joinFieldValue(tokens, line);
5290
+ return;
5291
+ case "referenceplane":
5292
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
5293
+ line,
5294
+ column: tokens[0].column
5295
+ });
5296
+ section.event.referencePlane = joinFieldValue(tokens, line);
5297
+ return;
5298
+ case "tags":
5299
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
5300
+ return;
5301
+ case "color":
5302
+ section.event.color = joinFieldValue(tokens, line);
5303
+ return;
5304
+ case "hidden":
5305
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
5306
+ line,
5307
+ column: tokens[0].column
5308
+ });
5309
+ return;
5310
+ default:
5311
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
5312
+ }
5313
+ }
5314
+ function parseEventPoseField(tokens, line, seenFields) {
5315
+ if (tokens.length < 2) {
5316
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
5317
+ }
5318
+ const key = tokens[0].value;
5319
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
5320
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
5321
+ }
5322
+ if (seenFields.has(key)) {
5323
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
5324
+ }
5325
+ seenFields.add(key);
5326
+ return {
5327
+ type: "field",
5328
+ key,
5329
+ values: tokens.slice(1).map((token) => token.value),
5330
+ location: { line, column: tokens[0].column }
5331
+ };
5332
+ }
4393
5333
  function applyObjectField(section, indent, tokens, line) {
4394
5334
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4395
5335
  section.activeBlock = null;
@@ -4448,7 +5388,7 @@
4448
5388
  function parseObjectTypeTokens(tokens, line) {
4449
5389
  return parseTokenList(tokens, line, "objectTypes").filter((value) => value === "star" || value === "planet" || value === "moon" || value === "belt" || value === "asteroid" || value === "comet" || value === "ring" || value === "structure" || value === "phenomenon");
4450
5390
  }
4451
- function parseLayerTokens(tokens, line) {
5391
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4452
5392
  const layers = {};
4453
5393
  for (const token of parseTokenList(tokens, line, "layers")) {
4454
5394
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -4458,7 +5398,13 @@
4458
5398
  layers["orbits-front"] = enabled;
4459
5399
  continue;
4460
5400
  }
4461
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
5401
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
5402
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
5403
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
5404
+ line,
5405
+ column: tokens[0]?.column ?? 1
5406
+ });
5407
+ }
4462
5408
  layers[raw] = enabled;
4463
5409
  }
4464
5410
  }
@@ -4476,11 +5422,15 @@
4476
5422
  }
4477
5423
  function parseProjectionValue(value, line, column) {
4478
5424
  const normalized = value.toLowerCase();
4479
- if (normalized !== "topdown" && normalized !== "isometric") {
5425
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
4480
5426
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4481
5427
  }
4482
5428
  return normalized;
4483
5429
  }
5430
+ function isSchema25Projection(value) {
5431
+ const normalized = value.toLowerCase();
5432
+ return normalized === "orthographic" || normalized === "perspective";
5433
+ }
4484
5434
  function parsePresetValue(value, line, column) {
4485
5435
  const normalized = value.toLowerCase();
4486
5436
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -4510,6 +5460,48 @@
4510
5460
  groupIds: []
4511
5461
  };
4512
5462
  }
5463
+ function createEmptyViewCamera2() {
5464
+ return {
5465
+ azimuth: null,
5466
+ elevation: null,
5467
+ roll: null,
5468
+ distance: null
5469
+ };
5470
+ }
5471
+ function parseInlineViewCamera(tokens, line, current) {
5472
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
5473
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
5474
+ }
5475
+ const camera = current ? { ...current } : createEmptyViewCamera2();
5476
+ const seen = /* @__PURE__ */ new Set();
5477
+ for (let index = 0; index < tokens.length; index += 2) {
5478
+ const fieldToken = tokens[index];
5479
+ const valueToken = tokens[index + 1];
5480
+ const key = fieldToken.value.toLowerCase();
5481
+ if (seen.has(key)) {
5482
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5483
+ }
5484
+ seen.add(key);
5485
+ const value = valueToken.value;
5486
+ switch (key) {
5487
+ case "azimuth":
5488
+ camera.azimuth = parseFiniteNumber2(value, line, fieldToken.column, "camera.azimuth");
5489
+ break;
5490
+ case "elevation":
5491
+ camera.elevation = parseFiniteNumber2(value, line, fieldToken.column, "camera.elevation");
5492
+ break;
5493
+ case "roll":
5494
+ camera.roll = parseFiniteNumber2(value, line, fieldToken.column, "camera.roll");
5495
+ break;
5496
+ case "distance":
5497
+ camera.distance = parsePositiveNumber2(value, line, fieldToken.column, "camera.distance");
5498
+ break;
5499
+ default:
5500
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5501
+ }
5502
+ }
5503
+ return camera;
5504
+ }
4513
5505
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
4514
5506
  const fields = [];
4515
5507
  let index = 0;
@@ -4597,7 +5589,7 @@
4597
5589
  }
4598
5590
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4599
5591
  const fieldMap = collectDraftFields(node.fields);
4600
- const placement = extractDraftPlacement(node.objectType, fieldMap);
5592
+ const placement = extractPlacementFromFieldMap(fieldMap);
4601
5593
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
4602
5594
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4603
5595
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -4642,21 +5634,41 @@
4642
5634
  object.tolerances = tolerances;
4643
5635
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
4644
5636
  object.typedBlocks = typedBlocks;
4645
- if (sourceSchemaVersion !== "2.1") {
5637
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4646
5638
  if (object.groups || object.epoch || object.referencePlane || object.tidalLock !== void 0 || object.resonance || object.renderHints || object.deriveRules?.length || object.validationRules?.length || object.lockedFields?.length || object.tolerances?.length || object.typedBlocks) {
4647
5639
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4648
5640
  }
4649
5641
  }
4650
5642
  return object;
4651
5643
  }
4652
- function collectDraftFields(fields) {
5644
+ function normalizeDraftEvent(event, rawPoses) {
5645
+ return {
5646
+ ...event,
5647
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5648
+ tags: [...new Set(event.tags)],
5649
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5650
+ };
5651
+ }
5652
+ function normalizeDraftEventPose(rawPose) {
5653
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
5654
+ const placement = extractPlacementFromFieldMap(fieldMap);
5655
+ return {
5656
+ objectId: rawPose.objectId,
5657
+ placement,
5658
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5659
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
5660
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
5661
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
5662
+ };
5663
+ }
5664
+ function collectDraftFields(fields, _mode = "object") {
4653
5665
  const grouped = /* @__PURE__ */ new Map();
4654
5666
  for (const field of fields) {
4655
5667
  const spec = getDraftObjectFieldSpec(field.key);
4656
- if (!spec) {
5668
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
4657
5669
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4658
5670
  }
4659
- if (!spec.allowRepeat && grouped.has(field.key)) {
5671
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
4660
5672
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4661
5673
  }
4662
5674
  const existing = grouped.get(field.key) ?? [];
@@ -4665,7 +5677,7 @@
4665
5677
  }
4666
5678
  return grouped;
4667
5679
  }
4668
- function extractDraftPlacement(objectType, fieldMap) {
5680
+ function extractPlacementFromFieldMap(fieldMap) {
4669
5681
  const orbitField = fieldMap.get("orbit")?.[0];
4670
5682
  const atField = fieldMap.get("at")?.[0];
4671
5683
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4833,7 +5845,7 @@
4833
5845
  }
4834
5846
  }
4835
5847
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4836
- if (sourceSchemaVersion === "2.1") {
5848
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4837
5849
  return;
4838
5850
  }
4839
5851
  diagnostics.push({
@@ -4845,6 +5857,34 @@
4845
5857
  column: location.column
4846
5858
  });
4847
5859
  }
5860
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5861
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
5862
+ return;
5863
+ }
5864
+ diagnostics.push({
5865
+ code: "parse.schema25.featureCompatibility",
5866
+ severity: "warning",
5867
+ source: "parse",
5868
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5869
+ line: location.line,
5870
+ column: location.column
5871
+ });
5872
+ }
5873
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
5874
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
5875
+ }
5876
+ function schemaVersionRank(version) {
5877
+ switch (version) {
5878
+ case "2.0-draft":
5879
+ return 0;
5880
+ case "2.0":
5881
+ return 1;
5882
+ case "2.1":
5883
+ return 2;
5884
+ case "2.5":
5885
+ return 3;
5886
+ }
5887
+ }
4848
5888
  function preprocessAtlasSource(source) {
4849
5889
  const chars = [...source];
4850
5890
  const comments = [];
@@ -4932,7 +5972,7 @@
4932
5972
  }
4933
5973
 
4934
5974
  // packages/core/dist/atlas-edit.js
4935
- function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
5975
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.5") {
4936
5976
  return {
4937
5977
  format: "worldorbit",
4938
5978
  version,
@@ -4958,6 +5998,7 @@
4958
5998
  },
4959
5999
  groups: [],
4960
6000
  relations: [],
6001
+ events: [],
4961
6002
  objects: [],
4962
6003
  diagnostics: []
4963
6004
  };
@@ -4984,6 +6025,12 @@
4984
6025
  for (const relation of [...document.relations].sort(compareIdLike2)) {
4985
6026
  paths.push({ kind: "relation", id: relation.id });
4986
6027
  }
6028
+ for (const event of [...document.events].sort(compareIdLike2)) {
6029
+ paths.push({ kind: "event", id: event.id });
6030
+ for (const pose of [...event.positions].sort(comparePoseObjectId2)) {
6031
+ paths.push({ kind: "event-pose", id: event.id, key: pose.objectId });
6032
+ }
6033
+ }
4987
6034
  for (const object of [...document.objects].sort(compareIdLike2)) {
4988
6035
  paths.push({ kind: "object", id: object.id });
4989
6036
  }
@@ -4999,6 +6046,10 @@
4999
6046
  return path.key ? document.system?.atlasMetadata[path.key] ?? null : null;
5000
6047
  case "group":
5001
6048
  return path.id ? findGroup(document, path.id) : null;
6049
+ case "event":
6050
+ return path.id ? findEvent(document, path.id) : null;
6051
+ case "event-pose":
6052
+ return path.id && path.key ? findEventPose(document, path.id, path.key) : null;
5002
6053
  case "object":
5003
6054
  return path.id ? findObject(document, path.id) : null;
5004
6055
  case "viewpoint":
@@ -5038,6 +6089,18 @@
5038
6089
  }
5039
6090
  upsertById(next.groups, value);
5040
6091
  return next;
6092
+ case "event":
6093
+ if (!path.id) {
6094
+ throw new Error('Event updates require an "id" value.');
6095
+ }
6096
+ upsertById(next.events, value);
6097
+ return next;
6098
+ case "event-pose":
6099
+ if (!path.id || !path.key) {
6100
+ throw new Error('Event pose updates require an event "id" and pose "key" value.');
6101
+ }
6102
+ upsertEventPose(next.events, path.id, value);
6103
+ return next;
5041
6104
  case "object":
5042
6105
  if (!path.id) {
5043
6106
  throw new Error('Object updates require an "id" value.');
@@ -5086,6 +6149,19 @@
5086
6149
  next.groups = next.groups.filter((group) => group.id !== path.id);
5087
6150
  }
5088
6151
  return next;
6152
+ case "event":
6153
+ if (path.id) {
6154
+ next.events = next.events.filter((event) => event.id !== path.id);
6155
+ }
6156
+ return next;
6157
+ case "event-pose":
6158
+ if (path.id && path.key) {
6159
+ const event = findEvent(next, path.id);
6160
+ if (event) {
6161
+ event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
6162
+ }
6163
+ }
6164
+ return next;
5089
6165
  case "viewpoint":
5090
6166
  if (path.id) {
5091
6167
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -5154,6 +6230,22 @@
5154
6230
  };
5155
6231
  }
5156
6232
  }
6233
+ if (diagnostic.field?.startsWith("event.")) {
6234
+ const parts = diagnostic.field.split(".");
6235
+ if (parts[1] && findEvent(document, parts[1])) {
6236
+ if (parts[2] === "pose" && parts[3] && findEventPose(document, parts[1], parts[3])) {
6237
+ return {
6238
+ kind: "event-pose",
6239
+ id: parts[1],
6240
+ key: parts[3]
6241
+ };
6242
+ }
6243
+ return {
6244
+ kind: "event",
6245
+ id: parts[1]
6246
+ };
6247
+ }
6248
+ }
5157
6249
  if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
5158
6250
  return {
5159
6251
  kind: "metadata",
@@ -5185,6 +6277,12 @@
5185
6277
  function findRelation(document, relationId) {
5186
6278
  return document.relations.find((relation) => relation.id === relationId) ?? null;
5187
6279
  }
6280
+ function findEvent(document, eventId) {
6281
+ return document.events.find((event) => event.id === eventId) ?? null;
6282
+ }
6283
+ function findEventPose(document, eventId, objectId) {
6284
+ return findEvent(document, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
6285
+ }
5188
6286
  function findViewpoint(system, viewpointId) {
5189
6287
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
5190
6288
  }
@@ -5200,13 +6298,30 @@
5200
6298
  }
5201
6299
  items[index] = value;
5202
6300
  }
6301
+ function upsertEventPose(events, eventId, value) {
6302
+ const event = events.find((entry) => entry.id === eventId);
6303
+ if (!event) {
6304
+ throw new Error(`Unknown event "${eventId}" for pose update.`);
6305
+ }
6306
+ const index = event.positions.findIndex((entry) => entry.objectId === value.objectId);
6307
+ if (index === -1) {
6308
+ event.positions.push(value);
6309
+ event.positions.sort(comparePoseObjectId2);
6310
+ return;
6311
+ }
6312
+ event.positions[index] = value;
6313
+ }
5203
6314
  function compareIdLike2(left, right) {
5204
6315
  return left.id.localeCompare(right.id);
5205
6316
  }
6317
+ function comparePoseObjectId2(left, right) {
6318
+ return left.objectId.localeCompare(right.objectId);
6319
+ }
5206
6320
 
5207
6321
  // packages/core/dist/load.js
5208
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
6322
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
5209
6323
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
6324
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
5210
6325
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
5211
6326
  function detectWorldOrbitSchemaVersion(source) {
5212
6327
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -5220,6 +6335,9 @@
5220
6335
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5221
6336
  return "2.1";
5222
6337
  }
6338
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
6339
+ return "2.5";
6340
+ }
5223
6341
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
5224
6342
  return "2.0";
5225
6343
  }
@@ -5280,7 +6398,7 @@
5280
6398
  }
5281
6399
  function loadWorldOrbitSourceWithDiagnostics(source) {
5282
6400
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
5283
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
6401
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1" || schemaVersion === "2.5") {
5284
6402
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
5285
6403
  }
5286
6404
  let ast;