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
@@ -615,6 +615,7 @@ var WorldOrbit = (() => {
615
615
  system,
616
616
  groups: [],
617
617
  relations: [],
618
+ events: [],
618
619
  objects
619
620
  };
620
621
  }
@@ -1068,12 +1069,16 @@ var WorldOrbit = (() => {
1068
1069
  const height = frame.height;
1069
1070
  const padding = frame.padding;
1070
1071
  const layoutPreset = resolveLayoutPreset(document);
1071
- const projection = resolveProjection(document, options.projection);
1072
+ const schemaProjection = resolveProjection(document, options.projection);
1073
+ const camera = normalizeViewCamera(options.camera ?? null);
1074
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
1072
1075
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1073
1076
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1074
1077
  const systemId = document.system?.id ?? null;
1075
- const objectMap = new Map(document.objects.map((object) => [object.id, object]));
1076
- const relationships = buildSceneRelationships(document.objects, objectMap);
1078
+ const activeEventId = options.activeEventId ?? null;
1079
+ const effectiveObjects = createEffectiveObjects(document.objects, document.events ?? [], activeEventId);
1080
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
1081
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
1077
1082
  const positions = /* @__PURE__ */ new Map();
1078
1083
  const orbitDrafts = [];
1079
1084
  const leaderDrafts = [];
@@ -1082,7 +1087,7 @@ var WorldOrbit = (() => {
1082
1087
  const atObjects = [];
1083
1088
  const surfaceChildren = /* @__PURE__ */ new Map();
1084
1089
  const orbitChildren = /* @__PURE__ */ new Map();
1085
- for (const object of document.objects) {
1090
+ for (const object of effectiveObjects) {
1086
1091
  const placement = object.placement;
1087
1092
  if (!placement) {
1088
1093
  rootObjects.push(object);
@@ -1109,7 +1114,7 @@ var WorldOrbit = (() => {
1109
1114
  surfaceChildren,
1110
1115
  objectMap,
1111
1116
  spacingFactor,
1112
- projection,
1117
+ projection: renderProjection,
1113
1118
  scaleModel
1114
1119
  };
1115
1120
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -1121,7 +1126,7 @@ var WorldOrbit = (() => {
1121
1126
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1122
1127
  secondaryRoots.forEach((object, index) => {
1123
1128
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
1124
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
1129
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
1125
1130
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
1126
1131
  });
1127
1132
  }
@@ -1177,38 +1182,48 @@ var WorldOrbit = (() => {
1177
1182
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1178
1183
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1179
1184
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1180
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1185
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1181
1186
  const relations = createSceneRelations(document, objects);
1182
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1183
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1187
+ const events = createSceneEvents(document.events ?? [], objects, activeEventId);
1188
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1189
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1184
1190
  const semanticGroups = createSceneSemanticGroups(document, objects);
1185
- const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
1186
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1191
+ const viewpoints = createSceneViewpoints(document, schemaProjection, frame.preset, relationships, objectMap);
1192
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1187
1193
  return {
1188
1194
  width,
1189
1195
  height,
1190
1196
  padding,
1191
1197
  renderPreset: frame.preset,
1192
- projection,
1198
+ projection: schemaProjection,
1199
+ renderProjection,
1200
+ camera,
1193
1201
  scaleModel,
1194
1202
  title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1195
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1203
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1196
1204
  systemId,
1197
- viewMode: projection,
1205
+ viewMode: schemaProjection,
1198
1206
  layoutPreset,
1199
1207
  metadata: {
1200
1208
  format: document.format,
1201
1209
  version: document.version,
1202
- view: projection,
1210
+ view: schemaProjection,
1211
+ renderProjection,
1203
1212
  scale: String(document.system?.properties.scale ?? layoutPreset),
1204
1213
  units: String(document.system?.properties.units ?? "mixed"),
1205
- preset: frame.preset ?? "custom"
1214
+ preset: frame.preset ?? "custom",
1215
+ ...camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {},
1216
+ ...camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {},
1217
+ ...camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {},
1218
+ ...camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}
1206
1219
  },
1207
1220
  contentBounds,
1208
1221
  layers,
1209
1222
  groups,
1210
1223
  semanticGroups,
1211
1224
  viewpoints,
1225
+ events,
1226
+ activeEventId,
1212
1227
  objects,
1213
1228
  orbitVisuals,
1214
1229
  relations,
@@ -1227,6 +1242,56 @@ var WorldOrbit = (() => {
1227
1242
  y: center.y + dx * sin + dy * cos
1228
1243
  };
1229
1244
  }
1245
+ function createEffectiveObjects(objects, events, activeEventId) {
1246
+ const cloned = objects.map((object) => structuredClone(object));
1247
+ if (!activeEventId) {
1248
+ return cloned;
1249
+ }
1250
+ const activeEvent = events.find((event) => event.id === activeEventId);
1251
+ if (!activeEvent) {
1252
+ return cloned;
1253
+ }
1254
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1255
+ const referencedIds = /* @__PURE__ */ new Set([
1256
+ ...activeEvent.targetObjectId ? [activeEvent.targetObjectId] : [],
1257
+ ...activeEvent.participantObjectIds,
1258
+ ...activeEvent.positions.map((pose) => pose.objectId)
1259
+ ]);
1260
+ for (const objectId of referencedIds) {
1261
+ const object = objectMap.get(objectId);
1262
+ if (!object) {
1263
+ continue;
1264
+ }
1265
+ if (activeEvent.epoch) {
1266
+ object.epoch = activeEvent.epoch;
1267
+ }
1268
+ if (activeEvent.referencePlane) {
1269
+ object.referencePlane = activeEvent.referencePlane;
1270
+ }
1271
+ }
1272
+ for (const pose of activeEvent.positions) {
1273
+ const object = objectMap.get(pose.objectId);
1274
+ if (!object) {
1275
+ continue;
1276
+ }
1277
+ if (pose.placement) {
1278
+ object.placement = structuredClone(pose.placement);
1279
+ }
1280
+ if (pose.inner) {
1281
+ object.properties.inner = { ...pose.inner };
1282
+ }
1283
+ if (pose.outer) {
1284
+ object.properties.outer = { ...pose.outer };
1285
+ }
1286
+ if (pose.epoch) {
1287
+ object.epoch = pose.epoch;
1288
+ }
1289
+ if (pose.referencePlane) {
1290
+ object.referencePlane = pose.referencePlane;
1291
+ }
1292
+ }
1293
+ return cloned;
1294
+ }
1230
1295
  function resolveLayoutPreset(document) {
1231
1296
  const rawScale = String(document.system?.properties.scale ?? "balanced").toLowerCase();
1232
1297
  switch (rawScale) {
@@ -1263,10 +1328,59 @@ var WorldOrbit = (() => {
1263
1328
  }
1264
1329
  }
1265
1330
  function resolveProjection(document, projection) {
1266
- if (projection === "topdown" || projection === "isometric") {
1331
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1267
1332
  return projection;
1268
1333
  }
1269
- return String(document.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1334
+ const documentView = String(document.system?.properties.view ?? "topdown").toLowerCase();
1335
+ return parseViewProjection(documentView) ?? "topdown";
1336
+ }
1337
+ function resolveRenderProjection(projection, camera) {
1338
+ switch (projection) {
1339
+ case "topdown":
1340
+ return "topdown";
1341
+ case "isometric":
1342
+ return "isometric";
1343
+ case "orthographic":
1344
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null) ? "isometric" : "topdown";
1345
+ case "perspective":
1346
+ return "isometric";
1347
+ }
1348
+ }
1349
+ function normalizeViewCamera(camera) {
1350
+ if (!camera) {
1351
+ return null;
1352
+ }
1353
+ const normalized = {
1354
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
1355
+ elevation: normalizeFiniteCameraValue(camera.elevation),
1356
+ roll: normalizeFiniteCameraValue(camera.roll),
1357
+ distance: normalizePositiveCameraDistance(camera.distance)
1358
+ };
1359
+ return normalized.azimuth !== null || normalized.elevation !== null || normalized.roll !== null || normalized.distance !== null ? normalized : null;
1360
+ }
1361
+ function normalizeFiniteCameraValue(value) {
1362
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1363
+ }
1364
+ function normalizePositiveCameraDistance(value) {
1365
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1366
+ }
1367
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
1368
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
1369
+ if (projection !== renderProjection) {
1370
+ parts.push(`2D ${renderProjection} fallback`);
1371
+ }
1372
+ if (camera) {
1373
+ const cameraParts = [
1374
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
1375
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
1376
+ camera.roll !== null ? `roll ${camera.roll}` : null,
1377
+ camera.distance !== null ? `dist ${camera.distance}` : null
1378
+ ].filter(Boolean);
1379
+ if (cameraParts.length > 0) {
1380
+ parts.push(`camera ${cameraParts.join(" / ")}`);
1381
+ }
1382
+ }
1383
+ return parts.join(" - ");
1270
1384
  }
1271
1385
  function resolveScaleModel(layoutPreset, overrides) {
1272
1386
  const defaults = defaultScaleModel(layoutPreset);
@@ -1382,24 +1496,14 @@ var WorldOrbit = (() => {
1382
1496
  hidden: draft.object.properties.hidden === true
1383
1497
  };
1384
1498
  }
1385
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1499
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1386
1500
  const labels = [];
1387
1501
  const occupied = [];
1388
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1502
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1503
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1389
1504
  for (const object of visibleObjects) {
1390
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1391
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1392
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1393
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1394
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1395
- let attempts = 0;
1396
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1397
- labelY += direction * 14 * labelMultiplier;
1398
- secondaryY += direction * 14 * labelMultiplier;
1399
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1400
- attempts += 1;
1401
- }
1402
- occupied.push(bounds);
1505
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1506
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1403
1507
  labels.push({
1404
1508
  renderId: `${object.renderId}-label`,
1405
1509
  objectId: object.objectId,
@@ -1408,17 +1512,128 @@ var WorldOrbit = (() => {
1408
1512
  semanticGroupIds: [...object.semanticGroupIds],
1409
1513
  label: object.label,
1410
1514
  secondaryLabel: object.secondaryLabel,
1411
- x: object.x,
1412
- y: labelY,
1413
- secondaryY,
1414
- textAnchor: "middle",
1415
- direction: direction < 0 ? "above" : "below",
1515
+ x: placement.x,
1516
+ y: placement.labelY,
1517
+ secondaryY: placement.secondaryY,
1518
+ textAnchor: placement.textAnchor,
1519
+ direction: placement.direction,
1416
1520
  hidden: object.hidden
1417
1521
  });
1418
1522
  }
1419
1523
  return labels;
1420
1524
  }
1421
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1525
+ function compareLabelPlacementOrder(left, right) {
1526
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1527
+ if (priorityDiff !== 0) {
1528
+ return priorityDiff;
1529
+ }
1530
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1531
+ if (renderPriorityDiff !== 0) {
1532
+ return renderPriorityDiff;
1533
+ }
1534
+ return left.sortKey - right.sortKey;
1535
+ }
1536
+ function labelPlacementPriority(object) {
1537
+ switch (object.object.type) {
1538
+ case "star":
1539
+ return 0;
1540
+ case "planet":
1541
+ return 1;
1542
+ case "moon":
1543
+ return 2;
1544
+ case "belt":
1545
+ case "ring":
1546
+ return 3;
1547
+ case "asteroid":
1548
+ case "comet":
1549
+ return 4;
1550
+ case "structure":
1551
+ case "phenomenon":
1552
+ return 5;
1553
+ }
1554
+ }
1555
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1556
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1557
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1558
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1559
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1560
+ const rect = createLabelRect(object, placement, labelMultiplier);
1561
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1562
+ return placement;
1563
+ }
1564
+ }
1565
+ }
1566
+ return null;
1567
+ }
1568
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1569
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1570
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1571
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1572
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1573
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1574
+ 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";
1575
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1576
+ }
1577
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1578
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1579
+ return object.y >= parent.y ? "below" : "above";
1580
+ }
1581
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1582
+ }
1583
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1584
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1585
+ return object.x >= parent.x ? "right" : "left";
1586
+ }
1587
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1588
+ }
1589
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1590
+ const step = 14 * labelMultiplier;
1591
+ switch (direction) {
1592
+ case "above": {
1593
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1594
+ return {
1595
+ x: object.x,
1596
+ labelY,
1597
+ secondaryY: labelY - 16 * labelMultiplier,
1598
+ textAnchor: "middle",
1599
+ direction
1600
+ };
1601
+ }
1602
+ case "below": {
1603
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1604
+ return {
1605
+ x: object.x,
1606
+ labelY,
1607
+ secondaryY: labelY + 16 * labelMultiplier,
1608
+ textAnchor: "middle",
1609
+ direction
1610
+ };
1611
+ }
1612
+ case "left": {
1613
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1614
+ const labelY = object.y - 4 * labelMultiplier;
1615
+ return {
1616
+ x,
1617
+ labelY,
1618
+ secondaryY: labelY + 16 * labelMultiplier,
1619
+ textAnchor: "end",
1620
+ direction
1621
+ };
1622
+ }
1623
+ case "right": {
1624
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1625
+ const labelY = object.y - 4 * labelMultiplier;
1626
+ return {
1627
+ x,
1628
+ labelY,
1629
+ secondaryY: labelY + 16 * labelMultiplier,
1630
+ textAnchor: "start",
1631
+ direction
1632
+ };
1633
+ }
1634
+ }
1635
+ }
1636
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1422
1637
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1423
1638
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1424
1639
  return [
@@ -1433,6 +1648,10 @@ var WorldOrbit = (() => {
1433
1648
  id: "relations",
1434
1649
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1435
1650
  },
1651
+ {
1652
+ id: "events",
1653
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1654
+ },
1436
1655
  {
1437
1656
  id: "objects",
1438
1657
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1444,7 +1663,7 @@ var WorldOrbit = (() => {
1444
1663
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1445
1664
  ];
1446
1665
  }
1447
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1666
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1448
1667
  const groups = /* @__PURE__ */ new Map();
1449
1668
  const ensureGroup = (groupId) => {
1450
1669
  if (!groupId) {
@@ -1493,7 +1712,7 @@ var WorldOrbit = (() => {
1493
1712
  }
1494
1713
  }
1495
1714
  for (const group of groups.values()) {
1496
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1715
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1497
1716
  }
1498
1717
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1499
1718
  }
@@ -1527,6 +1746,29 @@ var WorldOrbit = (() => {
1527
1746
  };
1528
1747
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1529
1748
  }
1749
+ function createSceneEvents(events, objects, activeEventId) {
1750
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1751
+ return events.map((event) => {
1752
+ const objectIds = [.../* @__PURE__ */ new Set([
1753
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1754
+ ...event.participantObjectIds
1755
+ ])];
1756
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1757
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1758
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1759
+ return {
1760
+ renderId: `${createRenderId(event.id)}-event`,
1761
+ eventId: event.id,
1762
+ event,
1763
+ objectIds,
1764
+ participantIds: [...event.participantObjectIds],
1765
+ targetObjectId: event.targetObjectId,
1766
+ x: centroidX,
1767
+ y: centroidY,
1768
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1769
+ };
1770
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1771
+ }
1530
1772
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
1531
1773
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
1532
1774
  const drafts = /* @__PURE__ */ new Map();
@@ -1574,13 +1816,18 @@ var WorldOrbit = (() => {
1574
1816
  function createGeneratedOverviewViewpoint(document, projection, preset) {
1575
1817
  const title = document.system?.title ?? document.system?.properties.title;
1576
1818
  const label = title ? `${String(title)} Overview` : "Overview";
1819
+ const camera = normalizeViewCamera(null);
1820
+ const renderProjection = resolveRenderProjection(projection, camera);
1577
1821
  return {
1578
1822
  id: "overview",
1579
1823
  label,
1580
1824
  summary: "Fit the whole system with the current atlas defaults.",
1581
1825
  objectId: null,
1582
1826
  selectedObjectId: null,
1827
+ eventIds: [],
1583
1828
  projection,
1829
+ renderProjection,
1830
+ camera,
1584
1831
  preset,
1585
1832
  rotationDeg: 0,
1586
1833
  scale: null,
@@ -1616,6 +1863,9 @@ var WorldOrbit = (() => {
1616
1863
  draft.select = normalizedValue;
1617
1864
  }
1618
1865
  return;
1866
+ case "events":
1867
+ draft.eventIds = splitListValue(normalizedValue);
1868
+ return;
1619
1869
  case "projection":
1620
1870
  case "view":
1621
1871
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1627,6 +1877,30 @@ var WorldOrbit = (() => {
1627
1877
  case "angle":
1628
1878
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1629
1879
  return;
1880
+ case "camera.azimuth":
1881
+ draft.camera = {
1882
+ ...draft.camera ?? createEmptyViewCamera(),
1883
+ azimuth: parseFiniteNumber(normalizedValue)
1884
+ };
1885
+ return;
1886
+ case "camera.elevation":
1887
+ draft.camera = {
1888
+ ...draft.camera ?? createEmptyViewCamera(),
1889
+ elevation: parseFiniteNumber(normalizedValue)
1890
+ };
1891
+ return;
1892
+ case "camera.roll":
1893
+ draft.camera = {
1894
+ ...draft.camera ?? createEmptyViewCamera(),
1895
+ roll: parseFiniteNumber(normalizedValue)
1896
+ };
1897
+ return;
1898
+ case "camera.distance":
1899
+ draft.camera = {
1900
+ ...draft.camera ?? createEmptyViewCamera(),
1901
+ distance: parsePositiveNumber(normalizedValue)
1902
+ };
1903
+ return;
1630
1904
  case "zoom":
1631
1905
  case "scale":
1632
1906
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -1666,13 +1940,19 @@ var WorldOrbit = (() => {
1666
1940
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1667
1941
  const filter = normalizeViewpointFilter(draft.filter);
1668
1942
  const label = draft.label?.trim() || humanizeIdentifier(draft.id);
1943
+ const resolvedProjection = draft.projection ?? projection;
1944
+ const camera = normalizeViewCamera(draft.camera ?? null);
1945
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
1669
1946
  return {
1670
1947
  id: draft.id,
1671
1948
  label,
1672
1949
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1673
1950
  objectId,
1674
1951
  selectedObjectId,
1675
- projection: draft.projection ?? projection,
1952
+ eventIds: [...new Set(draft.eventIds ?? [])],
1953
+ projection: resolvedProjection,
1954
+ renderProjection,
1955
+ camera,
1676
1956
  preset: draft.preset ?? preset,
1677
1957
  rotationDeg: draft.rotationDeg ?? 0,
1678
1958
  scale: draft.scale ?? null,
@@ -1689,6 +1969,14 @@ var WorldOrbit = (() => {
1689
1969
  groupIds: []
1690
1970
  };
1691
1971
  }
1972
+ function createEmptyViewCamera() {
1973
+ return {
1974
+ azimuth: null,
1975
+ elevation: null,
1976
+ roll: null,
1977
+ distance: null
1978
+ };
1979
+ }
1692
1980
  function normalizeViewpointFilter(filter) {
1693
1981
  if (!filter) {
1694
1982
  return null;
@@ -1702,7 +1990,18 @@ var WorldOrbit = (() => {
1702
1990
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1703
1991
  }
1704
1992
  function parseViewProjection(value) {
1705
- return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
1993
+ switch (value.toLowerCase()) {
1994
+ case "topdown":
1995
+ return "topdown";
1996
+ case "isometric":
1997
+ return "isometric";
1998
+ case "orthographic":
1999
+ return "orthographic";
2000
+ case "perspective":
2001
+ return "perspective";
2002
+ default:
2003
+ return null;
2004
+ }
1706
2005
  }
1707
2006
  function parseRenderPreset(value) {
1708
2007
  const normalized = value.toLowerCase();
@@ -1729,7 +2028,7 @@ var WorldOrbit = (() => {
1729
2028
  next["orbits-front"] = enabled;
1730
2029
  continue;
1731
2030
  }
1732
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2031
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1733
2032
  next[rawLayer] = enabled;
1734
2033
  }
1735
2034
  }
@@ -1740,7 +2039,7 @@ var WorldOrbit = (() => {
1740
2039
  }
1741
2040
  function parseViewpointGroups(value, document, relationships, objectMap) {
1742
2041
  return splitListValue(value).map((entry) => {
1743
- if (document.schemaVersion === "2.1" || document.groups.some((group) => group.id === entry)) {
2042
+ if (document.schemaVersion === "2.1" || document.schemaVersion === "2.5" || document.groups.some((group) => group.id === entry)) {
1744
2043
  return entry;
1745
2044
  }
1746
2045
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -1777,7 +2076,7 @@ var WorldOrbit = (() => {
1777
2076
  }
1778
2077
  return parts.join(" - ");
1779
2078
  }
1780
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
2079
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1781
2080
  let minX = Number.POSITIVE_INFINITY;
1782
2081
  let minY = Number.POSITIVE_INFINITY;
1783
2082
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1807,7 +2106,7 @@ var WorldOrbit = (() => {
1807
2106
  for (const label of labels) {
1808
2107
  if (label.hidden)
1809
2108
  continue;
1810
- includeLabelBounds(label, include);
2109
+ includeLabelBounds(label, include, labelMultiplier);
1811
2110
  }
1812
2111
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1813
2112
  return createBounds(0, 0, width, height);
@@ -1845,13 +2144,10 @@ var WorldOrbit = (() => {
1845
2144
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1846
2145
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1847
2146
  }
1848
- function includeLabelBounds(label, include) {
1849
- const labelScale = 1;
1850
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1851
- include(label.x - labelHalfWidth, label.y - 18);
1852
- include(label.x + labelHalfWidth, label.y + 8);
1853
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1854
- include(label.x + labelHalfWidth, label.secondaryY + 8);
2147
+ function includeLabelBounds(label, include, labelMultiplier) {
2148
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
2149
+ include(bounds.left, bounds.top);
2150
+ include(bounds.right, bounds.bottom);
1855
2151
  }
1856
2152
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1857
2153
  if (positions.has(object.id)) {
@@ -2241,7 +2537,7 @@ var WorldOrbit = (() => {
2241
2537
  return null;
2242
2538
  }
2243
2539
  }
2244
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2540
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2245
2541
  let minX = Number.POSITIVE_INFINITY;
2246
2542
  let minY = Number.POSITIVE_INFINITY;
2247
2543
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2270,7 +2566,7 @@ var WorldOrbit = (() => {
2270
2566
  }
2271
2567
  for (const label of labels) {
2272
2568
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2273
- includeLabelBounds(label, include);
2569
+ includeLabelBounds(label, include, labelMultiplier);
2274
2570
  }
2275
2571
  }
2276
2572
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2295,12 +2591,28 @@ var WorldOrbit = (() => {
2295
2591
  }
2296
2592
  return current.id;
2297
2593
  }
2298
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2594
+ function createLabelRect(object, placement, labelMultiplier) {
2595
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2596
+ }
2597
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2598
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2599
+ const labelWidth = labelHalfWidth * 2;
2600
+ const topPadding = direction === "above" ? 18 : 12;
2601
+ const bottomPadding = direction === "above" ? 8 : 12;
2602
+ let left = x - labelHalfWidth;
2603
+ let right = x + labelHalfWidth;
2604
+ if (textAnchor === "start") {
2605
+ left = x;
2606
+ right = x + labelWidth;
2607
+ } else if (textAnchor === "end") {
2608
+ left = x - labelWidth;
2609
+ right = x;
2610
+ }
2299
2611
  return {
2300
- left: x - labelHalfWidth,
2301
- right: x + labelHalfWidth,
2302
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2303
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2612
+ left,
2613
+ right,
2614
+ top: Math.min(labelY, secondaryY) - topPadding,
2615
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2304
2616
  };
2305
2617
  }
2306
2618
  function rectsOverlap(left, right) {
@@ -2487,11 +2799,6 @@ var WorldOrbit = (() => {
2487
2799
  function customColorFor(value) {
2488
2800
  return typeof value === "string" && value.trim() ? value : void 0;
2489
2801
  }
2490
- function estimateLabelHalfWidth(object, labelMultiplier) {
2491
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2492
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2493
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2494
- }
2495
2802
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2496
2803
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2497
2804
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2525,12 +2832,13 @@ var WorldOrbit = (() => {
2525
2832
  }
2526
2833
  return {
2527
2834
  format: "worldorbit",
2528
- version: "2.0",
2529
- schemaVersion: "2.0",
2835
+ version: "2.5",
2836
+ schemaVersion: "2.5",
2530
2837
  sourceVersion: document.version,
2531
2838
  system,
2532
2839
  groups: structuredClone(document.groups ?? []),
2533
2840
  relations: structuredClone(document.relations ?? []),
2841
+ events: structuredClone(document.events ?? []),
2534
2842
  objects: document.objects.map(cloneWorldOrbitObject),
2535
2843
  diagnostics
2536
2844
  };
@@ -2538,7 +2846,7 @@ var WorldOrbit = (() => {
2538
2846
  function upgradeDocumentToDraftV2(document, options = {}) {
2539
2847
  return convertAtlasDocumentToLegacyDraft(upgradeDocumentToV2(document, options));
2540
2848
  }
2541
- function materializeAtlasDocument(document) {
2849
+ function materializeAtlasDocument(document, options = {}) {
2542
2850
  const system = document.system ? {
2543
2851
  type: "system",
2544
2852
  id: document.system.id,
@@ -2549,6 +2857,8 @@ var WorldOrbit = (() => {
2549
2857
  properties: materializeDraftSystemProperties(document.system),
2550
2858
  info: materializeDraftSystemInfo(document.system)
2551
2859
  } : null;
2860
+ const objects = document.objects.map(cloneWorldOrbitObject);
2861
+ applyEventPoseOverrides(objects, document.events ?? [], options.activeEventId ?? null);
2552
2862
  return {
2553
2863
  format: "worldorbit",
2554
2864
  version: "1.0",
@@ -2556,7 +2866,8 @@ var WorldOrbit = (() => {
2556
2866
  system,
2557
2867
  groups: structuredClone(document.groups ?? []),
2558
2868
  relations: structuredClone(document.relations ?? []),
2559
- objects: document.objects.map(cloneWorldOrbitObject)
2869
+ events: document.events.map(cloneWorldOrbitEvent),
2870
+ objects
2560
2871
  };
2561
2872
  }
2562
2873
  function materializeDraftDocument(document) {
@@ -2581,8 +2892,9 @@ var WorldOrbit = (() => {
2581
2892
  };
2582
2893
  }
2583
2894
  function createDraftDefaults(document, preset, projection) {
2895
+ const rawView = typeof document.system?.properties.view === "string" ? document.system.properties.view.toLowerCase() : null;
2584
2896
  return {
2585
- view: typeof document.system?.properties.view === "string" && document.system.properties.view.toLowerCase() === "topdown" ? "topdown" : projection,
2897
+ view: rawView === "topdown" || rawView === "isometric" || rawView === "orthographic" || rawView === "perspective" ? rawView : projection,
2586
2898
  scale: typeof document.system?.properties.scale === "string" ? document.system.properties.scale : null,
2587
2899
  units: typeof document.system?.properties.units === "string" ? document.system.properties.units : null,
2588
2900
  preset,
@@ -2684,10 +2996,12 @@ var WorldOrbit = (() => {
2684
2996
  summary: viewpoint.summary,
2685
2997
  focusObjectId: viewpoint.objectId,
2686
2998
  selectedObjectId: viewpoint.selectedObjectId,
2999
+ events: [...viewpoint.eventIds],
2687
3000
  projection: viewpoint.projection,
2688
3001
  preset: viewpoint.preset,
2689
3002
  zoom: viewpoint.scale,
2690
3003
  rotationDeg: viewpoint.rotationDeg,
3004
+ camera: viewpoint.camera ? { ...viewpoint.camera } : null,
2691
3005
  layers: { ...viewpoint.layers },
2692
3006
  filter: viewpoint.filter ? {
2693
3007
  query: viewpoint.filter.query,
@@ -2716,6 +3030,75 @@ var WorldOrbit = (() => {
2716
3030
  info: { ...object.info }
2717
3031
  };
2718
3032
  }
3033
+ function cloneWorldOrbitEvent(event) {
3034
+ return {
3035
+ ...event,
3036
+ participantObjectIds: [...event.participantObjectIds],
3037
+ tags: [...event.tags],
3038
+ positions: event.positions.map(cloneWorldOrbitEventPose)
3039
+ };
3040
+ }
3041
+ function cloneWorldOrbitEventPose(pose) {
3042
+ return {
3043
+ objectId: pose.objectId,
3044
+ placement: clonePlacement(pose.placement),
3045
+ inner: pose.inner ? { ...pose.inner } : void 0,
3046
+ outer: pose.outer ? { ...pose.outer } : void 0,
3047
+ epoch: pose.epoch ?? null,
3048
+ referencePlane: pose.referencePlane ?? null
3049
+ };
3050
+ }
3051
+ function clonePlacement(placement) {
3052
+ return placement ? structuredClone(placement) : null;
3053
+ }
3054
+ function applyEventPoseOverrides(objects, events, activeEventId) {
3055
+ if (!activeEventId) {
3056
+ return;
3057
+ }
3058
+ const event = events.find((entry) => entry.id === activeEventId);
3059
+ if (!event) {
3060
+ return;
3061
+ }
3062
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
3063
+ const referencedIds = /* @__PURE__ */ new Set([
3064
+ ...event.targetObjectId ? [event.targetObjectId] : [],
3065
+ ...event.participantObjectIds,
3066
+ ...event.positions.map((pose) => pose.objectId)
3067
+ ]);
3068
+ for (const objectId of referencedIds) {
3069
+ const object = objectMap.get(objectId);
3070
+ if (!object) {
3071
+ continue;
3072
+ }
3073
+ if (event.epoch) {
3074
+ object.epoch = event.epoch;
3075
+ }
3076
+ if (event.referencePlane) {
3077
+ object.referencePlane = event.referencePlane;
3078
+ }
3079
+ }
3080
+ for (const pose of event.positions) {
3081
+ const object = objectMap.get(pose.objectId);
3082
+ if (!object) {
3083
+ continue;
3084
+ }
3085
+ if (pose.placement) {
3086
+ object.placement = clonePlacement(pose.placement);
3087
+ }
3088
+ if (pose.inner) {
3089
+ object.properties.inner = { ...pose.inner };
3090
+ }
3091
+ if (pose.outer) {
3092
+ object.properties.outer = { ...pose.outer };
3093
+ }
3094
+ if (pose.epoch) {
3095
+ object.epoch = pose.epoch;
3096
+ }
3097
+ if (pose.referencePlane) {
3098
+ object.referencePlane = pose.referencePlane;
3099
+ }
3100
+ }
3101
+ }
2719
3102
  function cloneProperties(properties) {
2720
3103
  const next = {};
2721
3104
  for (const [key, value] of Object.entries(properties)) {
@@ -2797,6 +3180,18 @@ var WorldOrbit = (() => {
2797
3180
  if (viewpoint.rotationDeg !== 0) {
2798
3181
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2799
3182
  }
3183
+ if (viewpoint.camera?.azimuth !== null) {
3184
+ info2[`${prefix}.camera.azimuth`] = String(viewpoint.camera?.azimuth);
3185
+ }
3186
+ if (viewpoint.camera?.elevation !== null) {
3187
+ info2[`${prefix}.camera.elevation`] = String(viewpoint.camera?.elevation);
3188
+ }
3189
+ if (viewpoint.camera?.roll !== null) {
3190
+ info2[`${prefix}.camera.roll`] = String(viewpoint.camera?.roll);
3191
+ }
3192
+ if (viewpoint.camera?.distance !== null) {
3193
+ info2[`${prefix}.camera.distance`] = String(viewpoint.camera?.distance);
3194
+ }
2800
3195
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2801
3196
  if (serializedLayers) {
2802
3197
  info2[`${prefix}.layers`] = serializedLayers;
@@ -2813,6 +3208,9 @@ var WorldOrbit = (() => {
2813
3208
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2814
3209
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2815
3210
  }
3211
+ if (viewpoint.events.length > 0) {
3212
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
3213
+ }
2816
3214
  }
2817
3215
  for (const annotation of system.annotations) {
2818
3216
  const prefix = `annotation.${annotation.id}`;
@@ -2837,7 +3235,7 @@ var WorldOrbit = (() => {
2837
3235
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2838
3236
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2839
3237
  }
2840
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3238
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2841
3239
  if (layers[key] !== void 0) {
2842
3240
  tokens.push(layers[key] ? key : `-${key}`);
2843
3241
  }
@@ -2890,26 +3288,26 @@ var WorldOrbit = (() => {
2890
3288
  ];
2891
3289
  function formatDocument(document, options = {}) {
2892
3290
  const schema = options.schema ?? "auto";
2893
- 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";
3291
+ 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";
2894
3292
  if (useDraft) {
2895
3293
  if (schema === "2.0-draft") {
2896
- const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" ? {
3294
+ const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" || document.version === "2.5" ? {
2897
3295
  ...document,
2898
3296
  version: "2.0-draft",
2899
3297
  schemaVersion: "2.0-draft"
2900
3298
  } : upgradeDocumentToDraftV2(document);
2901
3299
  return formatDraftDocument(legacyDraftDocument);
2902
3300
  }
2903
- const atlasDocument = document.version === "2.0" || document.version === "2.1" ? document : document.version === "2.0-draft" ? {
3301
+ const atlasDocument = document.version === "2.0" || document.version === "2.1" || document.version === "2.5" ? document : document.version === "2.0-draft" ? {
2904
3302
  ...document,
2905
3303
  version: "2.0",
2906
3304
  schemaVersion: "2.0"
2907
3305
  } : upgradeDocumentToV2(document);
2908
- if (schema === "2.1" && atlasDocument.version !== "2.1") {
3306
+ if ((schema === "2.0" || schema === "2.1" || schema === "2.5") && atlasDocument.version !== schema) {
2909
3307
  return formatAtlasDocument({
2910
3308
  ...atlasDocument,
2911
- version: "2.1",
2912
- schemaVersion: "2.1"
3309
+ version: schema,
3310
+ schemaVersion: schema
2913
3311
  });
2914
3312
  }
2915
3313
  return formatAtlasDocument(atlasDocument);
@@ -2941,6 +3339,10 @@ var WorldOrbit = (() => {
2941
3339
  lines.push("");
2942
3340
  lines.push(...formatAtlasRelation(relation));
2943
3341
  }
3342
+ for (const event of [...document.events].sort(compareIdLike)) {
3343
+ lines.push("");
3344
+ lines.push(...formatAtlasEvent(event));
3345
+ }
2944
3346
  const sortedObjects = [...document.objects].sort(compareObjects);
2945
3347
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2946
3348
  lines.push("");
@@ -2971,6 +3373,10 @@ var WorldOrbit = (() => {
2971
3373
  lines.push("");
2972
3374
  lines.push(...formatAtlasRelation(relation));
2973
3375
  }
3376
+ for (const event of [...legacy.events].sort(compareIdLike)) {
3377
+ lines.push("");
3378
+ lines.push(...formatAtlasEvent(event));
3379
+ }
2974
3380
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2975
3381
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2976
3382
  lines.push("");
@@ -3178,10 +3584,28 @@ var WorldOrbit = (() => {
3178
3584
  if (viewpoint.rotationDeg !== 0) {
3179
3585
  lines.push(` rotation ${viewpoint.rotationDeg}`);
3180
3586
  }
3587
+ if (viewpoint.camera && hasCameraValues(viewpoint.camera)) {
3588
+ lines.push(" camera");
3589
+ if (viewpoint.camera.azimuth !== null) {
3590
+ lines.push(` azimuth ${viewpoint.camera.azimuth}`);
3591
+ }
3592
+ if (viewpoint.camera.elevation !== null) {
3593
+ lines.push(` elevation ${viewpoint.camera.elevation}`);
3594
+ }
3595
+ if (viewpoint.camera.roll !== null) {
3596
+ lines.push(` roll ${viewpoint.camera.roll}`);
3597
+ }
3598
+ if (viewpoint.camera.distance !== null) {
3599
+ lines.push(` distance ${viewpoint.camera.distance}`);
3600
+ }
3601
+ }
3181
3602
  const layerTokens = formatDraftLayers(viewpoint.layers);
3182
3603
  if (layerTokens.length > 0) {
3183
3604
  lines.push(` layers ${layerTokens.join(" ")}`);
3184
3605
  }
3606
+ if (viewpoint.events.length > 0) {
3607
+ lines.push(` events ${viewpoint.events.join(" ")}`);
3608
+ }
3185
3609
  if (viewpoint.filter) {
3186
3610
  lines.push(" filter");
3187
3611
  if (viewpoint.filter.query) {
@@ -3254,6 +3678,65 @@ var WorldOrbit = (() => {
3254
3678
  }
3255
3679
  return lines;
3256
3680
  }
3681
+ function formatAtlasEvent(event) {
3682
+ const lines = [`event ${event.id}`, ` kind ${quoteIfNeeded(event.kind)}`];
3683
+ if (event.label) {
3684
+ lines.push(` label ${quoteIfNeeded(event.label)}`);
3685
+ }
3686
+ if (event.summary) {
3687
+ lines.push(` summary ${quoteIfNeeded(event.summary)}`);
3688
+ }
3689
+ if (event.targetObjectId) {
3690
+ lines.push(` target ${event.targetObjectId}`);
3691
+ }
3692
+ if (event.participantObjectIds.length > 0) {
3693
+ lines.push(` participants ${event.participantObjectIds.join(" ")}`);
3694
+ }
3695
+ if (event.timing) {
3696
+ lines.push(` timing ${quoteIfNeeded(event.timing)}`);
3697
+ }
3698
+ if (event.visibility) {
3699
+ lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
3700
+ }
3701
+ if (event.epoch) {
3702
+ lines.push(` epoch ${quoteIfNeeded(event.epoch)}`);
3703
+ }
3704
+ if (event.referencePlane) {
3705
+ lines.push(` referencePlane ${quoteIfNeeded(event.referencePlane)}`);
3706
+ }
3707
+ if (event.tags.length > 0) {
3708
+ lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3709
+ }
3710
+ if (event.color) {
3711
+ lines.push(` color ${quoteIfNeeded(event.color)}`);
3712
+ }
3713
+ if (event.hidden) {
3714
+ lines.push(" hidden true");
3715
+ }
3716
+ if (event.positions.length > 0) {
3717
+ lines.push("");
3718
+ lines.push(" positions");
3719
+ for (const pose of [...event.positions].sort(comparePoseObjectId)) {
3720
+ lines.push(` pose ${pose.objectId}`);
3721
+ for (const fieldLine of formatEventPoseFields(pose)) {
3722
+ lines.push(` ${fieldLine}`);
3723
+ }
3724
+ }
3725
+ }
3726
+ return lines;
3727
+ }
3728
+ function formatEventPoseFields(pose) {
3729
+ return [
3730
+ ...formatPlacement(pose.placement),
3731
+ ...pose.epoch ? [`epoch ${quoteIfNeeded(pose.epoch)}`] : [],
3732
+ ...pose.referencePlane ? [`referencePlane ${quoteIfNeeded(pose.referencePlane)}`] : [],
3733
+ ...formatOptionalUnit("inner", pose.inner),
3734
+ ...formatOptionalUnit("outer", pose.outer)
3735
+ ];
3736
+ }
3737
+ function hasCameraValues(camera) {
3738
+ return camera.azimuth !== null || camera.elevation !== null || camera.roll !== null || camera.distance !== null;
3739
+ }
3257
3740
  function formatValue(value) {
3258
3741
  if (Array.isArray(value)) {
3259
3742
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3295,7 +3778,7 @@ var WorldOrbit = (() => {
3295
3778
  if (orbitFront !== void 0 || orbitBack !== void 0) {
3296
3779
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
3297
3780
  }
3298
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3781
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
3299
3782
  if (layers[key] !== void 0) {
3300
3783
  tokens.push(layers[key] ? key : `-${key}`);
3301
3784
  }
@@ -3323,6 +3806,9 @@ var WorldOrbit = (() => {
3323
3806
  function compareIdLike(left, right) {
3324
3807
  return left.id.localeCompare(right.id);
3325
3808
  }
3809
+ function comparePoseObjectId(left, right) {
3810
+ return left.objectId.localeCompare(right.objectId);
3811
+ }
3326
3812
  function objectTypeIndex(objectType) {
3327
3813
  switch (objectType) {
3328
3814
  case "star":
@@ -3518,6 +4004,7 @@ var WorldOrbit = (() => {
3518
4004
  const diagnostics = [];
3519
4005
  const objectMap = new Map(document.objects.map((object) => [object.id, object]));
3520
4006
  const groupIds = new Set(document.groups.map((group) => group.id));
4007
+ const eventIds = new Set(document.events.map((event) => event.id));
3521
4008
  if (!document.system) {
3522
4009
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3523
4010
  }
@@ -3527,6 +4014,7 @@ var WorldOrbit = (() => {
3527
4014
  ["viewpoint", document.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3528
4015
  ["annotation", document.system?.annotations.map((annotation) => annotation.id) ?? []],
3529
4016
  ["relation", document.relations.map((relation) => relation.id)],
4017
+ ["event", document.events.map((event) => event.id)],
3530
4018
  ["object", document.objects.map((object) => object.id)]
3531
4019
  ]) {
3532
4020
  for (const id of ids) {
@@ -3542,11 +4030,14 @@ var WorldOrbit = (() => {
3542
4030
  validateRelation(relation, objectMap, diagnostics);
3543
4031
  }
3544
4032
  for (const viewpoint of document.system?.viewpoints ?? []) {
3545
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
4033
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
3546
4034
  }
3547
4035
  for (const object of document.objects) {
3548
4036
  validateObject(object, document.system, objectMap, groupIds, diagnostics);
3549
4037
  }
4038
+ for (const event of document.events) {
4039
+ validateEvent(event, document.system, objectMap, diagnostics);
4040
+ }
3550
4041
  return diagnostics;
3551
4042
  }
3552
4043
  function validateRelation(relation, objectMap, diagnostics) {
@@ -3564,15 +4055,24 @@ var WorldOrbit = (() => {
3564
4055
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3565
4056
  }
3566
4057
  }
3567
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3568
- if (!filter || sourceSchemaVersion !== "2.1") {
3569
- return;
3570
- }
3571
- for (const groupId of filter.groupIds) {
3572
- if (!groupIds.has(groupId)) {
3573
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
4058
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
4059
+ const filter = viewpoint.filter;
4060
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
4061
+ if (filter) {
4062
+ for (const groupId of filter.groupIds) {
4063
+ if (!groupIds.has(groupId)) {
4064
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
4065
+ }
4066
+ }
4067
+ }
4068
+ for (const eventId of viewpoint.events ?? []) {
4069
+ if (!eventIds.has(eventId)) {
4070
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
4071
+ }
3574
4072
  }
3575
4073
  }
4074
+ validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
4075
+ validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
3576
4076
  }
3577
4077
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
3578
4078
  const placement = object.placement;
@@ -3585,6 +4085,12 @@ var WorldOrbit = (() => {
3585
4085
  }
3586
4086
  }
3587
4087
  }
4088
+ if (typeof object.epoch === "string" && !object.epoch.trim()) {
4089
+ diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
4090
+ }
4091
+ if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
4092
+ diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
4093
+ }
3588
4094
  if (orbitPlacement) {
3589
4095
  if (!objectMap.has(orbitPlacement.target)) {
3590
4096
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -3656,6 +4162,122 @@ var WorldOrbit = (() => {
3656
4162
  }
3657
4163
  }
3658
4164
  }
4165
+ function validateEvent(event, system, objectMap, diagnostics) {
4166
+ const fieldPrefix = `event.${event.id}`;
4167
+ const referencedIds = /* @__PURE__ */ new Set();
4168
+ if (!event.kind.trim()) {
4169
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
4170
+ }
4171
+ if (typeof event.epoch === "string" && !event.epoch.trim()) {
4172
+ diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, void 0, `${fieldPrefix}.epoch`));
4173
+ }
4174
+ if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
4175
+ diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, void 0, `${fieldPrefix}.referencePlane`));
4176
+ }
4177
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
4178
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
4179
+ }
4180
+ if (event.targetObjectId) {
4181
+ referencedIds.add(event.targetObjectId);
4182
+ if (!objectMap.has(event.targetObjectId)) {
4183
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
4184
+ }
4185
+ }
4186
+ const seenParticipants = /* @__PURE__ */ new Set();
4187
+ for (const participantId of event.participantObjectIds) {
4188
+ referencedIds.add(participantId);
4189
+ if (seenParticipants.has(participantId)) {
4190
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
4191
+ continue;
4192
+ }
4193
+ seenParticipants.add(participantId);
4194
+ if (!objectMap.has(participantId)) {
4195
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
4196
+ }
4197
+ }
4198
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
4199
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
4200
+ }
4201
+ if (event.positions.length === 0) {
4202
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
4203
+ }
4204
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
4205
+ 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`));
4206
+ }
4207
+ const poseIds = /* @__PURE__ */ new Set();
4208
+ for (const pose of event.positions) {
4209
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
4210
+ if (poseIds.has(pose.objectId)) {
4211
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
4212
+ continue;
4213
+ }
4214
+ poseIds.add(pose.objectId);
4215
+ const object = objectMap.get(pose.objectId);
4216
+ if (!object) {
4217
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
4218
+ continue;
4219
+ }
4220
+ if (!referencedIds.has(pose.objectId)) {
4221
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
4222
+ }
4223
+ validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
4224
+ }
4225
+ const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
4226
+ if (event.positions.length > 0 && missingPoseIds.length > 0) {
4227
+ diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, void 0, `${fieldPrefix}.positions`));
4228
+ }
4229
+ }
4230
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
4231
+ const placement = pose.placement;
4232
+ if (!placement) {
4233
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
4234
+ return;
4235
+ }
4236
+ if (placement.mode === "orbit") {
4237
+ if (!objectMap.has(placement.target)) {
4238
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
4239
+ }
4240
+ if (placement.distance && placement.semiMajor) {
4241
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
4242
+ }
4243
+ if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
4244
+ diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, void 0, `${fieldPrefix}.phase`));
4245
+ }
4246
+ if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
4247
+ diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, void 0, `${fieldPrefix}.inclination`));
4248
+ }
4249
+ if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
4250
+ 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`));
4251
+ }
4252
+ return;
4253
+ }
4254
+ if (placement.mode === "surface") {
4255
+ const target = objectMap.get(placement.target);
4256
+ if (!target) {
4257
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
4258
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
4259
+ 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`));
4260
+ }
4261
+ return;
4262
+ }
4263
+ if (placement.mode === "at") {
4264
+ if (object.type !== "structure" && object.type !== "phenomenon") {
4265
+ 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`));
4266
+ }
4267
+ const reference = placement.reference;
4268
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
4269
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4270
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
4271
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4272
+ } else if (reference.kind === "lagrange") {
4273
+ if (!objectMap.has(reference.primary)) {
4274
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4275
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
4276
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4277
+ }
4278
+ }
4279
+ }
4280
+ }
3659
4281
  function validateAtTarget(object, objectMap, diagnostics) {
3660
4282
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3661
4283
  if (!reference) {
@@ -3761,6 +4383,52 @@ var WorldOrbit = (() => {
3761
4383
  return null;
3762
4384
  }
3763
4385
  }
4386
+ function validateProjection(projection, diagnostics, field, viewpointId) {
4387
+ if (projection !== "topdown" && projection !== "isometric" && projection !== "orthographic" && projection !== "perspective") {
4388
+ diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, void 0, field));
4389
+ }
4390
+ }
4391
+ function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
4392
+ if (!camera) {
4393
+ return;
4394
+ }
4395
+ const prefix = `viewpoint.${viewpointId}.camera`;
4396
+ for (const [key, value] of [
4397
+ ["azimuth", camera.azimuth],
4398
+ ["elevation", camera.elevation],
4399
+ ["roll", camera.roll],
4400
+ ["distance", camera.distance]
4401
+ ]) {
4402
+ if (value !== null && (!Number.isFinite(value) || key === "distance" && value <= 0)) {
4403
+ diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, void 0, `${prefix}.${key}`));
4404
+ }
4405
+ }
4406
+ if (camera.distance !== null && projection !== "perspective") {
4407
+ 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`));
4408
+ }
4409
+ if (projection === "topdown" && (camera.elevation !== null || camera.roll !== null)) {
4410
+ 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));
4411
+ }
4412
+ if (projection === "isometric" && camera.elevation !== null) {
4413
+ diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, void 0, `${prefix}.elevation`));
4414
+ }
4415
+ if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
4416
+ 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`));
4417
+ }
4418
+ const hasAnchor = focusObjectId !== null && objectMap.has(focusObjectId) || selectedObjectId !== null && objectMap.has(selectedObjectId) || !!filter;
4419
+ if (!hasAnchor) {
4420
+ diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, void 0, prefix));
4421
+ }
4422
+ }
4423
+ function resolveEffectiveEpoch(system, object, event, pose) {
4424
+ return normalizeOptionalContextString(pose?.epoch) ?? normalizeOptionalContextString(event?.epoch) ?? normalizeOptionalContextString(object.epoch) ?? normalizeOptionalContextString(system?.epoch) ?? null;
4425
+ }
4426
+ function resolveEffectiveReferencePlane(system, object, event, pose) {
4427
+ return normalizeOptionalContextString(pose?.referencePlane) ?? normalizeOptionalContextString(event?.referencePlane) ?? normalizeOptionalContextString(object.referencePlane) ?? normalizeOptionalContextString(system?.referencePlane) ?? null;
4428
+ }
4429
+ function normalizeOptionalContextString(value) {
4430
+ return typeof value === "string" && value.trim() ? value.trim() : null;
4431
+ }
3764
4432
  function toleranceForField(object, field) {
3765
4433
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3766
4434
  if (typeof tolerance === "number") {
@@ -3856,6 +4524,23 @@ var WorldOrbit = (() => {
3856
4524
  });
3857
4525
  }
3858
4526
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
4527
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
4528
+ "orbit",
4529
+ "distance",
4530
+ "semiMajor",
4531
+ "eccentricity",
4532
+ "period",
4533
+ "angle",
4534
+ "inclination",
4535
+ "phase",
4536
+ "at",
4537
+ "surface",
4538
+ "free",
4539
+ "inner",
4540
+ "outer",
4541
+ "epoch",
4542
+ "referencePlane"
4543
+ ]);
3859
4544
  function parseWorldOrbitAtlas(source) {
3860
4545
  return parseAtlasSource(source);
3861
4546
  }
@@ -3873,12 +4558,15 @@ var WorldOrbit = (() => {
3873
4558
  const objectNodes = [];
3874
4559
  const groups = [];
3875
4560
  const relations = [];
4561
+ const events = [];
4562
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3876
4563
  let sawDefaults = false;
3877
4564
  let sawAtlas = false;
3878
4565
  const viewpointIds = /* @__PURE__ */ new Set();
3879
4566
  const annotationIds = /* @__PURE__ */ new Set();
3880
4567
  const groupIds = /* @__PURE__ */ new Set();
3881
4568
  const relationIds = /* @__PURE__ */ new Set();
4569
+ const eventIds = /* @__PURE__ */ new Set();
3882
4570
  for (let index = 0; index < lines.length; index++) {
3883
4571
  const rawLine = lines[index];
3884
4572
  const lineNumber = index + 1;
@@ -3896,7 +4584,7 @@ var WorldOrbit = (() => {
3896
4584
  if (!sawSchemaHeader) {
3897
4585
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3898
4586
  sawSchemaHeader = true;
3899
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
4587
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3900
4588
  diagnostics.push({
3901
4589
  code: "parse.schema21.commentCompatibility",
3902
4590
  severity: "warning",
@@ -3909,7 +4597,7 @@ var WorldOrbit = (() => {
3909
4597
  continue;
3910
4598
  }
3911
4599
  if (indent === 0) {
3912
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
4600
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3913
4601
  if (section.kind === "system") {
3914
4602
  system = section.system;
3915
4603
  } else if (section.kind === "defaults") {
@@ -3928,6 +4616,7 @@ var WorldOrbit = (() => {
3928
4616
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3929
4617
  }
3930
4618
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4619
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3931
4620
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3932
4621
  const baseDocument = {
3933
4622
  format: "worldorbit",
@@ -3935,6 +4624,7 @@ var WorldOrbit = (() => {
3935
4624
  system,
3936
4625
  groups,
3937
4626
  relations,
4627
+ events: normalizedEvents,
3938
4628
  objects,
3939
4629
  diagnostics
3940
4630
  };
@@ -3964,13 +4654,13 @@ var WorldOrbit = (() => {
3964
4654
  return document;
3965
4655
  }
3966
4656
  function assertDraftSchemaHeader(tokens, line) {
3967
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3968
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4657
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
4658
+ 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);
3969
4659
  }
3970
4660
  const version = tokens[1].value.toLowerCase();
3971
- return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
4661
+ return version === "2.5" ? "2.5" : version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3972
4662
  }
3973
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
4663
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3974
4664
  const keyword = tokens[0]?.value.toLowerCase();
3975
4665
  switch (keyword) {
3976
4666
  case "system":
@@ -3988,6 +4678,8 @@ var WorldOrbit = (() => {
3988
4678
  return {
3989
4679
  kind: "defaults",
3990
4680
  system,
4681
+ sourceSchemaVersion,
4682
+ diagnostics,
3991
4683
  seenFields: /* @__PURE__ */ new Set()
3992
4684
  };
3993
4685
  case "atlas":
@@ -4007,7 +4699,7 @@ var WorldOrbit = (() => {
4007
4699
  if (!system) {
4008
4700
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
4009
4701
  }
4010
- return startViewpointSection(tokens, line, system, viewpointIds);
4702
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
4011
4703
  case "annotation":
4012
4704
  if (!system) {
4013
4705
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -4019,6 +4711,9 @@ var WorldOrbit = (() => {
4019
4711
  case "relation":
4020
4712
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
4021
4713
  return startRelationSection(tokens, line, relations, relationIds);
4714
+ case "event":
4715
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4716
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
4022
4717
  case "object":
4023
4718
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
4024
4719
  default:
@@ -4055,7 +4750,7 @@ var WorldOrbit = (() => {
4055
4750
  seenFields: /* @__PURE__ */ new Set()
4056
4751
  };
4057
4752
  }
4058
- function startViewpointSection(tokens, line, system, viewpointIds) {
4753
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
4059
4754
  if (tokens.length !== 2) {
4060
4755
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
4061
4756
  }
@@ -4072,10 +4767,12 @@ var WorldOrbit = (() => {
4072
4767
  summary: "",
4073
4768
  focusObjectId: null,
4074
4769
  selectedObjectId: null,
4770
+ events: [],
4075
4771
  projection: system.defaults.view,
4076
4772
  preset: system.defaults.preset,
4077
4773
  zoom: null,
4078
4774
  rotationDeg: 0,
4775
+ camera: null,
4079
4776
  layers: {},
4080
4777
  filter: null
4081
4778
  };
@@ -4084,10 +4781,15 @@ var WorldOrbit = (() => {
4084
4781
  return {
4085
4782
  kind: "viewpoint",
4086
4783
  viewpoint,
4784
+ sourceSchemaVersion,
4785
+ diagnostics,
4087
4786
  seenFields: /* @__PURE__ */ new Set(),
4088
4787
  inFilter: false,
4089
4788
  filterIndent: null,
4090
- seenFilterFields: /* @__PURE__ */ new Set()
4789
+ seenFilterFields: /* @__PURE__ */ new Set(),
4790
+ inCamera: false,
4791
+ cameraIndent: null,
4792
+ seenCameraFields: /* @__PURE__ */ new Set()
4091
4793
  };
4092
4794
  }
4093
4795
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -4174,6 +4876,51 @@ var WorldOrbit = (() => {
4174
4876
  seenFields: /* @__PURE__ */ new Set()
4175
4877
  };
4176
4878
  }
4879
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4880
+ if (tokens.length !== 2) {
4881
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4882
+ }
4883
+ const id = normalizeIdentifier2(tokens[1].value);
4884
+ if (!id) {
4885
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4886
+ }
4887
+ if (eventIds.has(id)) {
4888
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4889
+ }
4890
+ const event = {
4891
+ id,
4892
+ kind: "",
4893
+ label: humanizeIdentifier3(id),
4894
+ summary: null,
4895
+ targetObjectId: null,
4896
+ participantObjectIds: [],
4897
+ timing: null,
4898
+ visibility: null,
4899
+ epoch: null,
4900
+ referencePlane: null,
4901
+ tags: [],
4902
+ color: null,
4903
+ hidden: false,
4904
+ positions: []
4905
+ };
4906
+ const rawPoses = [];
4907
+ events.push(event);
4908
+ eventPoseNodes.set(id, rawPoses);
4909
+ eventIds.add(id);
4910
+ return {
4911
+ kind: "event",
4912
+ event,
4913
+ sourceSchemaVersion,
4914
+ diagnostics,
4915
+ seenFields: /* @__PURE__ */ new Set(),
4916
+ rawPoses,
4917
+ inPositions: false,
4918
+ positionsIndent: null,
4919
+ activePose: null,
4920
+ poseIndent: null,
4921
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4922
+ };
4923
+ }
4177
4924
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
4178
4925
  if (tokens.length < 3) {
4179
4926
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -4230,6 +4977,9 @@ var WorldOrbit = (() => {
4230
4977
  case "relation":
4231
4978
  applyRelationField(section, tokens, line);
4232
4979
  return;
4980
+ case "event":
4981
+ applyEventField(section, indent, tokens, line);
4982
+ return;
4233
4983
  case "object":
4234
4984
  applyObjectField(section, indent, tokens, line);
4235
4985
  return;
@@ -4272,6 +5022,12 @@ var WorldOrbit = (() => {
4272
5022
  const value = joinFieldValue(tokens, line);
4273
5023
  switch (key) {
4274
5024
  case "view":
5025
+ if (isSchema25Projection(value)) {
5026
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
5027
+ line,
5028
+ column: tokens[0].column
5029
+ });
5030
+ }
4275
5031
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
4276
5032
  return;
4277
5033
  case "scale":
@@ -4311,14 +5067,36 @@ var WorldOrbit = (() => {
4311
5067
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
4312
5068
  }
4313
5069
  function applyViewpointField2(section, indent, tokens, line) {
5070
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
5071
+ section.inCamera = false;
5072
+ section.cameraIndent = null;
5073
+ }
4314
5074
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
4315
5075
  section.inFilter = false;
4316
5076
  section.filterIndent = null;
4317
5077
  }
5078
+ if (section.inCamera) {
5079
+ applyViewpointCameraField(section, tokens, line);
5080
+ return;
5081
+ }
4318
5082
  if (section.inFilter) {
4319
5083
  applyViewpointFilterField(section, tokens, line);
4320
5084
  return;
4321
5085
  }
5086
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
5087
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
5088
+ line,
5089
+ column: tokens[0].column
5090
+ });
5091
+ if (section.seenFields.has("camera")) {
5092
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
5093
+ }
5094
+ section.seenFields.add("camera");
5095
+ section.inCamera = true;
5096
+ section.cameraIndent = indent;
5097
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera2();
5098
+ return;
5099
+ }
4322
5100
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
4323
5101
  if (section.seenFields.has("filter")) {
4324
5102
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -4344,6 +5122,12 @@ var WorldOrbit = (() => {
4344
5122
  section.viewpoint.selectedObjectId = value;
4345
5123
  return;
4346
5124
  case "projection":
5125
+ if (isSchema25Projection(value)) {
5126
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
5127
+ line,
5128
+ column: tokens[0].column
5129
+ });
5130
+ }
4347
5131
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
4348
5132
  return;
4349
5133
  case "preset":
@@ -4355,13 +5139,49 @@ var WorldOrbit = (() => {
4355
5139
  case "rotation":
4356
5140
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
4357
5141
  return;
5142
+ case "camera":
5143
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
5144
+ line,
5145
+ column: tokens[0].column
5146
+ });
5147
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
5148
+ return;
4358
5149
  case "layers":
4359
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
5150
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
5151
+ return;
5152
+ case "events":
5153
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
5154
+ line,
5155
+ column: tokens[0].column
5156
+ });
5157
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
4360
5158
  return;
4361
5159
  default:
4362
5160
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
4363
5161
  }
4364
5162
  }
5163
+ function applyViewpointCameraField(section, tokens, line) {
5164
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
5165
+ const value = joinFieldValue(tokens, line);
5166
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera2();
5167
+ switch (key) {
5168
+ case "azimuth":
5169
+ camera.azimuth = parseFiniteNumber2(value, line, tokens[0].column, "camera.azimuth");
5170
+ break;
5171
+ case "elevation":
5172
+ camera.elevation = parseFiniteNumber2(value, line, tokens[0].column, "camera.elevation");
5173
+ break;
5174
+ case "roll":
5175
+ camera.roll = parseFiniteNumber2(value, line, tokens[0].column, "camera.roll");
5176
+ break;
5177
+ case "distance":
5178
+ camera.distance = parsePositiveNumber2(value, line, tokens[0].column, "camera.distance");
5179
+ break;
5180
+ default:
5181
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
5182
+ }
5183
+ section.viewpoint.camera = camera;
5184
+ }
4365
5185
  function applyViewpointFilterField(section, tokens, line) {
4366
5186
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
4367
5187
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -4461,6 +5281,126 @@ var WorldOrbit = (() => {
4461
5281
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4462
5282
  }
4463
5283
  }
5284
+ function applyEventField(section, indent, tokens, line) {
5285
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
5286
+ section.activePose = null;
5287
+ section.poseIndent = null;
5288
+ section.activePoseSeenFields.clear();
5289
+ }
5290
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
5291
+ section.inPositions = false;
5292
+ section.positionsIndent = null;
5293
+ }
5294
+ if (section.activePose) {
5295
+ if (tokens[0]?.value === "epoch" || tokens[0]?.value === "referencePlane") {
5296
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
5297
+ line,
5298
+ column: tokens[0]?.column ?? 1
5299
+ });
5300
+ }
5301
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
5302
+ return;
5303
+ }
5304
+ if (section.inPositions) {
5305
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
5306
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
5307
+ }
5308
+ const objectId = tokens[1].value;
5309
+ if (!objectId.trim()) {
5310
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
5311
+ }
5312
+ const rawPose = {
5313
+ objectId,
5314
+ fields: [],
5315
+ location: { line, column: tokens[0].column }
5316
+ };
5317
+ section.rawPoses.push(rawPose);
5318
+ section.activePose = rawPose;
5319
+ section.poseIndent = indent;
5320
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
5321
+ return;
5322
+ }
5323
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
5324
+ if (section.seenFields.has("positions")) {
5325
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
5326
+ }
5327
+ section.seenFields.add("positions");
5328
+ section.inPositions = true;
5329
+ section.positionsIndent = indent;
5330
+ return;
5331
+ }
5332
+ const key = requireUniqueField(tokens, section.seenFields, line);
5333
+ switch (key) {
5334
+ case "kind":
5335
+ section.event.kind = joinFieldValue(tokens, line);
5336
+ return;
5337
+ case "label":
5338
+ section.event.label = joinFieldValue(tokens, line);
5339
+ return;
5340
+ case "summary":
5341
+ section.event.summary = joinFieldValue(tokens, line);
5342
+ return;
5343
+ case "target":
5344
+ section.event.targetObjectId = joinFieldValue(tokens, line);
5345
+ return;
5346
+ case "participants":
5347
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
5348
+ return;
5349
+ case "timing":
5350
+ section.event.timing = joinFieldValue(tokens, line);
5351
+ return;
5352
+ case "visibility":
5353
+ section.event.visibility = joinFieldValue(tokens, line);
5354
+ return;
5355
+ case "epoch":
5356
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
5357
+ line,
5358
+ column: tokens[0].column
5359
+ });
5360
+ section.event.epoch = joinFieldValue(tokens, line);
5361
+ return;
5362
+ case "referenceplane":
5363
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
5364
+ line,
5365
+ column: tokens[0].column
5366
+ });
5367
+ section.event.referencePlane = joinFieldValue(tokens, line);
5368
+ return;
5369
+ case "tags":
5370
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
5371
+ return;
5372
+ case "color":
5373
+ section.event.color = joinFieldValue(tokens, line);
5374
+ return;
5375
+ case "hidden":
5376
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
5377
+ line,
5378
+ column: tokens[0].column
5379
+ });
5380
+ return;
5381
+ default:
5382
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
5383
+ }
5384
+ }
5385
+ function parseEventPoseField(tokens, line, seenFields) {
5386
+ if (tokens.length < 2) {
5387
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
5388
+ }
5389
+ const key = tokens[0].value;
5390
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
5391
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
5392
+ }
5393
+ if (seenFields.has(key)) {
5394
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
5395
+ }
5396
+ seenFields.add(key);
5397
+ return {
5398
+ type: "field",
5399
+ key,
5400
+ values: tokens.slice(1).map((token) => token.value),
5401
+ location: { line, column: tokens[0].column }
5402
+ };
5403
+ }
4464
5404
  function applyObjectField(section, indent, tokens, line) {
4465
5405
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4466
5406
  section.activeBlock = null;
@@ -4519,7 +5459,7 @@ var WorldOrbit = (() => {
4519
5459
  function parseObjectTypeTokens(tokens, line) {
4520
5460
  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");
4521
5461
  }
4522
- function parseLayerTokens(tokens, line) {
5462
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4523
5463
  const layers = {};
4524
5464
  for (const token of parseTokenList(tokens, line, "layers")) {
4525
5465
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -4529,7 +5469,13 @@ var WorldOrbit = (() => {
4529
5469
  layers["orbits-front"] = enabled;
4530
5470
  continue;
4531
5471
  }
4532
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
5472
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
5473
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
5474
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
5475
+ line,
5476
+ column: tokens[0]?.column ?? 1
5477
+ });
5478
+ }
4533
5479
  layers[raw] = enabled;
4534
5480
  }
4535
5481
  }
@@ -4547,11 +5493,15 @@ var WorldOrbit = (() => {
4547
5493
  }
4548
5494
  function parseProjectionValue(value, line, column) {
4549
5495
  const normalized = value.toLowerCase();
4550
- if (normalized !== "topdown" && normalized !== "isometric") {
5496
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
4551
5497
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4552
5498
  }
4553
5499
  return normalized;
4554
5500
  }
5501
+ function isSchema25Projection(value) {
5502
+ const normalized = value.toLowerCase();
5503
+ return normalized === "orthographic" || normalized === "perspective";
5504
+ }
4555
5505
  function parsePresetValue(value, line, column) {
4556
5506
  const normalized = value.toLowerCase();
4557
5507
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -4581,6 +5531,48 @@ var WorldOrbit = (() => {
4581
5531
  groupIds: []
4582
5532
  };
4583
5533
  }
5534
+ function createEmptyViewCamera2() {
5535
+ return {
5536
+ azimuth: null,
5537
+ elevation: null,
5538
+ roll: null,
5539
+ distance: null
5540
+ };
5541
+ }
5542
+ function parseInlineViewCamera(tokens, line, current) {
5543
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
5544
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
5545
+ }
5546
+ const camera = current ? { ...current } : createEmptyViewCamera2();
5547
+ const seen = /* @__PURE__ */ new Set();
5548
+ for (let index = 0; index < tokens.length; index += 2) {
5549
+ const fieldToken = tokens[index];
5550
+ const valueToken = tokens[index + 1];
5551
+ const key = fieldToken.value.toLowerCase();
5552
+ if (seen.has(key)) {
5553
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5554
+ }
5555
+ seen.add(key);
5556
+ const value = valueToken.value;
5557
+ switch (key) {
5558
+ case "azimuth":
5559
+ camera.azimuth = parseFiniteNumber2(value, line, fieldToken.column, "camera.azimuth");
5560
+ break;
5561
+ case "elevation":
5562
+ camera.elevation = parseFiniteNumber2(value, line, fieldToken.column, "camera.elevation");
5563
+ break;
5564
+ case "roll":
5565
+ camera.roll = parseFiniteNumber2(value, line, fieldToken.column, "camera.roll");
5566
+ break;
5567
+ case "distance":
5568
+ camera.distance = parsePositiveNumber2(value, line, fieldToken.column, "camera.distance");
5569
+ break;
5570
+ default:
5571
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5572
+ }
5573
+ }
5574
+ return camera;
5575
+ }
4584
5576
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
4585
5577
  const fields = [];
4586
5578
  let index = 0;
@@ -4668,7 +5660,7 @@ var WorldOrbit = (() => {
4668
5660
  }
4669
5661
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4670
5662
  const fieldMap = collectDraftFields(node.fields);
4671
- const placement = extractDraftPlacement(node.objectType, fieldMap);
5663
+ const placement = extractPlacementFromFieldMap(fieldMap);
4672
5664
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
4673
5665
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4674
5666
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -4713,21 +5705,41 @@ var WorldOrbit = (() => {
4713
5705
  object.tolerances = tolerances;
4714
5706
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
4715
5707
  object.typedBlocks = typedBlocks;
4716
- if (sourceSchemaVersion !== "2.1") {
5708
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4717
5709
  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) {
4718
5710
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4719
5711
  }
4720
5712
  }
4721
5713
  return object;
4722
5714
  }
4723
- function collectDraftFields(fields) {
5715
+ function normalizeDraftEvent(event, rawPoses) {
5716
+ return {
5717
+ ...event,
5718
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5719
+ tags: [...new Set(event.tags)],
5720
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5721
+ };
5722
+ }
5723
+ function normalizeDraftEventPose(rawPose) {
5724
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
5725
+ const placement = extractPlacementFromFieldMap(fieldMap);
5726
+ return {
5727
+ objectId: rawPose.objectId,
5728
+ placement,
5729
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5730
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
5731
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
5732
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
5733
+ };
5734
+ }
5735
+ function collectDraftFields(fields, _mode = "object") {
4724
5736
  const grouped = /* @__PURE__ */ new Map();
4725
5737
  for (const field of fields) {
4726
5738
  const spec = getDraftObjectFieldSpec(field.key);
4727
- if (!spec) {
5739
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
4728
5740
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4729
5741
  }
4730
- if (!spec.allowRepeat && grouped.has(field.key)) {
5742
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
4731
5743
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4732
5744
  }
4733
5745
  const existing = grouped.get(field.key) ?? [];
@@ -4736,7 +5748,7 @@ var WorldOrbit = (() => {
4736
5748
  }
4737
5749
  return grouped;
4738
5750
  }
4739
- function extractDraftPlacement(objectType, fieldMap) {
5751
+ function extractPlacementFromFieldMap(fieldMap) {
4740
5752
  const orbitField = fieldMap.get("orbit")?.[0];
4741
5753
  const atField = fieldMap.get("at")?.[0];
4742
5754
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4904,7 +5916,7 @@ var WorldOrbit = (() => {
4904
5916
  }
4905
5917
  }
4906
5918
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4907
- if (sourceSchemaVersion === "2.1") {
5919
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4908
5920
  return;
4909
5921
  }
4910
5922
  diagnostics.push({
@@ -4916,6 +5928,34 @@ var WorldOrbit = (() => {
4916
5928
  column: location.column
4917
5929
  });
4918
5930
  }
5931
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5932
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
5933
+ return;
5934
+ }
5935
+ diagnostics.push({
5936
+ code: "parse.schema25.featureCompatibility",
5937
+ severity: "warning",
5938
+ source: "parse",
5939
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5940
+ line: location.line,
5941
+ column: location.column
5942
+ });
5943
+ }
5944
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
5945
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
5946
+ }
5947
+ function schemaVersionRank(version) {
5948
+ switch (version) {
5949
+ case "2.0-draft":
5950
+ return 0;
5951
+ case "2.0":
5952
+ return 1;
5953
+ case "2.1":
5954
+ return 2;
5955
+ case "2.5":
5956
+ return 3;
5957
+ }
5958
+ }
4919
5959
  function preprocessAtlasSource(source) {
4920
5960
  const chars = [...source];
4921
5961
  const comments = [];
@@ -5003,7 +6043,7 @@ var WorldOrbit = (() => {
5003
6043
  }
5004
6044
 
5005
6045
  // packages/core/dist/atlas-edit.js
5006
- function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
6046
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.5") {
5007
6047
  return {
5008
6048
  format: "worldorbit",
5009
6049
  version,
@@ -5029,6 +6069,7 @@ var WorldOrbit = (() => {
5029
6069
  },
5030
6070
  groups: [],
5031
6071
  relations: [],
6072
+ events: [],
5032
6073
  objects: [],
5033
6074
  diagnostics: []
5034
6075
  };
@@ -5055,6 +6096,12 @@ var WorldOrbit = (() => {
5055
6096
  for (const relation of [...document.relations].sort(compareIdLike2)) {
5056
6097
  paths.push({ kind: "relation", id: relation.id });
5057
6098
  }
6099
+ for (const event of [...document.events].sort(compareIdLike2)) {
6100
+ paths.push({ kind: "event", id: event.id });
6101
+ for (const pose of [...event.positions].sort(comparePoseObjectId2)) {
6102
+ paths.push({ kind: "event-pose", id: event.id, key: pose.objectId });
6103
+ }
6104
+ }
5058
6105
  for (const object of [...document.objects].sort(compareIdLike2)) {
5059
6106
  paths.push({ kind: "object", id: object.id });
5060
6107
  }
@@ -5070,6 +6117,10 @@ var WorldOrbit = (() => {
5070
6117
  return path.key ? document.system?.atlasMetadata[path.key] ?? null : null;
5071
6118
  case "group":
5072
6119
  return path.id ? findGroup(document, path.id) : null;
6120
+ case "event":
6121
+ return path.id ? findEvent(document, path.id) : null;
6122
+ case "event-pose":
6123
+ return path.id && path.key ? findEventPose(document, path.id, path.key) : null;
5073
6124
  case "object":
5074
6125
  return path.id ? findObject(document, path.id) : null;
5075
6126
  case "viewpoint":
@@ -5109,6 +6160,18 @@ var WorldOrbit = (() => {
5109
6160
  }
5110
6161
  upsertById(next.groups, value);
5111
6162
  return next;
6163
+ case "event":
6164
+ if (!path.id) {
6165
+ throw new Error('Event updates require an "id" value.');
6166
+ }
6167
+ upsertById(next.events, value);
6168
+ return next;
6169
+ case "event-pose":
6170
+ if (!path.id || !path.key) {
6171
+ throw new Error('Event pose updates require an event "id" and pose "key" value.');
6172
+ }
6173
+ upsertEventPose(next.events, path.id, value);
6174
+ return next;
5112
6175
  case "object":
5113
6176
  if (!path.id) {
5114
6177
  throw new Error('Object updates require an "id" value.');
@@ -5157,6 +6220,19 @@ var WorldOrbit = (() => {
5157
6220
  next.groups = next.groups.filter((group) => group.id !== path.id);
5158
6221
  }
5159
6222
  return next;
6223
+ case "event":
6224
+ if (path.id) {
6225
+ next.events = next.events.filter((event) => event.id !== path.id);
6226
+ }
6227
+ return next;
6228
+ case "event-pose":
6229
+ if (path.id && path.key) {
6230
+ const event = findEvent(next, path.id);
6231
+ if (event) {
6232
+ event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
6233
+ }
6234
+ }
6235
+ return next;
5160
6236
  case "viewpoint":
5161
6237
  if (path.id) {
5162
6238
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -5225,6 +6301,22 @@ var WorldOrbit = (() => {
5225
6301
  };
5226
6302
  }
5227
6303
  }
6304
+ if (diagnostic.field?.startsWith("event.")) {
6305
+ const parts = diagnostic.field.split(".");
6306
+ if (parts[1] && findEvent(document, parts[1])) {
6307
+ if (parts[2] === "pose" && parts[3] && findEventPose(document, parts[1], parts[3])) {
6308
+ return {
6309
+ kind: "event-pose",
6310
+ id: parts[1],
6311
+ key: parts[3]
6312
+ };
6313
+ }
6314
+ return {
6315
+ kind: "event",
6316
+ id: parts[1]
6317
+ };
6318
+ }
6319
+ }
5228
6320
  if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
5229
6321
  return {
5230
6322
  kind: "metadata",
@@ -5256,6 +6348,12 @@ var WorldOrbit = (() => {
5256
6348
  function findRelation(document, relationId) {
5257
6349
  return document.relations.find((relation) => relation.id === relationId) ?? null;
5258
6350
  }
6351
+ function findEvent(document, eventId) {
6352
+ return document.events.find((event) => event.id === eventId) ?? null;
6353
+ }
6354
+ function findEventPose(document, eventId, objectId) {
6355
+ return findEvent(document, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
6356
+ }
5259
6357
  function findViewpoint(system, viewpointId) {
5260
6358
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
5261
6359
  }
@@ -5271,13 +6369,30 @@ var WorldOrbit = (() => {
5271
6369
  }
5272
6370
  items[index] = value;
5273
6371
  }
6372
+ function upsertEventPose(events, eventId, value) {
6373
+ const event = events.find((entry) => entry.id === eventId);
6374
+ if (!event) {
6375
+ throw new Error(`Unknown event "${eventId}" for pose update.`);
6376
+ }
6377
+ const index = event.positions.findIndex((entry) => entry.objectId === value.objectId);
6378
+ if (index === -1) {
6379
+ event.positions.push(value);
6380
+ event.positions.sort(comparePoseObjectId2);
6381
+ return;
6382
+ }
6383
+ event.positions[index] = value;
6384
+ }
5274
6385
  function compareIdLike2(left, right) {
5275
6386
  return left.id.localeCompare(right.id);
5276
6387
  }
6388
+ function comparePoseObjectId2(left, right) {
6389
+ return left.objectId.localeCompare(right.objectId);
6390
+ }
5277
6391
 
5278
6392
  // packages/core/dist/load.js
5279
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
6393
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
5280
6394
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
6395
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
5281
6396
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
5282
6397
  function detectWorldOrbitSchemaVersion(source) {
5283
6398
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -5291,6 +6406,9 @@ var WorldOrbit = (() => {
5291
6406
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5292
6407
  return "2.1";
5293
6408
  }
6409
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
6410
+ return "2.5";
6411
+ }
5294
6412
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
5295
6413
  return "2.0";
5296
6414
  }
@@ -5351,7 +6469,7 @@ var WorldOrbit = (() => {
5351
6469
  }
5352
6470
  function loadWorldOrbitSourceWithDiagnostics(source) {
5353
6471
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
5354
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
6472
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1" || schemaVersion === "2.5") {
5355
6473
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
5356
6474
  }
5357
6475
  let ast;