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
@@ -568,6 +568,7 @@ var WorldOrbit = (() => {
568
568
  system,
569
569
  groups: [],
570
570
  relations: [],
571
+ events: [],
571
572
  objects
572
573
  };
573
574
  }
@@ -947,12 +948,16 @@ var WorldOrbit = (() => {
947
948
  const height = frame.height;
948
949
  const padding = frame.padding;
949
950
  const layoutPreset = resolveLayoutPreset(document2);
950
- const projection = resolveProjection(document2, options.projection);
951
+ const schemaProjection = resolveProjection(document2, options.projection);
952
+ const camera = normalizeViewCamera(options.camera ?? null);
953
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
951
954
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
952
955
  const spacingFactor = layoutPresetSpacing(layoutPreset);
953
956
  const systemId = document2.system?.id ?? null;
954
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
955
- const relationships = buildSceneRelationships(document2.objects, objectMap);
957
+ const activeEventId = options.activeEventId ?? null;
958
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
959
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
960
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
956
961
  const positions = /* @__PURE__ */ new Map();
957
962
  const orbitDrafts = [];
958
963
  const leaderDrafts = [];
@@ -961,7 +966,7 @@ var WorldOrbit = (() => {
961
966
  const atObjects = [];
962
967
  const surfaceChildren = /* @__PURE__ */ new Map();
963
968
  const orbitChildren = /* @__PURE__ */ new Map();
964
- for (const object of document2.objects) {
969
+ for (const object of effectiveObjects) {
965
970
  const placement = object.placement;
966
971
  if (!placement) {
967
972
  rootObjects.push(object);
@@ -988,7 +993,7 @@ var WorldOrbit = (() => {
988
993
  surfaceChildren,
989
994
  objectMap,
990
995
  spacingFactor,
991
- projection,
996
+ projection: renderProjection,
992
997
  scaleModel
993
998
  };
994
999
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -1000,7 +1005,7 @@ var WorldOrbit = (() => {
1000
1005
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1001
1006
  secondaryRoots.forEach((object, index) => {
1002
1007
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
1003
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
1008
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
1004
1009
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
1005
1010
  });
1006
1011
  }
@@ -1056,38 +1061,48 @@ var WorldOrbit = (() => {
1056
1061
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1057
1062
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1058
1063
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1059
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1064
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1060
1065
  const relations = createSceneRelations(document2, objects);
1061
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1062
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1066
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1067
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1068
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1063
1069
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1064
- const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1065
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1070
+ const viewpoints = createSceneViewpoints(document2, schemaProjection, frame.preset, relationships, objectMap);
1071
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1066
1072
  return {
1067
1073
  width,
1068
1074
  height,
1069
1075
  padding,
1070
1076
  renderPreset: frame.preset,
1071
- projection,
1077
+ projection: schemaProjection,
1078
+ renderProjection,
1079
+ camera,
1072
1080
  scaleModel,
1073
1081
  title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1074
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1082
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1075
1083
  systemId,
1076
- viewMode: projection,
1084
+ viewMode: schemaProjection,
1077
1085
  layoutPreset,
1078
1086
  metadata: {
1079
1087
  format: document2.format,
1080
1088
  version: document2.version,
1081
- view: projection,
1089
+ view: schemaProjection,
1090
+ renderProjection,
1082
1091
  scale: String(document2.system?.properties.scale ?? layoutPreset),
1083
1092
  units: String(document2.system?.properties.units ?? "mixed"),
1084
- preset: frame.preset ?? "custom"
1093
+ preset: frame.preset ?? "custom",
1094
+ ...camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {},
1095
+ ...camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {},
1096
+ ...camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {},
1097
+ ...camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}
1085
1098
  },
1086
1099
  contentBounds,
1087
1100
  layers,
1088
1101
  groups,
1089
1102
  semanticGroups,
1090
1103
  viewpoints,
1104
+ events,
1105
+ activeEventId,
1091
1106
  objects,
1092
1107
  orbitVisuals,
1093
1108
  relations,
@@ -1095,6 +1110,56 @@ var WorldOrbit = (() => {
1095
1110
  labels
1096
1111
  };
1097
1112
  }
1113
+ function createEffectiveObjects(objects, events, activeEventId) {
1114
+ const cloned = objects.map((object) => structuredClone(object));
1115
+ if (!activeEventId) {
1116
+ return cloned;
1117
+ }
1118
+ const activeEvent = events.find((event) => event.id === activeEventId);
1119
+ if (!activeEvent) {
1120
+ return cloned;
1121
+ }
1122
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1123
+ const referencedIds = /* @__PURE__ */ new Set([
1124
+ ...activeEvent.targetObjectId ? [activeEvent.targetObjectId] : [],
1125
+ ...activeEvent.participantObjectIds,
1126
+ ...activeEvent.positions.map((pose) => pose.objectId)
1127
+ ]);
1128
+ for (const objectId of referencedIds) {
1129
+ const object = objectMap.get(objectId);
1130
+ if (!object) {
1131
+ continue;
1132
+ }
1133
+ if (activeEvent.epoch) {
1134
+ object.epoch = activeEvent.epoch;
1135
+ }
1136
+ if (activeEvent.referencePlane) {
1137
+ object.referencePlane = activeEvent.referencePlane;
1138
+ }
1139
+ }
1140
+ for (const pose of activeEvent.positions) {
1141
+ const object = objectMap.get(pose.objectId);
1142
+ if (!object) {
1143
+ continue;
1144
+ }
1145
+ if (pose.placement) {
1146
+ object.placement = structuredClone(pose.placement);
1147
+ }
1148
+ if (pose.inner) {
1149
+ object.properties.inner = { ...pose.inner };
1150
+ }
1151
+ if (pose.outer) {
1152
+ object.properties.outer = { ...pose.outer };
1153
+ }
1154
+ if (pose.epoch) {
1155
+ object.epoch = pose.epoch;
1156
+ }
1157
+ if (pose.referencePlane) {
1158
+ object.referencePlane = pose.referencePlane;
1159
+ }
1160
+ }
1161
+ return cloned;
1162
+ }
1098
1163
  function resolveLayoutPreset(document2) {
1099
1164
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1100
1165
  switch (rawScale) {
@@ -1131,10 +1196,59 @@ var WorldOrbit = (() => {
1131
1196
  }
1132
1197
  }
1133
1198
  function resolveProjection(document2, projection) {
1134
- if (projection === "topdown" || projection === "isometric") {
1199
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1135
1200
  return projection;
1136
1201
  }
1137
- return String(document2.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1202
+ const documentView = String(document2.system?.properties.view ?? "topdown").toLowerCase();
1203
+ return parseViewProjection(documentView) ?? "topdown";
1204
+ }
1205
+ function resolveRenderProjection(projection, camera) {
1206
+ switch (projection) {
1207
+ case "topdown":
1208
+ return "topdown";
1209
+ case "isometric":
1210
+ return "isometric";
1211
+ case "orthographic":
1212
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null) ? "isometric" : "topdown";
1213
+ case "perspective":
1214
+ return "isometric";
1215
+ }
1216
+ }
1217
+ function normalizeViewCamera(camera) {
1218
+ if (!camera) {
1219
+ return null;
1220
+ }
1221
+ const normalized = {
1222
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
1223
+ elevation: normalizeFiniteCameraValue(camera.elevation),
1224
+ roll: normalizeFiniteCameraValue(camera.roll),
1225
+ distance: normalizePositiveCameraDistance(camera.distance)
1226
+ };
1227
+ return normalized.azimuth !== null || normalized.elevation !== null || normalized.roll !== null || normalized.distance !== null ? normalized : null;
1228
+ }
1229
+ function normalizeFiniteCameraValue(value) {
1230
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1231
+ }
1232
+ function normalizePositiveCameraDistance(value) {
1233
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1234
+ }
1235
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
1236
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
1237
+ if (projection !== renderProjection) {
1238
+ parts.push(`2D ${renderProjection} fallback`);
1239
+ }
1240
+ if (camera) {
1241
+ const cameraParts = [
1242
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
1243
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
1244
+ camera.roll !== null ? `roll ${camera.roll}` : null,
1245
+ camera.distance !== null ? `dist ${camera.distance}` : null
1246
+ ].filter(Boolean);
1247
+ if (cameraParts.length > 0) {
1248
+ parts.push(`camera ${cameraParts.join(" / ")}`);
1249
+ }
1250
+ }
1251
+ return parts.join(" - ");
1138
1252
  }
1139
1253
  function resolveScaleModel(layoutPreset, overrides) {
1140
1254
  const defaults = defaultScaleModel(layoutPreset);
@@ -1250,24 +1364,14 @@ var WorldOrbit = (() => {
1250
1364
  hidden: draft.object.properties.hidden === true
1251
1365
  };
1252
1366
  }
1253
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1367
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1254
1368
  const labels = [];
1255
1369
  const occupied = [];
1256
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1370
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1371
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1257
1372
  for (const object of visibleObjects) {
1258
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1259
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1260
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1261
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1262
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1263
- let attempts = 0;
1264
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1265
- labelY += direction * 14 * labelMultiplier;
1266
- secondaryY += direction * 14 * labelMultiplier;
1267
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1268
- attempts += 1;
1269
- }
1270
- occupied.push(bounds);
1373
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1374
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1271
1375
  labels.push({
1272
1376
  renderId: `${object.renderId}-label`,
1273
1377
  objectId: object.objectId,
@@ -1276,17 +1380,128 @@ var WorldOrbit = (() => {
1276
1380
  semanticGroupIds: [...object.semanticGroupIds],
1277
1381
  label: object.label,
1278
1382
  secondaryLabel: object.secondaryLabel,
1279
- x: object.x,
1280
- y: labelY,
1281
- secondaryY,
1282
- textAnchor: "middle",
1283
- direction: direction < 0 ? "above" : "below",
1383
+ x: placement.x,
1384
+ y: placement.labelY,
1385
+ secondaryY: placement.secondaryY,
1386
+ textAnchor: placement.textAnchor,
1387
+ direction: placement.direction,
1284
1388
  hidden: object.hidden
1285
1389
  });
1286
1390
  }
1287
1391
  return labels;
1288
1392
  }
1289
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1393
+ function compareLabelPlacementOrder(left, right) {
1394
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1395
+ if (priorityDiff !== 0) {
1396
+ return priorityDiff;
1397
+ }
1398
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1399
+ if (renderPriorityDiff !== 0) {
1400
+ return renderPriorityDiff;
1401
+ }
1402
+ return left.sortKey - right.sortKey;
1403
+ }
1404
+ function labelPlacementPriority(object) {
1405
+ switch (object.object.type) {
1406
+ case "star":
1407
+ return 0;
1408
+ case "planet":
1409
+ return 1;
1410
+ case "moon":
1411
+ return 2;
1412
+ case "belt":
1413
+ case "ring":
1414
+ return 3;
1415
+ case "asteroid":
1416
+ case "comet":
1417
+ return 4;
1418
+ case "structure":
1419
+ case "phenomenon":
1420
+ return 5;
1421
+ }
1422
+ }
1423
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1424
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1425
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1426
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1427
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1428
+ const rect = createLabelRect(object, placement, labelMultiplier);
1429
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1430
+ return placement;
1431
+ }
1432
+ }
1433
+ }
1434
+ return null;
1435
+ }
1436
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1437
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1438
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1439
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1440
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1441
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1442
+ 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";
1443
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1444
+ }
1445
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1446
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1447
+ return object.y >= parent.y ? "below" : "above";
1448
+ }
1449
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1450
+ }
1451
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1452
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1453
+ return object.x >= parent.x ? "right" : "left";
1454
+ }
1455
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1456
+ }
1457
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1458
+ const step = 14 * labelMultiplier;
1459
+ switch (direction) {
1460
+ case "above": {
1461
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1462
+ return {
1463
+ x: object.x,
1464
+ labelY,
1465
+ secondaryY: labelY - 16 * labelMultiplier,
1466
+ textAnchor: "middle",
1467
+ direction
1468
+ };
1469
+ }
1470
+ case "below": {
1471
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1472
+ return {
1473
+ x: object.x,
1474
+ labelY,
1475
+ secondaryY: labelY + 16 * labelMultiplier,
1476
+ textAnchor: "middle",
1477
+ direction
1478
+ };
1479
+ }
1480
+ case "left": {
1481
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1482
+ const labelY = object.y - 4 * labelMultiplier;
1483
+ return {
1484
+ x,
1485
+ labelY,
1486
+ secondaryY: labelY + 16 * labelMultiplier,
1487
+ textAnchor: "end",
1488
+ direction
1489
+ };
1490
+ }
1491
+ case "right": {
1492
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1493
+ const labelY = object.y - 4 * labelMultiplier;
1494
+ return {
1495
+ x,
1496
+ labelY,
1497
+ secondaryY: labelY + 16 * labelMultiplier,
1498
+ textAnchor: "start",
1499
+ direction
1500
+ };
1501
+ }
1502
+ }
1503
+ }
1504
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1290
1505
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1291
1506
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1292
1507
  return [
@@ -1301,6 +1516,10 @@ var WorldOrbit = (() => {
1301
1516
  id: "relations",
1302
1517
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1303
1518
  },
1519
+ {
1520
+ id: "events",
1521
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1522
+ },
1304
1523
  {
1305
1524
  id: "objects",
1306
1525
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1312,7 +1531,7 @@ var WorldOrbit = (() => {
1312
1531
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1313
1532
  ];
1314
1533
  }
1315
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1534
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1316
1535
  const groups = /* @__PURE__ */ new Map();
1317
1536
  const ensureGroup = (groupId) => {
1318
1537
  if (!groupId) {
@@ -1361,7 +1580,7 @@ var WorldOrbit = (() => {
1361
1580
  }
1362
1581
  }
1363
1582
  for (const group of groups.values()) {
1364
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1583
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1365
1584
  }
1366
1585
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1367
1586
  }
@@ -1395,6 +1614,29 @@ var WorldOrbit = (() => {
1395
1614
  };
1396
1615
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1397
1616
  }
1617
+ function createSceneEvents(events, objects, activeEventId) {
1618
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1619
+ return events.map((event) => {
1620
+ const objectIds = [.../* @__PURE__ */ new Set([
1621
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1622
+ ...event.participantObjectIds
1623
+ ])];
1624
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1625
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1626
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1627
+ return {
1628
+ renderId: `${createRenderId(event.id)}-event`,
1629
+ eventId: event.id,
1630
+ event,
1631
+ objectIds,
1632
+ participantIds: [...event.participantObjectIds],
1633
+ targetObjectId: event.targetObjectId,
1634
+ x: centroidX,
1635
+ y: centroidY,
1636
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1637
+ };
1638
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1639
+ }
1398
1640
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1399
1641
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1400
1642
  const drafts = /* @__PURE__ */ new Map();
@@ -1442,13 +1684,18 @@ var WorldOrbit = (() => {
1442
1684
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1443
1685
  const title = document2.system?.title ?? document2.system?.properties.title;
1444
1686
  const label = title ? `${String(title)} Overview` : "Overview";
1687
+ const camera = normalizeViewCamera(null);
1688
+ const renderProjection = resolveRenderProjection(projection, camera);
1445
1689
  return {
1446
1690
  id: "overview",
1447
1691
  label,
1448
1692
  summary: "Fit the whole system with the current atlas defaults.",
1449
1693
  objectId: null,
1450
1694
  selectedObjectId: null,
1695
+ eventIds: [],
1451
1696
  projection,
1697
+ renderProjection,
1698
+ camera,
1452
1699
  preset,
1453
1700
  rotationDeg: 0,
1454
1701
  scale: null,
@@ -1484,6 +1731,9 @@ var WorldOrbit = (() => {
1484
1731
  draft.select = normalizedValue;
1485
1732
  }
1486
1733
  return;
1734
+ case "events":
1735
+ draft.eventIds = splitListValue(normalizedValue);
1736
+ return;
1487
1737
  case "projection":
1488
1738
  case "view":
1489
1739
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1495,6 +1745,30 @@ var WorldOrbit = (() => {
1495
1745
  case "angle":
1496
1746
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1497
1747
  return;
1748
+ case "camera.azimuth":
1749
+ draft.camera = {
1750
+ ...draft.camera ?? createEmptyViewCamera(),
1751
+ azimuth: parseFiniteNumber(normalizedValue)
1752
+ };
1753
+ return;
1754
+ case "camera.elevation":
1755
+ draft.camera = {
1756
+ ...draft.camera ?? createEmptyViewCamera(),
1757
+ elevation: parseFiniteNumber(normalizedValue)
1758
+ };
1759
+ return;
1760
+ case "camera.roll":
1761
+ draft.camera = {
1762
+ ...draft.camera ?? createEmptyViewCamera(),
1763
+ roll: parseFiniteNumber(normalizedValue)
1764
+ };
1765
+ return;
1766
+ case "camera.distance":
1767
+ draft.camera = {
1768
+ ...draft.camera ?? createEmptyViewCamera(),
1769
+ distance: parsePositiveNumber(normalizedValue)
1770
+ };
1771
+ return;
1498
1772
  case "zoom":
1499
1773
  case "scale":
1500
1774
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -1534,13 +1808,19 @@ var WorldOrbit = (() => {
1534
1808
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1535
1809
  const filter = normalizeViewpointFilter(draft.filter);
1536
1810
  const label = draft.label?.trim() || humanizeIdentifier(draft.id);
1811
+ const resolvedProjection = draft.projection ?? projection;
1812
+ const camera = normalizeViewCamera(draft.camera ?? null);
1813
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
1537
1814
  return {
1538
1815
  id: draft.id,
1539
1816
  label,
1540
1817
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1541
1818
  objectId,
1542
1819
  selectedObjectId,
1543
- projection: draft.projection ?? projection,
1820
+ eventIds: [...new Set(draft.eventIds ?? [])],
1821
+ projection: resolvedProjection,
1822
+ renderProjection,
1823
+ camera,
1544
1824
  preset: draft.preset ?? preset,
1545
1825
  rotationDeg: draft.rotationDeg ?? 0,
1546
1826
  scale: draft.scale ?? null,
@@ -1557,6 +1837,14 @@ var WorldOrbit = (() => {
1557
1837
  groupIds: []
1558
1838
  };
1559
1839
  }
1840
+ function createEmptyViewCamera() {
1841
+ return {
1842
+ azimuth: null,
1843
+ elevation: null,
1844
+ roll: null,
1845
+ distance: null
1846
+ };
1847
+ }
1560
1848
  function normalizeViewpointFilter(filter) {
1561
1849
  if (!filter) {
1562
1850
  return null;
@@ -1570,7 +1858,18 @@ var WorldOrbit = (() => {
1570
1858
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1571
1859
  }
1572
1860
  function parseViewProjection(value) {
1573
- return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
1861
+ switch (value.toLowerCase()) {
1862
+ case "topdown":
1863
+ return "topdown";
1864
+ case "isometric":
1865
+ return "isometric";
1866
+ case "orthographic":
1867
+ return "orthographic";
1868
+ case "perspective":
1869
+ return "perspective";
1870
+ default:
1871
+ return null;
1872
+ }
1574
1873
  }
1575
1874
  function parseRenderPreset(value) {
1576
1875
  const normalized = value.toLowerCase();
@@ -1597,7 +1896,7 @@ var WorldOrbit = (() => {
1597
1896
  next["orbits-front"] = enabled;
1598
1897
  continue;
1599
1898
  }
1600
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1899
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1601
1900
  next[rawLayer] = enabled;
1602
1901
  }
1603
1902
  }
@@ -1608,7 +1907,7 @@ var WorldOrbit = (() => {
1608
1907
  }
1609
1908
  function parseViewpointGroups(value, document2, relationships, objectMap) {
1610
1909
  return splitListValue(value).map((entry) => {
1611
- if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
1910
+ if (document2.schemaVersion === "2.1" || document2.schemaVersion === "2.5" || document2.groups.some((group) => group.id === entry)) {
1612
1911
  return entry;
1613
1912
  }
1614
1913
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -1645,7 +1944,7 @@ var WorldOrbit = (() => {
1645
1944
  }
1646
1945
  return parts.join(" - ");
1647
1946
  }
1648
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1947
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1649
1948
  let minX = Number.POSITIVE_INFINITY;
1650
1949
  let minY = Number.POSITIVE_INFINITY;
1651
1950
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1675,7 +1974,7 @@ var WorldOrbit = (() => {
1675
1974
  for (const label of labels) {
1676
1975
  if (label.hidden)
1677
1976
  continue;
1678
- includeLabelBounds(label, include);
1977
+ includeLabelBounds(label, include, labelMultiplier);
1679
1978
  }
1680
1979
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1681
1980
  return createBounds(0, 0, width, height);
@@ -1713,13 +2012,10 @@ var WorldOrbit = (() => {
1713
2012
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1714
2013
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1715
2014
  }
1716
- function includeLabelBounds(label, include) {
1717
- const labelScale = 1;
1718
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1719
- include(label.x - labelHalfWidth, label.y - 18);
1720
- include(label.x + labelHalfWidth, label.y + 8);
1721
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1722
- include(label.x + labelHalfWidth, label.secondaryY + 8);
2015
+ function includeLabelBounds(label, include, labelMultiplier) {
2016
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
2017
+ include(bounds.left, bounds.top);
2018
+ include(bounds.right, bounds.bottom);
1723
2019
  }
1724
2020
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1725
2021
  if (positions.has(object.id)) {
@@ -2109,7 +2405,7 @@ var WorldOrbit = (() => {
2109
2405
  return null;
2110
2406
  }
2111
2407
  }
2112
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2408
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2113
2409
  let minX = Number.POSITIVE_INFINITY;
2114
2410
  let minY = Number.POSITIVE_INFINITY;
2115
2411
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2138,7 +2434,7 @@ var WorldOrbit = (() => {
2138
2434
  }
2139
2435
  for (const label of labels) {
2140
2436
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2141
- includeLabelBounds(label, include);
2437
+ includeLabelBounds(label, include, labelMultiplier);
2142
2438
  }
2143
2439
  }
2144
2440
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2163,12 +2459,28 @@ var WorldOrbit = (() => {
2163
2459
  }
2164
2460
  return current.id;
2165
2461
  }
2166
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2462
+ function createLabelRect(object, placement, labelMultiplier) {
2463
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2464
+ }
2465
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2466
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2467
+ const labelWidth = labelHalfWidth * 2;
2468
+ const topPadding = direction === "above" ? 18 : 12;
2469
+ const bottomPadding = direction === "above" ? 8 : 12;
2470
+ let left = x - labelHalfWidth;
2471
+ let right = x + labelHalfWidth;
2472
+ if (textAnchor === "start") {
2473
+ left = x;
2474
+ right = x + labelWidth;
2475
+ } else if (textAnchor === "end") {
2476
+ left = x - labelWidth;
2477
+ right = x;
2478
+ }
2167
2479
  return {
2168
- left: x - labelHalfWidth,
2169
- right: x + labelHalfWidth,
2170
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2171
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2480
+ left,
2481
+ right,
2482
+ top: Math.min(labelY, secondaryY) - topPadding,
2483
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2172
2484
  };
2173
2485
  }
2174
2486
  function rectsOverlap(left, right) {
@@ -2355,11 +2667,6 @@ var WorldOrbit = (() => {
2355
2667
  function customColorFor(value) {
2356
2668
  return typeof value === "string" && value.trim() ? value : void 0;
2357
2669
  }
2358
- function estimateLabelHalfWidth(object, labelMultiplier) {
2359
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2360
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2361
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2362
- }
2363
2670
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2364
2671
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2365
2672
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2376,7 +2683,7 @@ var WorldOrbit = (() => {
2376
2683
  }
2377
2684
 
2378
2685
  // packages/core/dist/draft.js
2379
- function materializeAtlasDocument(document2) {
2686
+ function materializeAtlasDocument(document2, options = {}) {
2380
2687
  const system = document2.system ? {
2381
2688
  type: "system",
2382
2689
  id: document2.system.id,
@@ -2387,6 +2694,8 @@ var WorldOrbit = (() => {
2387
2694
  properties: materializeDraftSystemProperties(document2.system),
2388
2695
  info: materializeDraftSystemInfo(document2.system)
2389
2696
  } : null;
2697
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2698
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2390
2699
  return {
2391
2700
  format: "worldorbit",
2392
2701
  version: "1.0",
@@ -2394,7 +2703,8 @@ var WorldOrbit = (() => {
2394
2703
  system,
2395
2704
  groups: structuredClone(document2.groups ?? []),
2396
2705
  relations: structuredClone(document2.relations ?? []),
2397
- objects: document2.objects.map(cloneWorldOrbitObject)
2706
+ events: document2.events.map(cloneWorldOrbitEvent),
2707
+ objects
2398
2708
  };
2399
2709
  }
2400
2710
  function cloneWorldOrbitObject(object) {
@@ -2416,6 +2726,75 @@ var WorldOrbit = (() => {
2416
2726
  info: { ...object.info }
2417
2727
  };
2418
2728
  }
2729
+ function cloneWorldOrbitEvent(event) {
2730
+ return {
2731
+ ...event,
2732
+ participantObjectIds: [...event.participantObjectIds],
2733
+ tags: [...event.tags],
2734
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2735
+ };
2736
+ }
2737
+ function cloneWorldOrbitEventPose(pose) {
2738
+ return {
2739
+ objectId: pose.objectId,
2740
+ placement: clonePlacement(pose.placement),
2741
+ inner: pose.inner ? { ...pose.inner } : void 0,
2742
+ outer: pose.outer ? { ...pose.outer } : void 0,
2743
+ epoch: pose.epoch ?? null,
2744
+ referencePlane: pose.referencePlane ?? null
2745
+ };
2746
+ }
2747
+ function clonePlacement(placement) {
2748
+ return placement ? structuredClone(placement) : null;
2749
+ }
2750
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2751
+ if (!activeEventId) {
2752
+ return;
2753
+ }
2754
+ const event = events.find((entry) => entry.id === activeEventId);
2755
+ if (!event) {
2756
+ return;
2757
+ }
2758
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2759
+ const referencedIds = /* @__PURE__ */ new Set([
2760
+ ...event.targetObjectId ? [event.targetObjectId] : [],
2761
+ ...event.participantObjectIds,
2762
+ ...event.positions.map((pose) => pose.objectId)
2763
+ ]);
2764
+ for (const objectId of referencedIds) {
2765
+ const object = objectMap.get(objectId);
2766
+ if (!object) {
2767
+ continue;
2768
+ }
2769
+ if (event.epoch) {
2770
+ object.epoch = event.epoch;
2771
+ }
2772
+ if (event.referencePlane) {
2773
+ object.referencePlane = event.referencePlane;
2774
+ }
2775
+ }
2776
+ for (const pose of event.positions) {
2777
+ const object = objectMap.get(pose.objectId);
2778
+ if (!object) {
2779
+ continue;
2780
+ }
2781
+ if (pose.placement) {
2782
+ object.placement = clonePlacement(pose.placement);
2783
+ }
2784
+ if (pose.inner) {
2785
+ object.properties.inner = { ...pose.inner };
2786
+ }
2787
+ if (pose.outer) {
2788
+ object.properties.outer = { ...pose.outer };
2789
+ }
2790
+ if (pose.epoch) {
2791
+ object.epoch = pose.epoch;
2792
+ }
2793
+ if (pose.referencePlane) {
2794
+ object.referencePlane = pose.referencePlane;
2795
+ }
2796
+ }
2797
+ }
2419
2798
  function cloneProperties(properties) {
2420
2799
  const next = {};
2421
2800
  for (const [key, value] of Object.entries(properties)) {
@@ -2488,6 +2867,18 @@ var WorldOrbit = (() => {
2488
2867
  if (viewpoint.rotationDeg !== 0) {
2489
2868
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2490
2869
  }
2870
+ if (viewpoint.camera?.azimuth !== null) {
2871
+ info2[`${prefix}.camera.azimuth`] = String(viewpoint.camera?.azimuth);
2872
+ }
2873
+ if (viewpoint.camera?.elevation !== null) {
2874
+ info2[`${prefix}.camera.elevation`] = String(viewpoint.camera?.elevation);
2875
+ }
2876
+ if (viewpoint.camera?.roll !== null) {
2877
+ info2[`${prefix}.camera.roll`] = String(viewpoint.camera?.roll);
2878
+ }
2879
+ if (viewpoint.camera?.distance !== null) {
2880
+ info2[`${prefix}.camera.distance`] = String(viewpoint.camera?.distance);
2881
+ }
2491
2882
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2492
2883
  if (serializedLayers) {
2493
2884
  info2[`${prefix}.layers`] = serializedLayers;
@@ -2504,6 +2895,9 @@ var WorldOrbit = (() => {
2504
2895
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2505
2896
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2506
2897
  }
2898
+ if (viewpoint.events.length > 0) {
2899
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2900
+ }
2507
2901
  }
2508
2902
  for (const annotation of system.annotations) {
2509
2903
  const prefix = `annotation.${annotation.id}`;
@@ -2528,7 +2922,7 @@ var WorldOrbit = (() => {
2528
2922
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2529
2923
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2530
2924
  }
2531
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2925
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2532
2926
  if (layers[key] !== void 0) {
2533
2927
  tokens.push(layers[key] ? key : `-${key}`);
2534
2928
  }
@@ -2702,6 +3096,7 @@ var WorldOrbit = (() => {
2702
3096
  const diagnostics = [];
2703
3097
  const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
2704
3098
  const groupIds = new Set(document2.groups.map((group) => group.id));
3099
+ const eventIds = new Set(document2.events.map((event) => event.id));
2705
3100
  if (!document2.system) {
2706
3101
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
2707
3102
  }
@@ -2711,6 +3106,7 @@ var WorldOrbit = (() => {
2711
3106
  ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
2712
3107
  ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
2713
3108
  ["relation", document2.relations.map((relation) => relation.id)],
3109
+ ["event", document2.events.map((event) => event.id)],
2714
3110
  ["object", document2.objects.map((object) => object.id)]
2715
3111
  ]) {
2716
3112
  for (const id of ids) {
@@ -2726,11 +3122,14 @@ var WorldOrbit = (() => {
2726
3122
  validateRelation(relation, objectMap, diagnostics);
2727
3123
  }
2728
3124
  for (const viewpoint of document2.system?.viewpoints ?? []) {
2729
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3125
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
2730
3126
  }
2731
3127
  for (const object of document2.objects) {
2732
3128
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
2733
3129
  }
3130
+ for (const event of document2.events) {
3131
+ validateEvent(event, document2.system, objectMap, diagnostics);
3132
+ }
2734
3133
  return diagnostics;
2735
3134
  }
2736
3135
  function validateRelation(relation, objectMap, diagnostics) {
@@ -2748,15 +3147,24 @@ var WorldOrbit = (() => {
2748
3147
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
2749
3148
  }
2750
3149
  }
2751
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
2752
- if (!filter || sourceSchemaVersion !== "2.1") {
2753
- return;
2754
- }
2755
- for (const groupId of filter.groupIds) {
2756
- if (!groupIds.has(groupId)) {
2757
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3150
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
3151
+ const filter = viewpoint.filter;
3152
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
3153
+ if (filter) {
3154
+ for (const groupId of filter.groupIds) {
3155
+ if (!groupIds.has(groupId)) {
3156
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
3157
+ }
3158
+ }
3159
+ }
3160
+ for (const eventId of viewpoint.events ?? []) {
3161
+ if (!eventIds.has(eventId)) {
3162
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
3163
+ }
2758
3164
  }
2759
3165
  }
3166
+ validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
3167
+ validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
2760
3168
  }
2761
3169
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
2762
3170
  const placement = object.placement;
@@ -2769,6 +3177,12 @@ var WorldOrbit = (() => {
2769
3177
  }
2770
3178
  }
2771
3179
  }
3180
+ if (typeof object.epoch === "string" && !object.epoch.trim()) {
3181
+ diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
3182
+ }
3183
+ if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
3184
+ diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
3185
+ }
2772
3186
  if (orbitPlacement) {
2773
3187
  if (!objectMap.has(orbitPlacement.target)) {
2774
3188
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -2840,6 +3254,122 @@ var WorldOrbit = (() => {
2840
3254
  }
2841
3255
  }
2842
3256
  }
3257
+ function validateEvent(event, system, objectMap, diagnostics) {
3258
+ const fieldPrefix = `event.${event.id}`;
3259
+ const referencedIds = /* @__PURE__ */ new Set();
3260
+ if (!event.kind.trim()) {
3261
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3262
+ }
3263
+ if (typeof event.epoch === "string" && !event.epoch.trim()) {
3264
+ diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, void 0, `${fieldPrefix}.epoch`));
3265
+ }
3266
+ if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
3267
+ diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, void 0, `${fieldPrefix}.referencePlane`));
3268
+ }
3269
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3270
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3271
+ }
3272
+ if (event.targetObjectId) {
3273
+ referencedIds.add(event.targetObjectId);
3274
+ if (!objectMap.has(event.targetObjectId)) {
3275
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3276
+ }
3277
+ }
3278
+ const seenParticipants = /* @__PURE__ */ new Set();
3279
+ for (const participantId of event.participantObjectIds) {
3280
+ referencedIds.add(participantId);
3281
+ if (seenParticipants.has(participantId)) {
3282
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3283
+ continue;
3284
+ }
3285
+ seenParticipants.add(participantId);
3286
+ if (!objectMap.has(participantId)) {
3287
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3288
+ }
3289
+ }
3290
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3291
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3292
+ }
3293
+ if (event.positions.length === 0) {
3294
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3295
+ }
3296
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3297
+ 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`));
3298
+ }
3299
+ const poseIds = /* @__PURE__ */ new Set();
3300
+ for (const pose of event.positions) {
3301
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3302
+ if (poseIds.has(pose.objectId)) {
3303
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3304
+ continue;
3305
+ }
3306
+ poseIds.add(pose.objectId);
3307
+ const object = objectMap.get(pose.objectId);
3308
+ if (!object) {
3309
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3310
+ continue;
3311
+ }
3312
+ if (!referencedIds.has(pose.objectId)) {
3313
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3314
+ }
3315
+ validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
3316
+ }
3317
+ const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
3318
+ if (event.positions.length > 0 && missingPoseIds.length > 0) {
3319
+ diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, void 0, `${fieldPrefix}.positions`));
3320
+ }
3321
+ }
3322
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
3323
+ const placement = pose.placement;
3324
+ if (!placement) {
3325
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3326
+ return;
3327
+ }
3328
+ if (placement.mode === "orbit") {
3329
+ if (!objectMap.has(placement.target)) {
3330
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3331
+ }
3332
+ if (placement.distance && placement.semiMajor) {
3333
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3334
+ }
3335
+ if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
3336
+ diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, void 0, `${fieldPrefix}.phase`));
3337
+ }
3338
+ if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
3339
+ diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, void 0, `${fieldPrefix}.inclination`));
3340
+ }
3341
+ if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
3342
+ 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`));
3343
+ }
3344
+ return;
3345
+ }
3346
+ if (placement.mode === "surface") {
3347
+ const target = objectMap.get(placement.target);
3348
+ if (!target) {
3349
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3350
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3351
+ 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`));
3352
+ }
3353
+ return;
3354
+ }
3355
+ if (placement.mode === "at") {
3356
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3357
+ 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`));
3358
+ }
3359
+ const reference = placement.reference;
3360
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3361
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3362
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3363
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3364
+ } else if (reference.kind === "lagrange") {
3365
+ if (!objectMap.has(reference.primary)) {
3366
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3367
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3368
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3369
+ }
3370
+ }
3371
+ }
3372
+ }
2843
3373
  function validateAtTarget(object, objectMap, diagnostics) {
2844
3374
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
2845
3375
  if (!reference) {
@@ -2945,6 +3475,52 @@ var WorldOrbit = (() => {
2945
3475
  return null;
2946
3476
  }
2947
3477
  }
3478
+ function validateProjection(projection, diagnostics, field, viewpointId) {
3479
+ if (projection !== "topdown" && projection !== "isometric" && projection !== "orthographic" && projection !== "perspective") {
3480
+ diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, void 0, field));
3481
+ }
3482
+ }
3483
+ function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
3484
+ if (!camera) {
3485
+ return;
3486
+ }
3487
+ const prefix = `viewpoint.${viewpointId}.camera`;
3488
+ for (const [key, value] of [
3489
+ ["azimuth", camera.azimuth],
3490
+ ["elevation", camera.elevation],
3491
+ ["roll", camera.roll],
3492
+ ["distance", camera.distance]
3493
+ ]) {
3494
+ if (value !== null && (!Number.isFinite(value) || key === "distance" && value <= 0)) {
3495
+ diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, void 0, `${prefix}.${key}`));
3496
+ }
3497
+ }
3498
+ if (camera.distance !== null && projection !== "perspective") {
3499
+ 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`));
3500
+ }
3501
+ if (projection === "topdown" && (camera.elevation !== null || camera.roll !== null)) {
3502
+ 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));
3503
+ }
3504
+ if (projection === "isometric" && camera.elevation !== null) {
3505
+ diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, void 0, `${prefix}.elevation`));
3506
+ }
3507
+ if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
3508
+ 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`));
3509
+ }
3510
+ const hasAnchor = focusObjectId !== null && objectMap.has(focusObjectId) || selectedObjectId !== null && objectMap.has(selectedObjectId) || !!filter;
3511
+ if (!hasAnchor) {
3512
+ diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, void 0, prefix));
3513
+ }
3514
+ }
3515
+ function resolveEffectiveEpoch(system, object, event, pose) {
3516
+ return normalizeOptionalContextString(pose?.epoch) ?? normalizeOptionalContextString(event?.epoch) ?? normalizeOptionalContextString(object.epoch) ?? normalizeOptionalContextString(system?.epoch) ?? null;
3517
+ }
3518
+ function resolveEffectiveReferencePlane(system, object, event, pose) {
3519
+ return normalizeOptionalContextString(pose?.referencePlane) ?? normalizeOptionalContextString(event?.referencePlane) ?? normalizeOptionalContextString(object.referencePlane) ?? normalizeOptionalContextString(system?.referencePlane) ?? null;
3520
+ }
3521
+ function normalizeOptionalContextString(value) {
3522
+ return typeof value === "string" && value.trim() ? value.trim() : null;
3523
+ }
2948
3524
  function toleranceForField(object, field) {
2949
3525
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
2950
3526
  if (typeof tolerance === "number") {
@@ -3040,6 +3616,23 @@ var WorldOrbit = (() => {
3040
3616
  });
3041
3617
  }
3042
3618
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3619
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3620
+ "orbit",
3621
+ "distance",
3622
+ "semiMajor",
3623
+ "eccentricity",
3624
+ "period",
3625
+ "angle",
3626
+ "inclination",
3627
+ "phase",
3628
+ "at",
3629
+ "surface",
3630
+ "free",
3631
+ "inner",
3632
+ "outer",
3633
+ "epoch",
3634
+ "referencePlane"
3635
+ ]);
3043
3636
  function parseWorldOrbitAtlas(source) {
3044
3637
  return parseAtlasSource(source);
3045
3638
  }
@@ -3054,12 +3647,15 @@ var WorldOrbit = (() => {
3054
3647
  const objectNodes = [];
3055
3648
  const groups = [];
3056
3649
  const relations = [];
3650
+ const events = [];
3651
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3057
3652
  let sawDefaults = false;
3058
3653
  let sawAtlas = false;
3059
3654
  const viewpointIds = /* @__PURE__ */ new Set();
3060
3655
  const annotationIds = /* @__PURE__ */ new Set();
3061
3656
  const groupIds = /* @__PURE__ */ new Set();
3062
3657
  const relationIds = /* @__PURE__ */ new Set();
3658
+ const eventIds = /* @__PURE__ */ new Set();
3063
3659
  for (let index = 0; index < lines.length; index++) {
3064
3660
  const rawLine = lines[index];
3065
3661
  const lineNumber = index + 1;
@@ -3077,7 +3673,7 @@ var WorldOrbit = (() => {
3077
3673
  if (!sawSchemaHeader) {
3078
3674
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3079
3675
  sawSchemaHeader = true;
3080
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3676
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3081
3677
  diagnostics.push({
3082
3678
  code: "parse.schema21.commentCompatibility",
3083
3679
  severity: "warning",
@@ -3090,7 +3686,7 @@ var WorldOrbit = (() => {
3090
3686
  continue;
3091
3687
  }
3092
3688
  if (indent === 0) {
3093
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
3689
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3094
3690
  if (section.kind === "system") {
3095
3691
  system = section.system;
3096
3692
  } else if (section.kind === "defaults") {
@@ -3109,6 +3705,7 @@ var WorldOrbit = (() => {
3109
3705
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3110
3706
  }
3111
3707
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3708
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3112
3709
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3113
3710
  const baseDocument = {
3114
3711
  format: "worldorbit",
@@ -3116,6 +3713,7 @@ var WorldOrbit = (() => {
3116
3713
  system,
3117
3714
  groups,
3118
3715
  relations,
3716
+ events: normalizedEvents,
3119
3717
  objects,
3120
3718
  diagnostics
3121
3719
  };
@@ -3145,13 +3743,13 @@ var WorldOrbit = (() => {
3145
3743
  return document2;
3146
3744
  }
3147
3745
  function assertDraftSchemaHeader(tokens, line) {
3148
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3149
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3746
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
3747
+ 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);
3150
3748
  }
3151
3749
  const version = tokens[1].value.toLowerCase();
3152
- return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3750
+ return version === "2.5" ? "2.5" : version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3153
3751
  }
3154
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
3752
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3155
3753
  const keyword = tokens[0]?.value.toLowerCase();
3156
3754
  switch (keyword) {
3157
3755
  case "system":
@@ -3169,6 +3767,8 @@ var WorldOrbit = (() => {
3169
3767
  return {
3170
3768
  kind: "defaults",
3171
3769
  system,
3770
+ sourceSchemaVersion,
3771
+ diagnostics,
3172
3772
  seenFields: /* @__PURE__ */ new Set()
3173
3773
  };
3174
3774
  case "atlas":
@@ -3188,7 +3788,7 @@ var WorldOrbit = (() => {
3188
3788
  if (!system) {
3189
3789
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3190
3790
  }
3191
- return startViewpointSection(tokens, line, system, viewpointIds);
3791
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3192
3792
  case "annotation":
3193
3793
  if (!system) {
3194
3794
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3200,6 +3800,9 @@ var WorldOrbit = (() => {
3200
3800
  case "relation":
3201
3801
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3202
3802
  return startRelationSection(tokens, line, relations, relationIds);
3803
+ case "event":
3804
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
3805
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3203
3806
  case "object":
3204
3807
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3205
3808
  default:
@@ -3236,7 +3839,7 @@ var WorldOrbit = (() => {
3236
3839
  seenFields: /* @__PURE__ */ new Set()
3237
3840
  };
3238
3841
  }
3239
- function startViewpointSection(tokens, line, system, viewpointIds) {
3842
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3240
3843
  if (tokens.length !== 2) {
3241
3844
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3242
3845
  }
@@ -3253,10 +3856,12 @@ var WorldOrbit = (() => {
3253
3856
  summary: "",
3254
3857
  focusObjectId: null,
3255
3858
  selectedObjectId: null,
3859
+ events: [],
3256
3860
  projection: system.defaults.view,
3257
3861
  preset: system.defaults.preset,
3258
3862
  zoom: null,
3259
3863
  rotationDeg: 0,
3864
+ camera: null,
3260
3865
  layers: {},
3261
3866
  filter: null
3262
3867
  };
@@ -3265,10 +3870,15 @@ var WorldOrbit = (() => {
3265
3870
  return {
3266
3871
  kind: "viewpoint",
3267
3872
  viewpoint,
3873
+ sourceSchemaVersion,
3874
+ diagnostics,
3268
3875
  seenFields: /* @__PURE__ */ new Set(),
3269
3876
  inFilter: false,
3270
3877
  filterIndent: null,
3271
- seenFilterFields: /* @__PURE__ */ new Set()
3878
+ seenFilterFields: /* @__PURE__ */ new Set(),
3879
+ inCamera: false,
3880
+ cameraIndent: null,
3881
+ seenCameraFields: /* @__PURE__ */ new Set()
3272
3882
  };
3273
3883
  }
3274
3884
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -3355,6 +3965,51 @@ var WorldOrbit = (() => {
3355
3965
  seenFields: /* @__PURE__ */ new Set()
3356
3966
  };
3357
3967
  }
3968
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
3969
+ if (tokens.length !== 2) {
3970
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
3971
+ }
3972
+ const id = normalizeIdentifier(tokens[1].value);
3973
+ if (!id) {
3974
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
3975
+ }
3976
+ if (eventIds.has(id)) {
3977
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
3978
+ }
3979
+ const event = {
3980
+ id,
3981
+ kind: "",
3982
+ label: humanizeIdentifier2(id),
3983
+ summary: null,
3984
+ targetObjectId: null,
3985
+ participantObjectIds: [],
3986
+ timing: null,
3987
+ visibility: null,
3988
+ epoch: null,
3989
+ referencePlane: null,
3990
+ tags: [],
3991
+ color: null,
3992
+ hidden: false,
3993
+ positions: []
3994
+ };
3995
+ const rawPoses = [];
3996
+ events.push(event);
3997
+ eventPoseNodes.set(id, rawPoses);
3998
+ eventIds.add(id);
3999
+ return {
4000
+ kind: "event",
4001
+ event,
4002
+ sourceSchemaVersion,
4003
+ diagnostics,
4004
+ seenFields: /* @__PURE__ */ new Set(),
4005
+ rawPoses,
4006
+ inPositions: false,
4007
+ positionsIndent: null,
4008
+ activePose: null,
4009
+ poseIndent: null,
4010
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4011
+ };
4012
+ }
3358
4013
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3359
4014
  if (tokens.length < 3) {
3360
4015
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -3411,6 +4066,9 @@ var WorldOrbit = (() => {
3411
4066
  case "relation":
3412
4067
  applyRelationField(section, tokens, line);
3413
4068
  return;
4069
+ case "event":
4070
+ applyEventField(section, indent, tokens, line);
4071
+ return;
3414
4072
  case "object":
3415
4073
  applyObjectField(section, indent, tokens, line);
3416
4074
  return;
@@ -3453,6 +4111,12 @@ var WorldOrbit = (() => {
3453
4111
  const value = joinFieldValue(tokens, line);
3454
4112
  switch (key) {
3455
4113
  case "view":
4114
+ if (isSchema25Projection(value)) {
4115
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
4116
+ line,
4117
+ column: tokens[0].column
4118
+ });
4119
+ }
3456
4120
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
3457
4121
  return;
3458
4122
  case "scale":
@@ -3492,14 +4156,36 @@ var WorldOrbit = (() => {
3492
4156
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
3493
4157
  }
3494
4158
  function applyViewpointField2(section, indent, tokens, line) {
4159
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
4160
+ section.inCamera = false;
4161
+ section.cameraIndent = null;
4162
+ }
3495
4163
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
3496
4164
  section.inFilter = false;
3497
4165
  section.filterIndent = null;
3498
4166
  }
4167
+ if (section.inCamera) {
4168
+ applyViewpointCameraField(section, tokens, line);
4169
+ return;
4170
+ }
3499
4171
  if (section.inFilter) {
3500
4172
  applyViewpointFilterField(section, tokens, line);
3501
4173
  return;
3502
4174
  }
4175
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
4176
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4177
+ line,
4178
+ column: tokens[0].column
4179
+ });
4180
+ if (section.seenFields.has("camera")) {
4181
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
4182
+ }
4183
+ section.seenFields.add("camera");
4184
+ section.inCamera = true;
4185
+ section.cameraIndent = indent;
4186
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4187
+ return;
4188
+ }
3503
4189
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
3504
4190
  if (section.seenFields.has("filter")) {
3505
4191
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -3525,6 +4211,12 @@ var WorldOrbit = (() => {
3525
4211
  section.viewpoint.selectedObjectId = value;
3526
4212
  return;
3527
4213
  case "projection":
4214
+ if (isSchema25Projection(value)) {
4215
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
4216
+ line,
4217
+ column: tokens[0].column
4218
+ });
4219
+ }
3528
4220
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
3529
4221
  return;
3530
4222
  case "preset":
@@ -3536,13 +4228,49 @@ var WorldOrbit = (() => {
3536
4228
  case "rotation":
3537
4229
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3538
4230
  return;
4231
+ case "camera":
4232
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4233
+ line,
4234
+ column: tokens[0].column
4235
+ });
4236
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
4237
+ return;
3539
4238
  case "layers":
3540
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4239
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4240
+ return;
4241
+ case "events":
4242
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4243
+ line,
4244
+ column: tokens[0].column
4245
+ });
4246
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3541
4247
  return;
3542
4248
  default:
3543
4249
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
3544
4250
  }
3545
4251
  }
4252
+ function applyViewpointCameraField(section, tokens, line) {
4253
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
4254
+ const value = joinFieldValue(tokens, line);
4255
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4256
+ switch (key) {
4257
+ case "azimuth":
4258
+ camera.azimuth = parseFiniteNumber2(value, line, tokens[0].column, "camera.azimuth");
4259
+ break;
4260
+ case "elevation":
4261
+ camera.elevation = parseFiniteNumber2(value, line, tokens[0].column, "camera.elevation");
4262
+ break;
4263
+ case "roll":
4264
+ camera.roll = parseFiniteNumber2(value, line, tokens[0].column, "camera.roll");
4265
+ break;
4266
+ case "distance":
4267
+ camera.distance = parsePositiveNumber2(value, line, tokens[0].column, "camera.distance");
4268
+ break;
4269
+ default:
4270
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
4271
+ }
4272
+ section.viewpoint.camera = camera;
4273
+ }
3546
4274
  function applyViewpointFilterField(section, tokens, line) {
3547
4275
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
3548
4276
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -3642,6 +4370,126 @@ var WorldOrbit = (() => {
3642
4370
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
3643
4371
  }
3644
4372
  }
4373
+ function applyEventField(section, indent, tokens, line) {
4374
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4375
+ section.activePose = null;
4376
+ section.poseIndent = null;
4377
+ section.activePoseSeenFields.clear();
4378
+ }
4379
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4380
+ section.inPositions = false;
4381
+ section.positionsIndent = null;
4382
+ }
4383
+ if (section.activePose) {
4384
+ if (tokens[0]?.value === "epoch" || tokens[0]?.value === "referencePlane") {
4385
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
4386
+ line,
4387
+ column: tokens[0]?.column ?? 1
4388
+ });
4389
+ }
4390
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4391
+ return;
4392
+ }
4393
+ if (section.inPositions) {
4394
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4395
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4396
+ }
4397
+ const objectId = tokens[1].value;
4398
+ if (!objectId.trim()) {
4399
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4400
+ }
4401
+ const rawPose = {
4402
+ objectId,
4403
+ fields: [],
4404
+ location: { line, column: tokens[0].column }
4405
+ };
4406
+ section.rawPoses.push(rawPose);
4407
+ section.activePose = rawPose;
4408
+ section.poseIndent = indent;
4409
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4410
+ return;
4411
+ }
4412
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4413
+ if (section.seenFields.has("positions")) {
4414
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4415
+ }
4416
+ section.seenFields.add("positions");
4417
+ section.inPositions = true;
4418
+ section.positionsIndent = indent;
4419
+ return;
4420
+ }
4421
+ const key = requireUniqueField(tokens, section.seenFields, line);
4422
+ switch (key) {
4423
+ case "kind":
4424
+ section.event.kind = joinFieldValue(tokens, line);
4425
+ return;
4426
+ case "label":
4427
+ section.event.label = joinFieldValue(tokens, line);
4428
+ return;
4429
+ case "summary":
4430
+ section.event.summary = joinFieldValue(tokens, line);
4431
+ return;
4432
+ case "target":
4433
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4434
+ return;
4435
+ case "participants":
4436
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4437
+ return;
4438
+ case "timing":
4439
+ section.event.timing = joinFieldValue(tokens, line);
4440
+ return;
4441
+ case "visibility":
4442
+ section.event.visibility = joinFieldValue(tokens, line);
4443
+ return;
4444
+ case "epoch":
4445
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
4446
+ line,
4447
+ column: tokens[0].column
4448
+ });
4449
+ section.event.epoch = joinFieldValue(tokens, line);
4450
+ return;
4451
+ case "referenceplane":
4452
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
4453
+ line,
4454
+ column: tokens[0].column
4455
+ });
4456
+ section.event.referencePlane = joinFieldValue(tokens, line);
4457
+ return;
4458
+ case "tags":
4459
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4460
+ return;
4461
+ case "color":
4462
+ section.event.color = joinFieldValue(tokens, line);
4463
+ return;
4464
+ case "hidden":
4465
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4466
+ line,
4467
+ column: tokens[0].column
4468
+ });
4469
+ return;
4470
+ default:
4471
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4472
+ }
4473
+ }
4474
+ function parseEventPoseField(tokens, line, seenFields) {
4475
+ if (tokens.length < 2) {
4476
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4477
+ }
4478
+ const key = tokens[0].value;
4479
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4480
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4481
+ }
4482
+ if (seenFields.has(key)) {
4483
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4484
+ }
4485
+ seenFields.add(key);
4486
+ return {
4487
+ type: "field",
4488
+ key,
4489
+ values: tokens.slice(1).map((token) => token.value),
4490
+ location: { line, column: tokens[0].column }
4491
+ };
4492
+ }
3645
4493
  function applyObjectField(section, indent, tokens, line) {
3646
4494
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
3647
4495
  section.activeBlock = null;
@@ -3700,7 +4548,7 @@ var WorldOrbit = (() => {
3700
4548
  function parseObjectTypeTokens(tokens, line) {
3701
4549
  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");
3702
4550
  }
3703
- function parseLayerTokens(tokens, line) {
4551
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
3704
4552
  const layers = {};
3705
4553
  for (const token of parseTokenList(tokens, line, "layers")) {
3706
4554
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -3710,7 +4558,13 @@ var WorldOrbit = (() => {
3710
4558
  layers["orbits-front"] = enabled;
3711
4559
  continue;
3712
4560
  }
3713
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4561
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4562
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4563
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4564
+ line,
4565
+ column: tokens[0]?.column ?? 1
4566
+ });
4567
+ }
3714
4568
  layers[raw] = enabled;
3715
4569
  }
3716
4570
  }
@@ -3728,11 +4582,15 @@ var WorldOrbit = (() => {
3728
4582
  }
3729
4583
  function parseProjectionValue(value, line, column) {
3730
4584
  const normalized = value.toLowerCase();
3731
- if (normalized !== "topdown" && normalized !== "isometric") {
4585
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
3732
4586
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3733
4587
  }
3734
4588
  return normalized;
3735
4589
  }
4590
+ function isSchema25Projection(value) {
4591
+ const normalized = value.toLowerCase();
4592
+ return normalized === "orthographic" || normalized === "perspective";
4593
+ }
3736
4594
  function parsePresetValue(value, line, column) {
3737
4595
  const normalized = value.toLowerCase();
3738
4596
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -3762,6 +4620,48 @@ var WorldOrbit = (() => {
3762
4620
  groupIds: []
3763
4621
  };
3764
4622
  }
4623
+ function createEmptyViewCamera2() {
4624
+ return {
4625
+ azimuth: null,
4626
+ elevation: null,
4627
+ roll: null,
4628
+ distance: null
4629
+ };
4630
+ }
4631
+ function parseInlineViewCamera(tokens, line, current) {
4632
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
4633
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
4634
+ }
4635
+ const camera = current ? { ...current } : createEmptyViewCamera2();
4636
+ const seen = /* @__PURE__ */ new Set();
4637
+ for (let index = 0; index < tokens.length; index += 2) {
4638
+ const fieldToken = tokens[index];
4639
+ const valueToken = tokens[index + 1];
4640
+ const key = fieldToken.value.toLowerCase();
4641
+ if (seen.has(key)) {
4642
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
4643
+ }
4644
+ seen.add(key);
4645
+ const value = valueToken.value;
4646
+ switch (key) {
4647
+ case "azimuth":
4648
+ camera.azimuth = parseFiniteNumber2(value, line, fieldToken.column, "camera.azimuth");
4649
+ break;
4650
+ case "elevation":
4651
+ camera.elevation = parseFiniteNumber2(value, line, fieldToken.column, "camera.elevation");
4652
+ break;
4653
+ case "roll":
4654
+ camera.roll = parseFiniteNumber2(value, line, fieldToken.column, "camera.roll");
4655
+ break;
4656
+ case "distance":
4657
+ camera.distance = parsePositiveNumber2(value, line, fieldToken.column, "camera.distance");
4658
+ break;
4659
+ default:
4660
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
4661
+ }
4662
+ }
4663
+ return camera;
4664
+ }
3765
4665
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3766
4666
  const fields = [];
3767
4667
  let index = 0;
@@ -3849,7 +4749,7 @@ var WorldOrbit = (() => {
3849
4749
  }
3850
4750
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
3851
4751
  const fieldMap = collectDraftFields(node.fields);
3852
- const placement = extractDraftPlacement(node.objectType, fieldMap);
4752
+ const placement = extractPlacementFromFieldMap(fieldMap);
3853
4753
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
3854
4754
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
3855
4755
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -3894,21 +4794,41 @@ var WorldOrbit = (() => {
3894
4794
  object.tolerances = tolerances;
3895
4795
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
3896
4796
  object.typedBlocks = typedBlocks;
3897
- if (sourceSchemaVersion !== "2.1") {
4797
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3898
4798
  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) {
3899
4799
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
3900
4800
  }
3901
4801
  }
3902
4802
  return object;
3903
4803
  }
3904
- function collectDraftFields(fields) {
4804
+ function normalizeDraftEvent(event, rawPoses) {
4805
+ return {
4806
+ ...event,
4807
+ participantObjectIds: [...new Set(event.participantObjectIds)],
4808
+ tags: [...new Set(event.tags)],
4809
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
4810
+ };
4811
+ }
4812
+ function normalizeDraftEventPose(rawPose) {
4813
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
4814
+ const placement = extractPlacementFromFieldMap(fieldMap);
4815
+ return {
4816
+ objectId: rawPose.objectId,
4817
+ placement,
4818
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
4819
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
4820
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
4821
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
4822
+ };
4823
+ }
4824
+ function collectDraftFields(fields, _mode = "object") {
3905
4825
  const grouped = /* @__PURE__ */ new Map();
3906
4826
  for (const field of fields) {
3907
4827
  const spec = getDraftObjectFieldSpec(field.key);
3908
- if (!spec) {
4828
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
3909
4829
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
3910
4830
  }
3911
- if (!spec.allowRepeat && grouped.has(field.key)) {
4831
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
3912
4832
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
3913
4833
  }
3914
4834
  const existing = grouped.get(field.key) ?? [];
@@ -3917,7 +4837,7 @@ var WorldOrbit = (() => {
3917
4837
  }
3918
4838
  return grouped;
3919
4839
  }
3920
- function extractDraftPlacement(objectType, fieldMap) {
4840
+ function extractPlacementFromFieldMap(fieldMap) {
3921
4841
  const orbitField = fieldMap.get("orbit")?.[0];
3922
4842
  const atField = fieldMap.get("at")?.[0];
3923
4843
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4085,7 +5005,7 @@ var WorldOrbit = (() => {
4085
5005
  }
4086
5006
  }
4087
5007
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4088
- if (sourceSchemaVersion === "2.1") {
5008
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4089
5009
  return;
4090
5010
  }
4091
5011
  diagnostics.push({
@@ -4097,6 +5017,34 @@ var WorldOrbit = (() => {
4097
5017
  column: location.column
4098
5018
  });
4099
5019
  }
5020
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5021
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
5022
+ return;
5023
+ }
5024
+ diagnostics.push({
5025
+ code: "parse.schema25.featureCompatibility",
5026
+ severity: "warning",
5027
+ source: "parse",
5028
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5029
+ line: location.line,
5030
+ column: location.column
5031
+ });
5032
+ }
5033
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
5034
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
5035
+ }
5036
+ function schemaVersionRank(version) {
5037
+ switch (version) {
5038
+ case "2.0-draft":
5039
+ return 0;
5040
+ case "2.0":
5041
+ return 1;
5042
+ case "2.1":
5043
+ return 2;
5044
+ case "2.5":
5045
+ return 3;
5046
+ }
5047
+ }
4100
5048
  function preprocessAtlasSource(source) {
4101
5049
  const chars = [...source];
4102
5050
  const comments = [];
@@ -4184,8 +5132,9 @@ var WorldOrbit = (() => {
4184
5132
  }
4185
5133
 
4186
5134
  // packages/core/dist/load.js
4187
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5135
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
4188
5136
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
5137
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
4189
5138
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
4190
5139
  function detectWorldOrbitSchemaVersion(source) {
4191
5140
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -4199,6 +5148,9 @@ var WorldOrbit = (() => {
4199
5148
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
4200
5149
  return "2.1";
4201
5150
  }
5151
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
5152
+ return "2.5";
5153
+ }
4202
5154
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
4203
5155
  return "2.0";
4204
5156
  }
@@ -4259,7 +5211,7 @@ var WorldOrbit = (() => {
4259
5211
  }
4260
5212
  function loadWorldOrbitSourceWithDiagnostics(source) {
4261
5213
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
4262
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
5214
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1" || schemaVersion === "2.5") {
4263
5215
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
4264
5216
  }
4265
5217
  let ast;
@@ -4353,6 +5305,7 @@ var WorldOrbit = (() => {
4353
5305
  background: true,
4354
5306
  guides: true,
4355
5307
  relations: true,
5308
+ events: true,
4356
5309
  orbits: true,
4357
5310
  objects: true,
4358
5311
  labels: true,
@@ -4556,6 +5509,7 @@ var WorldOrbit = (() => {
4556
5509
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
4557
5510
  const leaderMarkup = layers.guides ? scene.leaders.filter((leader) => !leader.hidden).filter((leader) => visibleObjectIds.has(leader.objectId)).filter((leader) => layers.structures || !isStructureLike(leader.object)).map((leader) => `<line class="wo-leader wo-leader-${leader.mode}" x1="${leader.x1}" y1="${leader.y1}" x2="${leader.x2}" y2="${leader.y2}" data-render-id="${escapeXml(leader.renderId)}" data-group-id="${escapeAttribute(leader.groupId ?? "")}" />`).join("") : "";
4558
5511
  const relationMarkup = layers.relations ? scene.relations.filter((relation) => !relation.hidden).filter((relation) => visibleObjectIds.has(relation.fromObjectId) && visibleObjectIds.has(relation.toObjectId)).map((relation) => `<line class="wo-relation" x1="${relation.x1}" y1="${relation.y1}" x2="${relation.x2}" y2="${relation.y2}" data-render-id="${escapeXml(relation.renderId)}" data-relation-id="${escapeAttribute(relation.relationId)}" />`).join("") : "";
5512
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
4559
5513
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
4560
5514
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
4561
5515
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -4591,6 +5545,9 @@ var WorldOrbit = (() => {
4591
5545
  .wo-orbit-front { opacity: 0.9; }
4592
5546
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
4593
5547
  .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5548
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5549
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5550
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
4594
5551
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
4595
5552
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
4596
5553
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -4625,6 +5582,7 @@ var WorldOrbit = (() => {
4625
5582
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
4626
5583
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
4627
5584
  ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5585
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
4628
5586
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
4629
5587
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
4630
5588
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -4632,6 +5590,20 @@ var WorldOrbit = (() => {
4632
5590
  </g>
4633
5591
  </g>
4634
5592
  </svg>`;
5593
+ }
5594
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5595
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5596
+ if (participants.length === 0) {
5597
+ return "";
5598
+ }
5599
+ const stroke = event.event.color || theme.accent;
5600
+ const label = event.event.label || event.event.id;
5601
+ const lineMarkup = participants.map((object) => `<line class="wo-event-line" x1="${event.x}" y1="${event.y}" x2="${object.x}" y2="${object.y}" stroke="${escapeAttribute(stroke)}" data-event-id="${escapeAttribute(event.eventId)}" data-object-id="${escapeAttribute(object.objectId)}" />`).join("");
5602
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5603
+ ${lineMarkup}
5604
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5605
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5606
+ </g>`;
4635
5607
  }
4636
5608
  function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
4637
5609
  const backParts = [];